Standardize settings file naming and relocate documentation files Fix code quality violations from rsx:check Reorganize user_management directory into logical subdirectories Move Quill Bundle to core and align with Tom Select pattern Simplify Site Settings page to focus on core site information Complete Phase 5: Multi-tenant authentication with login flow and site selection Add route query parameter rule and synchronize filename validation logic Fix critical bug in UpdateNpmCommand causing missing JavaScript stubs Implement filename convention rule and resolve VS Code auto-rename conflict Implement js-sanitizer RPC server to eliminate 900+ Node.js process spawns Implement RPC server architecture for JavaScript parsing WIP: Add RPC server infrastructure for JS parsing (partial implementation) Update jqhtml terminology from destroy to stop, fix datagrid DOM preservation Add JQHTML-CLASS-01 rule and fix redundant class names Improve code quality rules and resolve violations Remove legacy fatal error format in favor of unified 'fatal' error type Filter internal keys from window.rsxapp output Update button styling and comprehensive form/modal documentation Add conditional fly-in animation for modals Fix non-deterministic bundle compilation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
273 lines
10 KiB
JavaScript
273 lines
10 KiB
JavaScript
/**
|
|
* 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
|