UNPKG

veko

Version:

Ultra-lightweight Node.js framework with ZERO dependencies - VSV components, Tailwind CSS, asset imports, SSR

1,704 lines (1,420 loc) 58.9 kB
/** * VSV Compiler * Compiles .jsv and .tsv files to optimized JavaScript * Zero dependencies - generates both server and client code */ const VSVParser = require('./parser'); class VSVCompiler { constructor(vsv) { this.vsv = vsv; this.parser = new VSVParser(vsv); } /** * Compile VSV source code */ async compile(source, options = {}) { try { // Try simple compilation first for basic JSV files const simpleResult = this.compileSimple(source, options); if (simpleResult) { return simpleResult; } // Fall back to full AST parsing const ast = this.parser.parse(source, options); const transformed = this.transform(ast); const serverCode = this.generateServer(transformed, options); const clientCode = this.generateClient(transformed, options); const finalServer = options.minify ? this.minify(serverCode) : serverCode; const finalClient = options.minify ? this.minify(clientCode) : clientCode; return { ast: transformed, server: finalServer, client: finalClient, name: options.name, hash: this.hash(source), dependencies: this.extractDependencies(ast), metadata: { components: ast.components.map(c => c.name), hasState: this.hasState(ast), hasEffects: this.hasEffects(ast), isSSR: true } }; } catch (error) { throw new Error(`Compilation failed: ${error.message}`); } } /** * Simple compilation for standard JSV files * Handles: export default function Name(props) { return <jsx> } */ compileSimple(source, options = {}) { const name = options.name || 'Component'; // Detect if this is a simple component file const hasExportDefault = /export\s+default\s+function/.test(source); const hasFunctionDecl = /function\s+\w+\s*\(/.test(source); const hasArrowExport = /export\s+default\s+(\(|[a-zA-Z])/.test(source); if (!hasExportDefault && !hasFunctionDecl && !hasArrowExport) { return null; // Not a simple component, use full parser } // Extract component name from source FIRST const nameMatch = source.match(/export\s+default\s+function\s+(\w+)/) || source.match(/function\s+(\w+)\s*\(/); const componentName = nameMatch ? nameMatch[1] : name; // Step 0: Extract asset imports (CSS, JS, images, fonts, etc.) const assets = this.extractAssetImports(source, options.filename); // Step 1: Remove all export/import statements let code = source; // Remove import statements code = code.replace(/import\s+[\s\S]*?from\s+['"][^'"]+['"];?\s*/g, ''); code = code.replace(/import\s+['"][^'"]+['"];?\s*/g, ''); // Transform "export default function Name" -> "function Name" code = code.replace(/export\s+default\s+function\s+(\w+)/g, 'function $1'); // Transform "export default (props) => " -> "function ComponentName(props)" code = code.replace(/export\s+default\s+\(([^)]*)\)\s*=>\s*/, `function ${componentName}($1) { return `); // Transform "export default Name" at end of file -> remove code = code.replace(/export\s+default\s+\w+\s*;?\s*$/g, ''); // Transform "export function" -> "function" code = code.replace(/export\s+function/g, 'function'); // Transform "export const/let/var" -> "const/let/var" code = code.replace(/export\s+(const|let|var)/g, '$1'); // Remove any remaining "export" keywords code = code.replace(/export\s*\{[^}]*\}\s*;?/g, ''); // Remove any remaining stray "export" keywords that might remain code = code.replace(/^export\s+/gm, ''); // Step 2: Transform JSX to VSV.h() calls code = this.transformJSX(code); // Step 3: Generate asset variable declarations // For imports like `import logo from './logo.png'`, inject `var logo = "/_vsv/assets/hash.png"` const assetVarDecls = this.generateAssetVarDeclarations(assets); // Generate server code with the cleaned and transformed code const serverCode = this.wrapServerCode(assetVarDecls + code, componentName); // Generate client code with cleaned code (no exports, transformed JSX) const clientCode = this.wrapClientCode(assetVarDecls + code, componentName); // Detect state/effects const hasState = /\$state\s*\(/.test(source); const hasEffects = /\$effect\s*\(/.test(source); return { ast: { source, name: componentName }, server: serverCode, client: clientCode, name: componentName, hash: this.hash(source), dependencies: [], assets, metadata: { components: [componentName], hasState, hasEffects, isSSR: true } }; } /** * Extract asset imports from source code (CSS, JS, images, fonts, etc.) * Supports: import './style.css', import styles from './module.css', * import logo from './logo.png', import script from './utils.js' */ extractAssetImports(source, filename) { const assets = { css: [], js: [], images: [], fonts: [], other: [] }; // Asset file extensions map const assetTypes = { css: ['.css', '.scss', '.sass', '.less'], js: ['.js', '.mjs'], images: ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.avif', '.bmp'], fonts: ['.woff', '.woff2', '.ttf', '.eot', '.otf'] }; // Match: import './file.css' or import styles from './file.css' // Match: import name from './file.ext' const importRegex = /import\s+(?:(\w+)\s+from\s+)?['"]([^'"]+)['"]\s*;?/g; let match; while ((match = importRegex.exec(source)) !== null) { const varName = match[1] || null; const importPath = match[2]; // Skip node module imports (no ./ or ../ prefix and no file extension) if (!importPath.startsWith('.') && !importPath.startsWith('/') && !importPath.includes('.')) { continue; } // Determine file extension const ext = importPath.includes('.') ? '.' + importPath.split('.').pop().toLowerCase() : ''; // Resolve actual file path let resolvedPath = importPath; if (filename) { const path = require('path'); const dir = path.dirname(filename); resolvedPath = path.resolve(dir, importPath); } // Classify the asset let classified = false; for (const [type, extensions] of Object.entries(assetTypes)) { if (extensions.includes(ext)) { assets[type].push({ path: importPath, resolvedPath, varName, ext }); classified = true; break; } } if (!classified && ext) { assets.other.push({ path: importPath, resolvedPath, varName, ext }); } } return assets; } /** * Generate variable declarations for imported assets * Converts: import logo from './logo.png' -> var logo = "/_vsv/assets/hash.png" * For CSS with varName: import styles from './module.css' -> var styles = "/_vsv/assets/hash.css" */ generateAssetVarDeclarations(assets) { const crypto = require('crypto'); const fs = require('fs'); let declarations = ''; const allAssets = [ ...(assets.css || []), ...(assets.js || []), ...(assets.images || []), ...(assets.fonts || []), ...(assets.other || []) ]; for (const asset of allAssets) { if (!asset.varName) continue; // Generate a deterministic hash from the file path let content; try { if (fs.existsSync(asset.resolvedPath)) { content = fs.readFileSync(asset.resolvedPath); } } catch (e) { // File not accessible yet, use path-based hash } const hashSource = content || asset.resolvedPath || asset.path; const hash = crypto.createHash('md5').update(hashSource).digest('hex').slice(0, 12); const ext = asset.ext || ''; const url = `/_vsv/assets/${hash}${ext}`; declarations += `var ${asset.varName} = ${JSON.stringify(url)};\n`; } return declarations; } /** * Transform ES6 exports to CommonJS compatible code */ transformExports(code) { let result = code; // export default function Name -> function Name result = result.replace(/export\s+default\s+function\s+(\w+)/g, 'function $1'); // export default Name -> (handled at end) result = result.replace(/export\s+default\s+(\w+)\s*;?/g, ''); // export function Name -> function Name result = result.replace(/export\s+function\s+(\w+)/g, 'function $1'); // export const/let/var Name -> const/let/var Name result = result.replace(/export\s+(const|let|var)\s+/g, '$1 '); // export { ... } -> remove result = result.replace(/export\s*\{[^}]*\}\s*;?/g, ''); // import statements -> remove (not supported in simple mode) result = result.replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, ''); result = result.replace(/import\s+['"][^'"]+['"];?/g, ''); return result; } /** * Transform JSX to VSV.h() calls using a proper recursive parser */ transformJSX(code) { return this.parseJSXInCode(code); } /** * Find and transform all JSX in code while preserving non-JSX parts */ parseJSXInCode(code) { let result = ''; let i = 0; while (i < code.length) { // Look for return statement or assignment with JSX if (code[i] === '<' && this.isJSXStart(code, i)) { const jsxResult = this.parseJSXElement(code, i); if (jsxResult) { result += jsxResult.code; i = jsxResult.end; continue; } } result += code[i]; i++; } return result; } /** * Check if this is the start of a JSX element (not a comparison operator) */ isJSXStart(code, pos) { // Must be followed by a letter (tag name) or > (fragment) or / if (pos + 1 >= code.length) return false; const next = code[pos + 1]; return /[a-zA-Z>\/]/.test(next); } /** * Parse a complete JSX element starting at position * Returns { code: string, end: number } or null */ parseJSXElement(code, start) { let i = start; // Skip < if (code[i] !== '<') return null; i++; // Handle fragments: <>...</> if (code[i] === '>') { return this.parseFragment(code, start); } // Handle closing tag - shouldn't happen at top level if (code[i] === '/') return null; // Get tag name let tagName = ''; while (i < code.length && /[a-zA-Z0-9_]/.test(code[i])) { tagName += code[i]; i++; } if (!tagName) return null; // Parse attributes const attrResult = this.parseJSXAttributes(code, i); i = attrResult.end; // Skip whitespace while (i < code.length && /\s/.test(code[i])) i++; // Self-closing tag? if (code[i] === '/' && code[i + 1] === '>') { const isComponent = /^[A-Z]/.test(tagName); const tagStr = isComponent ? tagName : `"${tagName}"`; return { code: `VSV.h(${tagStr}, ${attrResult.props})`, end: i + 2 }; } // Must be > if (code[i] !== '>') return null; i++; // Parse children const childResult = this.parseJSXChildren(code, i, tagName); i = childResult.end; // Build the VSV.h() call const isComponent = /^[A-Z]/.test(tagName); const tagStr = isComponent ? tagName : `"${tagName}"`; if (childResult.children.length === 0) { return { code: `VSV.h(${tagStr}, ${attrResult.props})`, end: i }; } return { code: `VSV.h(${tagStr}, ${attrResult.props}, ${childResult.children.join(', ')})`, end: i }; } /** * Parse JSX attributes: key="value" key={expr} @event={handler} */ parseJSXAttributes(code, start) { let i = start; const props = []; while (i < code.length) { // Skip whitespace while (i < code.length && /\s/.test(code[i])) i++; // End of attributes? if (code[i] === '>' || code[i] === '/') break; // Event handler: @click={handler} if (code[i] === '@') { i++; let eventName = ''; while (i < code.length && /[a-zA-Z0-9_]/.test(code[i])) { eventName += code[i]; i++; } // Skip = if (code[i] === '=') i++; // Get value if (code[i] === '{') { const exprResult = this.parseJSXExpression(code, i); const propName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1); props.push(`"${propName}": ${exprResult.expr}`); i = exprResult.end; } continue; } // Regular attribute let attrName = ''; while (i < code.length && /[a-zA-Z0-9_-]/.test(code[i])) { attrName += code[i]; i++; } if (!attrName) break; // Convert class to className const propName = attrName === 'class' ? 'className' : attrName; // Skip whitespace while (i < code.length && /\s/.test(code[i])) i++; // Boolean attribute (no = sign) if (code[i] !== '=') { props.push(`"${propName}": true`); continue; } i++; // Skip = // Skip whitespace while (i < code.length && /\s/.test(code[i])) i++; // String value: "..." if (code[i] === '"') { i++; let value = ''; while (i < code.length && code[i] !== '"') { if (code[i] === '\\' && i + 1 < code.length) { value += code[i] + code[i + 1]; i += 2; } else { value += code[i]; i++; } } i++; // Skip closing " props.push(`"${propName}": "${value}"`); continue; } // Single quote value: '...' if (code[i] === "'") { i++; let value = ''; while (i < code.length && code[i] !== "'") { if (code[i] === '\\' && i + 1 < code.length) { value += code[i] + code[i + 1]; i += 2; } else { value += code[i]; i++; } } i++; // Skip closing ' props.push(`"${propName}": "${value}"`); continue; } // Expression value: {...} if (code[i] === '{') { const exprResult = this.parseJSXExpression(code, i); props.push(`"${propName}": ${exprResult.expr}`); i = exprResult.end; continue; } } return { props: props.length > 0 ? `{ ${props.join(', ')} }` : 'null', end: i }; } /** * Parse a JSX expression: { ... } */ parseJSXExpression(code, start) { let i = start; if (code[i] !== '{') return { expr: '', end: i }; i++; // Skip { let depth = 1; let expr = ''; while (i < code.length && depth > 0) { if (code[i] === '{') depth++; else if (code[i] === '}') depth--; if (depth > 0) { expr += code[i]; } i++; } return { expr: expr.trim(), end: i }; } /** * Parse JSX children until closing tag */ parseJSXChildren(code, start, parentTag) { let i = start; const children = []; let textBuffer = ''; const flushText = () => { if (!textBuffer) return; const lines = textBuffer.split('\n'); // Find the last line with non-whitespace content let lastNonEmptyLine = -1; for (let l = lines.length - 1; l >= 0; l--) { if (lines[l].trim()) { lastNonEmptyLine = l; break; } } if (lastNonEmptyLine === -1) { textBuffer = ''; return; } const parts = []; for (let l = 0; l < lines.length; l++) { let line = lines[l].replace(/\t/g, ' '); // Trim start for non-first lines (removes indentation) if (l !== 0) { line = line.trimStart(); } // Trim end for non-last-non-empty lines if (l !== lastNonEmptyLine) { line = line.trimEnd(); } if (line) { if (parts.length > 0) { parts.push(' '); } parts.push(line); } } const text = parts.join(''); if (text) { children.push(JSON.stringify(text)); } textBuffer = ''; }; while (i < code.length) { // Check for closing tag if (code[i] === '<' && code[i + 1] === '/') { flushText(); // Verify it's our closing tag let closeTag = ''; let j = i + 2; while (j < code.length && /[a-zA-Z0-9_]/.test(code[j])) { closeTag += code[j]; j++; } if (closeTag === parentTag) { // Skip to after > while (j < code.length && code[j] !== '>') j++; return { children, end: j + 1 }; } // Not our closing tag - this shouldn't happen in valid JSX textBuffer += code[i]; i++; continue; } // Check for nested element if (code[i] === '<' && this.isJSXStart(code, i)) { flushText(); const childResult = this.parseJSXElement(code, i); if (childResult) { children.push(childResult.code); i = childResult.end; continue; } } // Check for expression: { ... } if (code[i] === '{') { flushText(); const exprResult = this.parseJSXExpression(code, i); if (exprResult.expr) { children.push(exprResult.expr); } i = exprResult.end; continue; } // Regular text textBuffer += code[i]; i++; } flushText(); return { children, end: i }; } /** * Parse a fragment: <>...</> */ parseFragment(code, start) { let i = start + 2; // Skip <> const children = []; let textBuffer = ''; const flushText = () => { if (!textBuffer) return; const lines = textBuffer.split('\n'); let lastNonEmptyLine = -1; for (let l = lines.length - 1; l >= 0; l--) { if (lines[l].trim()) { lastNonEmptyLine = l; break; } } if (lastNonEmptyLine === -1) { textBuffer = ''; return; } const parts = []; for (let l = 0; l < lines.length; l++) { let line = lines[l].replace(/\t/g, ' '); if (l !== 0) { line = line.trimStart(); } if (l !== lastNonEmptyLine) { line = line.trimEnd(); } if (line) { if (parts.length > 0) { parts.push(' '); } parts.push(line); } } const text = parts.join(''); if (text) { children.push(JSON.stringify(text)); } textBuffer = ''; }; while (i < code.length) { // Check for closing fragment: </> if (code[i] === '<' && code[i + 1] === '/' && code[i + 2] === '>') { flushText(); return { code: `VSV.h(VSV.Fragment, null${children.length > 0 ? ', ' + children.join(', ') : ''})`, end: i + 3 }; } // Check for nested element if (code[i] === '<' && this.isJSXStart(code, i)) { flushText(); const childResult = this.parseJSXElement(code, i); if (childResult) { children.push(childResult.code); i = childResult.end; continue; } } // Check for expression if (code[i] === '{') { flushText(); const exprResult = this.parseJSXExpression(code, i); if (exprResult.expr) { children.push(exprResult.expr); } i = exprResult.end; continue; } textBuffer += code[i]; i++; } flushText(); return { code: `VSV.h(VSV.Fragment, null${children.length > 0 ? ', ' + children.join(', ') : ''})`, end: i }; } /** * Transform VSV directives */ transformDirectives(code) { let result = code; // $state(value) stays as is - handled by runtime // $computed(() => ...) stays as is // $effect(() => ...) stays as is // $ref(initial) stays as is // $memo(() => ..., deps) stays as is return result; } /** * Transform event handlers */ transformEvents(code) { // @click -> onClick (already handled in parseJSXAttributes) return code; } /** * Wrap code for server execution */ wrapServerCode(code, name) { return ` // VSV Server Component: ${name} // Auto-generated - do not edit const VSV = { h: function(tag, props) { var children = Array.prototype.slice.call(arguments, 2); return { tag: tag, props: props || {}, children: children.flat().filter(function(c) { return c != null && c !== false; }) }; }, Fragment: Symbol('Fragment'), renderToString: function(vnode) { if (vnode == null || vnode === false) return ''; if (typeof vnode === 'string' || typeof vnode === 'number') return String(vnode); if (Array.isArray(vnode)) return vnode.map(function(v) { return VSV.renderToString(v); }).join(''); var tag = vnode.tag, props = vnode.props, children = vnode.children; if (tag === VSV.Fragment) { return children.map(function(c) { return VSV.renderToString(c); }).join(''); } if (typeof tag === 'function') { return VSV.renderToString(tag(props)); } var attrs = ''; for (var key in props) { if (key === 'children' || key.indexOf('on') === 0) continue; var value = props[key]; if (key === 'className') key = 'class'; if (value === true) attrs += ' ' + key; else if (value !== false && value != null) { attrs += ' ' + key + '="' + String(value).replace(/"/g, '&quot;') + '"'; } } var selfClosing = ['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']; if (selfClosing.indexOf(tag) !== -1) { return '<' + tag + attrs + '/>'; } var childrenStr = children.map(function(c) { return VSV.renderToString(c); }).join(''); return '<' + tag + attrs + '>' + childrenStr + '</' + tag + '>'; } }; // Server-side hooks (static for SSR) function $state(initial) { var value = typeof initial === 'function' ? initial() : initial; return [function() { return value; }, function(v) { value = typeof v === 'function' ? v(value) : v; }]; } function $computed(fn) { return fn; } function $effect(fn) { /* No-op on server */ } function $layoutEffect(fn) { /* No-op on server */ } function $ref(initial) { return { current: initial !== undefined ? initial : null }; } function $memo(fn) { return fn(); } function $callback(fn) { return fn; } function $reducer(reducer, init, initFn) { var s = initFn ? initFn(init) : init; return [s, function(a) { s = reducer(s, a); }]; } function $id() { return '_vsv_ssr_' + Math.random().toString(36).slice(2, 8); } function $transition() { return [false, function(fn) { fn(); }]; } function $deferred(v) { return v; } function $context(ctx) { return ctx._defaultValue || ctx._currentValue; } function $imperativeHandle() {} function $debugValue() {} function $syncExternalStore(sub, snap) { return snap(); } function $form(config) { var v = config.initialValues || {}; return { values: function() { return v; }, errors: function() { return {}; }, touched: function() { return {}; }, submitting: function() { return false; }, handleChange: function(){}, handleBlur: function(){}, handleSubmit: function(e){ if(e&&e.preventDefault)e.preventDefault(); if(config.onSubmit)config.onSubmit(v); }, setFieldValue: function(){}, setFieldError: function(){}, reset: function(){}, isValid: function(){return true;} }; } function $fetch() { return { data: function(){ return null; }, loading: function(){ return true; }, error: function(){ return null; }, refetch: function(){} }; } function $location() { return { pathname: '/', params: {}, query: {}, navigate: function(){} }; } function $params() { return {}; } function $query() { return {}; } function $navigate() { return function(){}; } function $animation() { return { ref: { current: null }, start: function(){}, running: function(){ return false; } }; } // Server-side components function Show(props) { return props.when ? props.children : (props.fallback || null); } function For(props) { var items = props.each || []; return typeof props.children === 'function' ? items.map(props.children) : null; } function Switch(props) { var ch = Array.isArray(props.children) ? props.children : [props.children]; for(var i=0;i<ch.length;i++){ if(ch[i]&&ch[i].props&&ch[i].props.when) return ch[i].props.children||ch[i]; } return props.fallback||null; } function Match(props) { return props.children; } function Head() { return null; } function createContext(defaultValue) { return { _defaultValue: defaultValue, _currentValue: defaultValue, Provider: function(p){ return p.children; }, Consumer: function(p){ return typeof p.children==='function'?p.children(defaultValue):p.children; } }; } function createStore(initialState) { var s = Object.assign({}, initialState); return { getState: function(){ return s; }, setState: function(p){ s = Object.assign({}, s, typeof p==='function'?p(s):p); }, subscribe: function(){ return function(){}; }, use: function(sel){ return sel?sel(s):s; } }; } // Server-side PropTypes (validation stubs for SSR) var PropTypes = (function() { function noop() { return null; } function mk() { var f = noop; f.isRequired = noop; return f; } return { string: mk(), number: mk(), bool: mk(), func: mk(), object: mk(), array: mk(), symbol: mk(), node: mk(), element: mk(), any: mk(), arrayOf: mk, objectOf: mk, oneOf: mk, oneOfType: mk, shape: mk, exact: mk, instanceOf: mk, custom: mk }; })(); function forwardRef(fn) { return function(props) { return fn(props, props.ref); }; } function memo(Comp) { return Comp; } function lazy(loader) { return function(props) { return VSV.h('div', null, 'Loading...'); }; } var Children = { map: function(ch, fn) { return (Array.isArray(ch)?ch:[ch]).filter(function(c){return c!=null;}).map(fn); }, forEach: function(ch, fn) { Children.map(ch, fn); }, count: function(ch) { return ch ? (Array.isArray(ch)?ch.filter(function(c){return c!=null;}).length:1) : 0; }, toArray: function(ch) { return ch ? (Array.isArray(ch)?ch.filter(function(c){return c!=null;}):[ch]) : []; }, only: function(ch) { if(Children.count(ch)!==1) throw new Error('Children.only expects one child'); return Array.isArray(ch)?ch[0]:ch; } }; ${code} // Export the component module.exports = typeof ${name} === 'function' ? ${name} : function(props) { return VSV.h('div', null, 'Component ${name} not found'); }; module.exports.render = function(props) { var Component = typeof ${name} === 'function' ? ${name} : null; if (Component) { return VSV.renderToString(Component(props || {})); } return '<div>Component ${name} not found</div>'; }; `; } /** * Wrap code for client execution */ wrapClientCode(code, name) { return ` // VSV Client Component: ${name} (function() { 'use strict'; var VSV = window.VSV; if (!VSV) return; // Client-side hooks var h = VSV.h; var Fragment = VSV.Fragment; var $state = VSV.$state; var $computed = VSV.$computed; var $effect = VSV.$effect; var $layoutEffect = VSV.$layoutEffect; var $ref = VSV.$ref; var $memo = VSV.$memo; var $callback = VSV.$callback; var $reducer = VSV.$reducer; var $id = VSV.$id; var $transition = VSV.$transition; var $deferred = VSV.$deferred; var $context = VSV.$context; var $imperativeHandle = VSV.$imperativeHandle; var $debugValue = VSV.$debugValue; var $syncExternalStore = VSV.$syncExternalStore; var $form = VSV.$form; var $fetch = VSV.$fetch; var $location = VSV.$location; var $params = VSV.$params; var $query = VSV.$query; var $navigate = VSV.$navigate; var $animation = VSV.$animation; // Components var Show = VSV.Show; var For = VSV.For; var Switch = VSV.Switch; var Match = VSV.Match; var Head = VSV.Head; var Router = VSV.Router; var Route = VSV.Route; var Link = VSV.Link; var NavLink = VSV.NavLink; var Redirect = VSV.Redirect; var Suspense = VSV.Suspense; var ErrorBoundary = VSV.ErrorBoundary; // Utilities var createContext = VSV.createContext; var createStore = VSV.createStore; var createPortal = VSV.createPortal; var forwardRef = VSV.forwardRef; var memo = VSV.memo; var lazy = VSV.lazy; var Children = VSV.Children; var css = VSV.css; var styled = VSV.styled; var navigate = VSV.navigate; var PropTypes = VSV.PropTypes; var resolveProps = VSV.resolveProps; ${code} // Register component var Component = typeof ${name} !== 'undefined' ? ${name} : (typeof __default__ !== 'undefined' ? __default__ : null); if (Component) { VSV.register('${name}', Component); } })(); `; } /** * Transform AST for optimization */ transform(ast) { // Clone AST const transformed = JSON.parse(JSON.stringify(ast)); // Apply transformations for (const component of transformed.components) { // Extract reactive declarations this.extractReactivity(component); // Optimize JSX this.optimizeJSX(component); // Static analysis for partial hydration this.analyzeHydration(component); } return transformed; } /** * Extract reactive declarations ($state, $computed, $effect) */ extractReactivity(component) { if (!component.body) return; component.reactivity = { state: [], computed: [], effects: [], refs: [] }; const processTokens = (tokens) => { for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'VSV_DIRECTIVE') { switch (token.value) { case '$state': component.reactivity.state.push({ name: tokens[i - 2]?.value, initialValue: this.extractValue(tokens, i + 2) }); break; case '$computed': component.reactivity.computed.push({ name: tokens[i - 2]?.value, dependencies: this.extractDeps(tokens, i), compute: this.extractFunction(tokens, i + 2) }); break; case '$effect': component.reactivity.effects.push({ dependencies: this.extractDeps(tokens, i), callback: this.extractFunction(tokens, i + 2) }); break; case '$ref': component.reactivity.refs.push({ name: tokens[i - 2]?.value }); break; } } } }; if (Array.isArray(component.body)) { processTokens(component.body); } } /** * Extract value from tokens */ extractValue(tokens, start) { if (!tokens[start]) return null; const token = tokens[start]; if (token.type === 'NUMBER') return token.value; if (token.type === 'STRING') return token.value; if (token.value === 'true') return true; if (token.value === 'false') return false; if (token.value === 'null') return null; if (token.value === '[') return this.extractArray(tokens, start); if (token.value === '{') return this.extractObject(tokens, start); return token.value; } /** * Extract array from tokens */ extractArray(tokens, start) { const arr = []; let i = start + 1; while (tokens[i]?.value !== ']') { if (tokens[i]?.type !== 'PUNCT' || tokens[i].value !== ',') { arr.push(this.extractValue(tokens, i)); } i++; } return arr; } /** * Extract object from tokens */ extractObject(tokens, start) { const obj = {}; let i = start + 1; while (tokens[i]?.value !== '}') { if (tokens[i]?.type === 'IDENTIFIER' || tokens[i]?.type === 'STRING') { const key = tokens[i].value; if (tokens[i + 1]?.value === ':') { obj[key] = this.extractValue(tokens, i + 2); i += 2; } } i++; } return obj; } /** * Extract dependencies */ extractDeps(tokens, start) { // Look for dependency array for (let i = start; i < Math.min(start + 20, tokens.length); i++) { if (tokens[i]?.value === '[') { return this.extractArray(tokens, i); } } return []; } /** * Extract function body */ extractFunction(tokens, start) { let i = start; // Skip to function body while (i < tokens.length && tokens[i].value !== '{' && tokens[i].value !== '=>') { i++; } if (tokens[i].value === '=>') { i++; } if (tokens[i]?.value === '{') { let depth = 1; let body = ''; i++; while (i < tokens.length && depth > 0) { if (tokens[i].value === '{') depth++; else if (tokens[i].value === '}') depth--; if (depth > 0) { body += tokens[i].value + ' '; } i++; } return body.trim(); } return tokens[i]?.value || ''; } /** * Optimize JSX in component */ optimizeJSX(component) { if (!component.body) return; // Find JSX tokens and analyze const jsxTokens = component.body.filter(t => t.type === 'JSX'); for (const jsx of jsxTokens) { jsx.optimized = this.analyzeJSX(jsx.value); } } /** * Analyze JSX for optimization */ analyzeJSX(jsx) { const analysis = { static: true, bindings: [], events: [], conditionals: [], loops: [] }; // Find dynamic expressions const exprRegex = /\{([^}]+)\}/g; let match; while ((match = exprRegex.exec(jsx)) !== null) { const expr = match[1].trim(); analysis.static = false; if (expr.includes('&&') || expr.includes('?')) { analysis.conditionals.push(expr); } else if (expr.includes('.map(') || expr.includes('.filter(')) { analysis.loops.push(expr); } else { analysis.bindings.push(expr); } } // Find event handlers const eventRegex = /@(\w+)=\{([^}]+)\}/g; while ((match = eventRegex.exec(jsx)) !== null) { analysis.events.push({ name: match[1], handler: match[2] }); } return analysis; } /** * Analyze for partial hydration */ analyzeHydration(component) { component.hydration = { needed: false, islands: [] }; // Check if hydration is needed if (component.reactivity) { const { state, effects, refs } = component.reactivity; if (state.length > 0 || effects.length > 0 || refs.length > 0) { component.hydration.needed = true; } } // Find interactive islands if (component.body) { const jsxTokens = component.body.filter(t => t.type === 'JSX'); for (const jsx of jsxTokens) { if (jsx.optimized?.events.length > 0) { component.hydration.needed = true; component.hydration.islands.push({ events: jsx.optimized.events, jsx: jsx.value }); } } } } /** * Generate server-side code */ generateServer(ast, options = {}) { let code = ` // VSV Server Component - Generated // ${options.name || 'Component'} const VSV = { h: (tag, props, ...children) => ({ tag, props: props || {}, children: children.flat() }), Fragment: Symbol('Fragment'), renderToString(vnode) { if (vnode == null || vnode === false) return ''; if (typeof vnode === 'string' || typeof vnode === 'number') return String(vnode); if (Array.isArray(vnode)) return vnode.map(v => VSV.renderToString(v)).join(''); const { tag, props, children } = vnode; if (tag === VSV.Fragment) { return children.map(c => VSV.renderToString(c)).join(''); } if (typeof tag === 'function') { return VSV.renderToString(tag(props)); } // Build attributes let attrs = ''; for (const [key, value] of Object.entries(props)) { if (key === 'children') continue; if (key.startsWith('@')) continue; // Skip event handlers for SSR if (value === true) attrs += \` \${key}\`; else if (value !== false && value != null) { attrs += \` \${key}="\${String(value).replace(/"/g, '&quot;')}"\`; } } // Self-closing tags const selfClosing = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']; if (selfClosing.includes(tag)) { return \`<\${tag}\${attrs}/>\`; } // Render children const childrenStr = children.map(c => VSV.renderToString(c)).join(''); return \`<\${tag}\${attrs}>\${childrenStr}</\${tag}>\`; } }; `; // Add imports for (const imp of ast.imports) { code += this.generateImport(imp) + '\n'; } // Add components for (const component of ast.components) { code += this.generateServerComponent(component) + '\n\n'; } // Export default component if (ast.components.length > 0) { const mainComponent = ast.components.find(c => ast.exports.some(e => e.default && e.declaration?.name === c.name) ) || ast.components[0]; code += `module.exports = ${mainComponent.name};\n`; code += `module.exports.render = (props) => VSV.renderToString(${mainComponent.name}(props));\n`; } return code; } /** * Generate import statement */ generateImport(imp) { const specs = imp.specifiers.map(s => { if (s.type === 'ImportDefaultSpecifier') return s.local; if (s.type === 'ImportNamespaceSpecifier') return `* as ${s.local}`; if (s.imported === s.local) return s.local; return `${s.imported} as ${s.local}`; }); if (specs.length === 1 && imp.specifiers[0].type === 'ImportDefaultSpecifier') { return `const ${specs[0]} = require('${imp.source}');`; } return `const { ${specs.join(', ')} } = require('${imp.source}');`; } /** * Generate server component */ generateServerComponent(component) { const name = component.name || 'Component'; const params = this.generateParams(component); let code = `function ${name}(${params}) {\n`; // Add state initialization if (component.reactivity?.state) { for (const s of component.reactivity.state) { code += ` let ${s.name} = ${JSON.stringify(s.initialValue)};\n`; } } // Add computed if (component.reactivity?.computed) { for (const c of component.reactivity.computed) { code += ` const ${c.name} = (() => { ${c.compute} })();\n`; } } // Generate render code += this.generateRenderBody(component); code += `}\n`; return code; } /** * Generate parameters */ generateParams(component) { if (component.params) { const params = component.params.map(p => { if (p.type === 'destructure') { return `{ ${p.properties.join(', ')} }`; } return p.default ? `${p.name} = ${JSON.stringify(p.default)}` : p.name; }); return params.join(', '); } if (component.props?.length > 0) { return `{ ${component.props.map(p => p.name).join(', ')} }`; } return 'props = {}'; } /** * Generate render body */ generateRenderBody(component) { if (!component.body) { return ' return null;\n'; } // Find JSX in body let hasReturn = false; let code = ''; for (const token of component.body) { if (token.type === 'JSX') { hasReturn = true; code += ` return ${this.compileJSX(token.value)};\n`; break; } if (token.type === 'KEYWORD' && token.value === 'return') { hasReturn = true; } } if (!hasReturn) { // Reconstruct body code += this.tokensToCode(component.body); } return code; } /** * Compile JSX to h() calls */ compileJSX(jsx) { // Parse JSX return this._legacyParseJSXElement(jsx); } /** * Legacy Parse JSX element (used by full parser path) */ _legacyParseJSXElement(jsx) { jsx = jsx.trim(); // Handle fragments if (jsx.startsWith('<>')) { const content = jsx.slice(2, jsx.lastIndexOf('</')).trim(); const children = this._legacyParseJSXChildren(content); return `VSV.h(VSV.Fragment, null, ${children.join(', ')})`; } // Parse opening tag const tagMatch = jsx.match(/^<(\w+)/); if (!tagMatch) { // Expression or text if (jsx.startsWith('{') && jsx.endsWith('}')) { return jsx.slice(1, -1); } return JSON.stringify(jsx); } const tagName = tagMatch[1]; const isComponent = /^[A-Z]/.test(tagName); // Parse attributes const attrs = this._legacyParseJSXAttributes(jsx); // Check self-closing if (jsx.endsWith('/>')) { const tag = isComponent ? tagName : JSON.stringify(tagName); return `VSV.h(${tag}, ${attrs})`; } // Find children const openTagEnd = jsx.indexOf('>'); const closeTagStart = jsx.lastIndexOf('</'); if (closeTagStart === -1) { const tag = isComponent ? tagName : JSON.stringify(tagName); return `VSV.h(${tag}, ${attrs})`; } const childrenContent = jsx.slice(openTagEnd + 1, closeTagStart).trim(); const children = this._legacyParseJSXChildren(childrenContent); const tag = isComponent ? tagName : JSON.stringify(tagName); return `VSV.h(${tag}, ${attrs}${children.length > 0 ? ', ' + children.join(', ') : ''})`; } /** * Legacy Parse JSX attributes (used by full parser path) */ _legacyParseJSXAttributes(jsx) { const attrRegex = /(\w+|@\w+)=(?:\{([^}]+)\}|"([^"]*)")|(\w+)(?=[\s/>])/g; const attrs = {}; let match; // Get attribute section const tagEnd = jsx.indexOf('>'); const attrSection = jsx.slice(jsx.indexOf(' '), tagEnd === -1 ? undefined : tagEnd); while ((match = attrRegex.exec(attrSection)) !== null) { const name = match[1] || match[4]; if (!name) continue; // Convert @event to onClick style const attrName = name.startsWith('@') ? 'on' + name[1].toUpperCase() + name.slice(2) : name; if (match[2]) { // Expression attrs[attrName] = match[2]; } else if (match[3] !== undefined) { // String attrs[attrName] = JSON.stringify(match[3]); } else { // Boolean attrs[attrName] = 'true'; } } if (Object.keys(attrs).length === 0) { return 'null'; } const attrStr = Object.entries(attrs) .map(([k, v]) => `"${k}": ${v}`) .join(', '); return `{ ${attrStr} }`; } /** * Legacy Parse JSX children (used by full parser path) */ _legacyParseJSXChildren(content) { if (!content) return []; const children = []; let i = 0; let current = ''; while (i < content.length) { const char = content[i]; if (char === '<') { // Save current text if (current.trim()) { children.push(JSON.stringify(current.trim())); } current = ''; // Parse nested element const elem = this.extractJSXElement(content, i); children.push(this._legacyParseJSXElement(elem.value)); i = elem.end; continue; } if (char === '{') { // Save current text if (current.trim()) { children.push(JSON.stringify(current.trim())); } current = ''; // Parse expression const expr = this.extractExpression(content, i); children.push(expr.value); i = expr.end; continue; } current += char; i++; } if (current.trim()) { children.push(JSON.stringify(current.trim())); } return children; } /** * Extract JSX element from string */ extractJSXElement(str, start) { let i = start; let depth = 0; let inString = false; let stringChar = ''; while (i < str.length) { const char = str[i]; if ((char === '"' || char === "'" || char === '`') && !inString) { inString = true; stringChar = char; i++; continue; } if (inString && char === stringChar && str[i - 1] !== '\\') { inString = false; i++; continue; } if (!inString) { if (char === '<' && str[i + 1] !== '/') { depth++; } else if (str.slice(i, i + 2) === '/>') { depth--; i += 2; if (depth === 0) break; continue; } else if (char === '<' && str[i + 1] === '/') { while (i < str.length && str[i] !== '>') i++; depth--; i++; if (depth === 0) break; continue; } } i++; } return { value: str.slice(start, i), end: i }; } /** * Extract expression from string */ extractExpression(str, start) { let i = start + 1; let depth = 1; while (i < str.length && depth > 0) { if (str[i] === '{') depth++; else if (str[i] === '}') depth--; i++; } return { value: str.slice(start + 1, i - 1), end: i }; } /** * Convert tokens back to code */ tokensToCode(tokens) { let code = ' '; for (const token of tokens) { if (token.type === 'JSX') { code += this.compileJSX(token.value); } else { code += token.value + ' '; } } return code + '\n'; } /** * Generate client-side code */ generateClient(ast, options = {}) { let code = ` // VSV Client - Generated // ${options.name || 'Component'} (function() { 'use strict'; const VSV = window.VSV || (window.VSV = { components: new Map(), state: new Map(), effects: [], // Virtual DOM h(tag, props, ...children) { return { tag, props: props || {}, children: children.flat() }; }, Fragment: Symbol('Fragment'), // Render to DOM render(vnode, container) { container.innerHTML = ''; container.appendChild(this.createElement(vnode)); }, // Create DOM element createElement(vnode) { if (vnode == null || vnode === false) return document.createTextNode(''); if (typeof vnode === 'string' || typeof vnode === 'number') { return document.createTextNode(String(vnode)); } if (Array.isArray(vnode)) { const frag = document.createDocumentFragment(); vnode.forEach(v => frag.appendChild(this.createElement(v))); return frag; } const { tag, props, children } = vnode; if (tag === this.Fragment) { const frag = document.createDocumentFragment(); children.forEach(c => frag.appendChild(this.createElement(c))); return frag; } if (typeof tag === 'function') { return this.createElement(tag(props)); } const el = document.createElement(