/** * 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