🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
273 lines
10 KiB
JavaScript
Executable File
273 lines
10 KiB
JavaScript
Executable File
/**
|
|
* Unified JQHTML Compiler Module
|
|
*
|
|
* Single source of truth for compiling JQHTML templates to JavaScript
|
|
* with proper sourcemap generation and version injection.
|
|
*/
|
|
import { Lexer } from './lexer.js';
|
|
import { Parser } from './parser.js';
|
|
import { CodeGenerator } from './codegen.js';
|
|
/**
|
|
* Compile a JQHTML template to JavaScript
|
|
*
|
|
* @param source - The JQHTML template source code
|
|
* @param filename - The source filename for sourcemap generation
|
|
* @param options - Compilation options
|
|
* @returns The compiled JavaScript code as a string
|
|
*/
|
|
export function compileTemplate(source, filename, options) {
|
|
// Validate: content() must be called with <%= %> not <% %>
|
|
if (source.includes('<% content()')) {
|
|
throw new Error(`Invalid content() usage in ${filename}:\n\n` +
|
|
`content() must be output using <%= %> tags, not <% %> tags.\n\n` +
|
|
`Wrong: <% content() %>\n` +
|
|
`Right: <%= content() %>\n\n` +
|
|
`This ensures the content is properly rendered as output.`);
|
|
}
|
|
// 1. Parse the template
|
|
const lexer = new Lexer(source);
|
|
const tokens = lexer.tokenize();
|
|
const parser = new Parser(tokens, source, filename);
|
|
const ast = parser.parse();
|
|
// 2. Generate code WITHOUT sourcemap (we'll create it after wrapping)
|
|
const generator = new CodeGenerator();
|
|
const result = generator.generate(ast, filename, source);
|
|
// 3. Get component info
|
|
const componentName = result.components.keys().next().value;
|
|
if (!componentName) {
|
|
throw new Error('No component found in template');
|
|
}
|
|
const component = result.components.get(componentName);
|
|
if (!component) {
|
|
throw new Error(`Component ${componentName} not found in results`);
|
|
}
|
|
// 4. Apply format wrapping and version injection
|
|
const version = options.version || getPackageVersion();
|
|
let output = formatOutput(component, componentName, options.format, version);
|
|
// 5. Generate sourcemap AFTER wrapping (if requested)
|
|
if (options.sourcemap) {
|
|
const sourcemap = generateSourcemapForWrappedCode(output, source, filename);
|
|
output = output + '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' + sourcemap;
|
|
}
|
|
return {
|
|
code: output,
|
|
componentName
|
|
};
|
|
}
|
|
/**
|
|
* Get package version from package.json
|
|
* We can't use dynamic import in TypeScript, so we'll inject at build time
|
|
*/
|
|
function getPackageVersion() {
|
|
// This will be replaced by the build process
|
|
// The CLI will pass the version directly
|
|
return '__PARSER_VERSION__';
|
|
}
|
|
/**
|
|
* Serialize defineArgs/defaultAttributes with proper handling of identifiers and expressions
|
|
* Quoted values become strings, identifiers/expressions become raw JavaScript
|
|
*/
|
|
function serializeAttributeObject(obj) {
|
|
if (!obj || Object.keys(obj).length === 0) {
|
|
return '{}';
|
|
}
|
|
const entries = [];
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
// Check if value is a parsed attribute object with type info
|
|
if (value && typeof value === 'object' && (value.identifier || value.expression)) {
|
|
// Identifier or expression - output as raw JavaScript (no quotes)
|
|
entries.push(`"${key}": ${value.value}`);
|
|
}
|
|
else if (value && typeof value === 'object' && value.quoted) {
|
|
// Quoted string - output as string literal
|
|
entries.push(`"${key}": ${JSON.stringify(value.value)}`);
|
|
}
|
|
else {
|
|
// Simple value - output as-is via JSON.stringify
|
|
entries.push(`"${key}": ${JSON.stringify(value)}`);
|
|
}
|
|
}
|
|
return `{${entries.join(', ')}}`;
|
|
}
|
|
/**
|
|
* Format the generated code according to the specified module format
|
|
* Moved from CLI compiler's formatOutput function
|
|
*/
|
|
function formatOutput(componentInfo, componentName, format, version) {
|
|
const name = componentInfo.name;
|
|
// Build the component definition
|
|
let componentDef = `{
|
|
_jqhtml_version: '${version}',
|
|
name: '${componentInfo.name}',
|
|
tag: '${componentInfo.tagName}',
|
|
defaultAttributes: ${serializeAttributeObject(componentInfo.defaultAttributes)},`;
|
|
// Add defineArgs if present ($ attributes from Define tag)
|
|
if (componentInfo.defineArgs) {
|
|
componentDef += `\n defineArgs: ${serializeAttributeObject(componentInfo.defineArgs)},`;
|
|
}
|
|
// Add extends if present (template inheritance)
|
|
if (componentInfo.extends) {
|
|
componentDef += `\n extends: '${componentInfo.extends}',`;
|
|
}
|
|
componentDef += `\n render: ${componentInfo.render_function.trimEnd()},
|
|
dependencies: ${JSON.stringify(componentInfo.dependencies)}
|
|
}`;
|
|
let output;
|
|
switch (format) {
|
|
case 'iife':
|
|
// Self-executing function that auto-registers with window.jqhtml
|
|
output = `// Compiled from: ${componentName}.jqhtml
|
|
(function() {
|
|
'use strict';
|
|
|
|
const template_${name} = ${componentDef};
|
|
|
|
// Self-register with jqhtml runtime
|
|
// Must use window.jqhtml since we're in bundle scope
|
|
if (!window.jqhtml) {
|
|
throw new Error('FATAL: window.jqhtml is not defined. The jqhtml runtime must be loaded before registering templates.');
|
|
}
|
|
|
|
// Auto-register following standard jqhtml pattern
|
|
window.jqhtml.register_template(template_${name});
|
|
})();`;
|
|
break;
|
|
case 'esm':
|
|
// ES Module export with auto-registration
|
|
output = `// ES Module: ${name}
|
|
import jqhtml from '@jqhtml/core';
|
|
|
|
const template_${name} = ${componentDef};
|
|
|
|
// Auto-register following standard jqhtml pattern
|
|
jqhtml.register_template(template_${name});
|
|
|
|
export { template_${name} };
|
|
export default template_${name};`;
|
|
break;
|
|
case 'cjs':
|
|
// CommonJS export with auto-registration
|
|
output = `// CommonJS Module: ${name}
|
|
'use strict';
|
|
|
|
const template_${name} = ${componentDef};
|
|
|
|
// Auto-register if jqhtml is available
|
|
if (typeof window !== 'undefined' && window.jqhtml) {
|
|
window.jqhtml.register_template(template_${name});
|
|
}
|
|
|
|
module.exports = template_${name};
|
|
module.exports.default = template_${name};
|
|
module.exports.template_${name} = template_${name};`;
|
|
break;
|
|
case 'umd':
|
|
// Universal Module Definition with auto-registration
|
|
output = `(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD
|
|
define(['@jqhtml/core'], factory);
|
|
} else if (typeof module === 'object' && module.exports) {
|
|
// CommonJS
|
|
module.exports = factory();
|
|
} else {
|
|
// Browser global
|
|
root.template_${name} = factory();
|
|
}
|
|
}(typeof self !== 'undefined' ? self : this, function (jqhtml) {
|
|
'use strict';
|
|
|
|
const template_${name} = ${componentDef};
|
|
|
|
// Auto-register with jqhtml runtime
|
|
if (typeof window !== 'undefined' && window.jqhtml) {
|
|
window.jqhtml.register_template(template_${name});
|
|
} else if (jqhtml) {
|
|
jqhtml.register_template(template_${name});
|
|
}
|
|
|
|
return template_${name};
|
|
}));`;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown format: ${format}`);
|
|
}
|
|
return output;
|
|
}
|
|
/**
|
|
* Generate a sourcemap for already-wrapped code
|
|
* This generates sourcemaps AFTER the wrapper has been applied
|
|
*/
|
|
function generateSourcemapForWrappedCode(wrappedCode, sourceContent, filename) {
|
|
// Count lines in wrapped output and source
|
|
const outputLines = wrappedCode.split('\n').length;
|
|
const sourceLines = sourceContent.split('\n').length;
|
|
// Find where the render function (template content) starts
|
|
const renderLineOffset = findRenderFunctionLine(wrappedCode);
|
|
if (renderLineOffset === 0) {
|
|
// Couldn't find render function, generate a basic mapping
|
|
console.warn('Could not find render function in wrapped output');
|
|
const mappings = new Array(outputLines).fill('AAAA').join(';');
|
|
const sourcemap = {
|
|
version: 3,
|
|
sources: [filename],
|
|
sourcesContent: [sourceContent],
|
|
mappings: mappings,
|
|
names: []
|
|
};
|
|
return Buffer.from(JSON.stringify(sourcemap)).toString('base64');
|
|
}
|
|
// Build mappings:
|
|
// 1. Lines before render content → all map to source line 1
|
|
// 2. Template content lines → map 1:1 to source lines
|
|
// 3. Lines after template content → all map to last source line
|
|
const mappings = [];
|
|
// Wrapper lines before template content
|
|
for (let i = 0; i < renderLineOffset - 1; i++) {
|
|
mappings.push('AAAA'); // Map to source line 1, column 0
|
|
}
|
|
// Template content lines (1:1 mapping)
|
|
// First line of template maps to source line 1
|
|
mappings.push('AAAA'); // Source line 1
|
|
// Remaining source lines map sequentially
|
|
for (let i = 1; i < sourceLines; i++) {
|
|
mappings.push('AACA'); // Each subsequent source line
|
|
}
|
|
// Any remaining wrapper lines after template content
|
|
const remainingLines = outputLines - mappings.length;
|
|
for (let i = 0; i < remainingLines; i++) {
|
|
mappings.push('AAAA'); // Map to last source line
|
|
}
|
|
// Create the sourcemap object
|
|
const sourcemap = {
|
|
version: 3,
|
|
sources: [filename],
|
|
sourcesContent: [sourceContent],
|
|
mappings: mappings.join(';'),
|
|
names: []
|
|
};
|
|
// Verify we have the right count
|
|
const finalCount = mappings.length;
|
|
if (finalCount !== outputLines) {
|
|
console.error(`Warning: Sourcemap line mismatch. Output has ${outputLines} lines, sourcemap has ${finalCount} mapping segments`);
|
|
}
|
|
return Buffer.from(JSON.stringify(sourcemap)).toString('base64');
|
|
}
|
|
/**
|
|
* Find the line number where template content starts in the wrapped output
|
|
* Returns 1-based line number
|
|
*/
|
|
function findRenderFunctionLine(outputCode) {
|
|
const lines = outputCode.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
// Look for the render function definition
|
|
if (lines[i].includes('render: function render(')) {
|
|
// Template content starts on the line AFTER the function declaration
|
|
// The function declaration ends with "{ let _output = []; ..."
|
|
// The next line is either empty or starts with template content
|
|
return i + 2; // i+1 for 1-based, +1 for next line after declaration
|
|
}
|
|
}
|
|
return 0; // Not found
|
|
}
|
|
//# sourceMappingURL=compiler.js.map
|