@zenithbuild/compiler 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/build-analyzer.d.ts +44 -0
  4. package/dist/build-analyzer.js +87 -0
  5. package/dist/bundler.d.ts +31 -0
  6. package/dist/bundler.js +86 -0
  7. package/dist/core/components/index.d.ts +9 -0
  8. package/dist/core/components/index.js +13 -0
  9. package/dist/core/config/index.d.ts +11 -0
  10. package/dist/core/config/index.js +10 -0
  11. package/dist/core/config/loader.d.ts +17 -0
  12. package/dist/core/config/loader.js +60 -0
  13. package/dist/core/config/types.d.ts +98 -0
  14. package/dist/core/config/types.js +32 -0
  15. package/dist/core/index.d.ts +7 -0
  16. package/dist/core/index.js +6 -0
  17. package/dist/core/lifecycle/index.d.ts +16 -0
  18. package/dist/core/lifecycle/index.js +19 -0
  19. package/dist/core/lifecycle/zen-mount.d.ts +66 -0
  20. package/dist/core/lifecycle/zen-mount.js +151 -0
  21. package/dist/core/lifecycle/zen-unmount.d.ts +54 -0
  22. package/dist/core/lifecycle/zen-unmount.js +76 -0
  23. package/dist/core/plugins/bridge.d.ts +116 -0
  24. package/dist/core/plugins/bridge.js +121 -0
  25. package/dist/core/plugins/index.d.ts +6 -0
  26. package/dist/core/plugins/index.js +6 -0
  27. package/dist/core/plugins/registry.d.ts +67 -0
  28. package/dist/core/plugins/registry.js +113 -0
  29. package/dist/core/reactivity/index.d.ts +30 -0
  30. package/dist/core/reactivity/index.js +33 -0
  31. package/dist/core/reactivity/tracking.d.ts +74 -0
  32. package/dist/core/reactivity/tracking.js +136 -0
  33. package/dist/core/reactivity/zen-batch.d.ts +45 -0
  34. package/dist/core/reactivity/zen-batch.js +54 -0
  35. package/dist/core/reactivity/zen-effect.d.ts +48 -0
  36. package/dist/core/reactivity/zen-effect.js +98 -0
  37. package/dist/core/reactivity/zen-memo.d.ts +43 -0
  38. package/dist/core/reactivity/zen-memo.js +100 -0
  39. package/dist/core/reactivity/zen-ref.d.ts +44 -0
  40. package/dist/core/reactivity/zen-ref.js +34 -0
  41. package/dist/core/reactivity/zen-signal.d.ts +48 -0
  42. package/dist/core/reactivity/zen-signal.js +84 -0
  43. package/dist/core/reactivity/zen-state.d.ts +35 -0
  44. package/dist/core/reactivity/zen-state.js +147 -0
  45. package/dist/core/reactivity/zen-untrack.d.ts +38 -0
  46. package/dist/core/reactivity/zen-untrack.js +41 -0
  47. package/dist/css/index.d.ts +73 -0
  48. package/dist/css/index.js +246 -0
  49. package/dist/discovery/componentDiscovery.d.ts +42 -0
  50. package/dist/discovery/componentDiscovery.js +56 -0
  51. package/dist/discovery/layouts.d.ts +13 -0
  52. package/dist/discovery/layouts.js +41 -0
  53. package/dist/errors/compilerError.d.ts +31 -0
  54. package/dist/errors/compilerError.js +51 -0
  55. package/dist/finalize/finalizeOutput.d.ts +32 -0
  56. package/dist/finalize/finalizeOutput.js +62 -0
  57. package/dist/finalize/generateFinalBundle.d.ts +24 -0
  58. package/dist/finalize/generateFinalBundle.js +68 -0
  59. package/dist/index.d.ts +36 -0
  60. package/dist/index.js +51 -0
  61. package/dist/ir/types.d.ts +181 -0
  62. package/dist/ir/types.js +8 -0
  63. package/dist/output/types.d.ts +30 -0
  64. package/dist/output/types.js +6 -0
  65. package/dist/parse/detectMapExpressions.d.ts +45 -0
  66. package/dist/parse/detectMapExpressions.js +77 -0
  67. package/dist/parse/parseScript.d.ts +8 -0
  68. package/dist/parse/parseScript.js +36 -0
  69. package/dist/parse/parseTemplate.d.ts +11 -0
  70. package/dist/parse/parseTemplate.js +487 -0
  71. package/dist/parse/parseZenFile.d.ts +11 -0
  72. package/dist/parse/parseZenFile.js +50 -0
  73. package/dist/parse/scriptAnalysis.d.ts +25 -0
  74. package/dist/parse/scriptAnalysis.js +60 -0
  75. package/dist/parse/trackLoopContext.d.ts +20 -0
  76. package/dist/parse/trackLoopContext.js +62 -0
  77. package/dist/parseZenFile.d.ts +10 -0
  78. package/dist/parseZenFile.js +55 -0
  79. package/dist/runtime/analyzeAndEmit.d.ts +20 -0
  80. package/dist/runtime/analyzeAndEmit.js +70 -0
  81. package/dist/runtime/build.d.ts +6 -0
  82. package/dist/runtime/build.js +13 -0
  83. package/dist/runtime/bundle-generator.d.ts +27 -0
  84. package/dist/runtime/bundle-generator.js +1263 -0
  85. package/dist/runtime/client-runtime.d.ts +41 -0
  86. package/dist/runtime/client-runtime.js +397 -0
  87. package/dist/runtime/dataExposure.d.ts +52 -0
  88. package/dist/runtime/dataExposure.js +227 -0
  89. package/dist/runtime/generateDOM.d.ts +21 -0
  90. package/dist/runtime/generateDOM.js +194 -0
  91. package/dist/runtime/generateHydrationBundle.d.ts +15 -0
  92. package/dist/runtime/generateHydrationBundle.js +399 -0
  93. package/dist/runtime/hydration.d.ts +53 -0
  94. package/dist/runtime/hydration.js +271 -0
  95. package/dist/runtime/navigation.d.ts +58 -0
  96. package/dist/runtime/navigation.js +372 -0
  97. package/dist/runtime/serve.d.ts +13 -0
  98. package/dist/runtime/serve.js +76 -0
  99. package/dist/runtime/thinRuntime.d.ts +23 -0
  100. package/dist/runtime/thinRuntime.js +158 -0
  101. package/dist/runtime/transformIR.d.ts +19 -0
  102. package/dist/runtime/transformIR.js +285 -0
  103. package/dist/runtime/wrapExpression.d.ts +24 -0
  104. package/dist/runtime/wrapExpression.js +76 -0
  105. package/dist/runtime/wrapExpressionWithLoop.d.ts +17 -0
  106. package/dist/runtime/wrapExpressionWithLoop.js +75 -0
  107. package/dist/spa-build.d.ts +26 -0
  108. package/dist/spa-build.js +866 -0
  109. package/dist/ssg-build.d.ts +32 -0
  110. package/dist/ssg-build.js +408 -0
  111. package/dist/test/analyze-emit.test.d.ts +1 -0
  112. package/dist/test/analyze-emit.test.js +88 -0
  113. package/dist/test/bundler-contract.test.d.ts +1 -0
  114. package/dist/test/bundler-contract.test.js +137 -0
  115. package/dist/test/compiler-authority.test.d.ts +1 -0
  116. package/dist/test/compiler-authority.test.js +90 -0
  117. package/dist/test/component-instance-test.d.ts +1 -0
  118. package/dist/test/component-instance-test.js +115 -0
  119. package/dist/test/error-native-bridge.test.d.ts +1 -0
  120. package/dist/test/error-native-bridge.test.js +51 -0
  121. package/dist/test/error-serialization.test.d.ts +1 -0
  122. package/dist/test/error-serialization.test.js +38 -0
  123. package/dist/test/macro-inlining.test.d.ts +1 -0
  124. package/dist/test/macro-inlining.test.js +178 -0
  125. package/dist/test/validate-test.d.ts +6 -0
  126. package/dist/test/validate-test.js +95 -0
  127. package/dist/transform/classifyExpression.d.ts +46 -0
  128. package/dist/transform/classifyExpression.js +354 -0
  129. package/dist/transform/componentResolver.d.ts +15 -0
  130. package/dist/transform/componentResolver.js +30 -0
  131. package/dist/transform/expressionTransformer.d.ts +19 -0
  132. package/dist/transform/expressionTransformer.js +333 -0
  133. package/dist/transform/fragmentLowering.d.ts +25 -0
  134. package/dist/transform/fragmentLowering.js +468 -0
  135. package/dist/transform/layoutProcessor.d.ts +5 -0
  136. package/dist/transform/layoutProcessor.js +34 -0
  137. package/dist/transform/transformTemplate.d.ts +11 -0
  138. package/dist/transform/transformTemplate.js +33 -0
  139. package/dist/validate/invariants.d.ts +23 -0
  140. package/dist/validate/invariants.js +55 -0
  141. package/native/compiler-native/compiler-native.node +0 -0
  142. package/native/compiler-native/index.d.ts +113 -0
  143. package/native/compiler-native/index.js +19 -0
  144. package/native/compiler-native/package.json +19 -0
  145. package/package.json +49 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Zenith SSG Build System
3
+ *
4
+ * SSG-first (Static Site Generation) build system that outputs:
5
+ * - Per-page HTML files: dist/{route}/index.html
6
+ * - Shared runtime: dist/assets/bundle.js
7
+ * - Global styles: dist/assets/styles.css
8
+ * - Page-specific JS only for pages needing hydration: dist/assets/page_{name}.js
9
+ *
10
+ * Static pages get pure HTML+CSS, no JavaScript.
11
+ * Hydrated pages reference the shared bundle.js and their page-specific JS.
12
+ */
13
+ export interface SSGBuildOptions {
14
+ /** Pages directory (e.g., app/pages) */
15
+ pagesDir: string;
16
+ /** Output directory (e.g., app/dist) */
17
+ outDir: string;
18
+ /** Base directory for components/layouts (e.g., app/) */
19
+ baseDir?: string;
20
+ /** Include source maps */
21
+ sourceMaps?: boolean;
22
+ }
23
+ /**
24
+ * Build all pages using SSG approach
25
+ *
26
+ * Follows the blind orchestrator pattern:
27
+ * - Plugins are initialized unconditionally
28
+ * - Data is collected via hooks
29
+ * - CLI never inspects plugin data
30
+ */
31
+ export declare function buildSSG(options: SSGBuildOptions): Promise<void>;
32
+ export { buildSSG as buildSPA };
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Zenith SSG Build System
3
+ *
4
+ * SSG-first (Static Site Generation) build system that outputs:
5
+ * - Per-page HTML files: dist/{route}/index.html
6
+ * - Shared runtime: dist/assets/bundle.js
7
+ * - Global styles: dist/assets/styles.css
8
+ * - Page-specific JS only for pages needing hydration: dist/assets/page_{name}.js
9
+ *
10
+ * Static pages get pure HTML+CSS, no JavaScript.
11
+ * Hydrated pages reference the shared bundle.js and their page-specific JS.
12
+ */
13
+ /**
14
+ * ═══════════════════════════════════════════════════════════════════════════════
15
+ * CLI HARDENING: BLIND ORCHESTRATOR PATTERN
16
+ * ═══════════════════════════════════════════════════════════════════════════════
17
+ *
18
+ * This build system uses the plugin bridge pattern:
19
+ * - Plugins are initialized unconditionally
20
+ * - Data is collected via 'cli:runtime:collect' hook
21
+ * - CLI never inspects or branches on plugin data
22
+ * ═══════════════════════════════════════════════════════════════════════════════
23
+ */
24
+ import fs from "fs";
25
+ import path from "path";
26
+ import { compileZenSource } from "./index";
27
+ import { discoverLayouts } from "./discovery/layouts";
28
+ import { processLayout } from "./transform/layoutProcessor";
29
+ import { discoverPages, generateRouteDefinition } from "@zenithbuild/router/manifest";
30
+ import { analyzePageSource, getAnalysisSummary, getBuildOutputType } from "./build-analyzer";
31
+ import { generateBundleJS } from "./runtime/bundle-generator";
32
+ import { compileCss, resolveGlobalsCss } from "./css";
33
+ import { loadZenithConfig } from "./core/config/loader";
34
+ import { PluginRegistry, createPluginContext, getPluginDataByNamespace } from "./core/plugins/registry";
35
+ import { createBridgeAPI, collectHookReturns, buildRuntimeEnvelope, clearHooks } from "./core/plugins/bridge";
36
+ import { bundlePageScript } from "./bundler";
37
+ // ============================================
38
+ // Page Compilation
39
+ // ============================================
40
+ /**
41
+ * Compile a single page file for SSG output
42
+ */
43
+ async function compilePage(pagePath, pagesDir, baseDir = process.cwd()) {
44
+ const source = fs.readFileSync(pagePath, 'utf-8');
45
+ // Analyze page requirements
46
+ const analysis = analyzePageSource(source);
47
+ // Determine source directory relative to pages (e.g., 'src' or 'app' or root)
48
+ const srcDir = path.dirname(pagesDir);
49
+ // Discover layouts
50
+ const layoutsDir = path.join(srcDir, 'layouts');
51
+ const layouts = discoverLayouts(layoutsDir);
52
+ // Process with layout if one is used
53
+ let processedSource = source;
54
+ const layoutToUse = layouts.get('DefaultLayout');
55
+ if (layoutToUse) {
56
+ processedSource = processLayout(source, layoutToUse);
57
+ }
58
+ // Compile with new pipeline
59
+ const result = await compileZenSource(processedSource, pagePath, {
60
+ componentsDir: path.join(srcDir, 'components')
61
+ });
62
+ if (!result.finalized) {
63
+ throw new Error(`Compilation failed for ${pagePath}: No finalized output`);
64
+ }
65
+ // Extract compiled output
66
+ const html = result.finalized.html;
67
+ const js = result.finalized.js || '';
68
+ const imports = result.finalized.npmImports || '';
69
+ const styles = result.finalized.styles || [];
70
+ // Generate route definition
71
+ const routeDef = generateRouteDefinition(pagePath, pagesDir);
72
+ // Determine output directory from route path
73
+ // "/" -> "index", "/about" -> "about", "/blog/post" -> "blog/post"
74
+ let outputDir = routeDef.path === '/' ? 'index' : routeDef.path.replace(/^\//, '');
75
+ // Handle dynamic routes - they'll be placeholders for now
76
+ // [id] segments become _id_ for folder names
77
+ outputDir = outputDir.replace(/\[([^\]]+)\]/g, '_$1_');
78
+ // Force hydration if we have compiled JS or if top-level analysis detected it
79
+ const needsHydration = analysis.needsHydration || js.trim().length > 0;
80
+ return {
81
+ routePath: routeDef.path,
82
+ filePath: pagePath,
83
+ html,
84
+ pageScript: needsHydration ? js : '',
85
+ pageImports: needsHydration ? imports : '',
86
+ styles,
87
+ score: routeDef.score,
88
+ paramNames: routeDef.paramNames,
89
+ analysis: {
90
+ ...analysis,
91
+ needsHydration,
92
+ isStatic: !needsHydration && !analysis.needsSSR
93
+ },
94
+ outputDir,
95
+ bundlePlan: result.finalized.bundlePlan
96
+ };
97
+ }
98
+ // ============================================
99
+ // HTML Generation
100
+ // ============================================
101
+ /**
102
+ * Generate the final HTML for a page
103
+ * Static pages: no JS references
104
+ * Hydrated pages: bundle.js + page-specific JS
105
+ *
106
+ * Uses the neutral __ZENITH_PLUGIN_DATA__ envelope - CLI never inspects contents.
107
+ */
108
+ function generatePageHTML(page, globalStyles, pluginEnvelope) {
109
+ const { html, styles, analysis, routePath, pageScript } = page;
110
+ // Combine styles
111
+ const pageStyles = styles.join('\n');
112
+ const allStyles = globalStyles + '\n' + pageStyles;
113
+ // Build script tags only if needed
114
+ let scriptTags = '';
115
+ if (analysis.needsHydration) {
116
+ scriptTags = `
117
+ <script src="/assets/bundle.js"></script>`;
118
+ if (pageScript) {
119
+ // Generate a safe filename from route path
120
+ const pageJsName = routePath === '/'
121
+ ? 'page_index.js'
122
+ : `page_${routePath.replace(/^\//, '').replace(/\//g, '_')}.js`;
123
+ scriptTags += `
124
+ <script type="module" src="/assets/${pageJsName}"></script>`;
125
+ }
126
+ }
127
+ // Check if HTML already has full document structure
128
+ const hasHtmlTag = /<html[^>]*>/i.test(html);
129
+ if (hasHtmlTag) {
130
+ // HTML already has structure from layout - inject styles and scripts
131
+ let finalHtml = html;
132
+ // Inject styles into <head> if not already there
133
+ if (!/<style[^>]*>/.test(finalHtml)) {
134
+ finalHtml = finalHtml.replace('</head>', ` <style>\n${allStyles}\n </style>\n</head>`);
135
+ }
136
+ // Inject scripts before </body>
137
+ if (scriptTags) {
138
+ finalHtml = finalHtml.replace('</body>', `${scriptTags}\n</body>`);
139
+ }
140
+ return finalHtml;
141
+ }
142
+ // Generate full HTML document for pages without layout
143
+ return `<!DOCTYPE html>
144
+ <html lang="en">
145
+ <head>
146
+ <meta charset="UTF-8">
147
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
148
+ <title>Zenith App</title>
149
+ <style>
150
+ ${allStyles}
151
+ </style>
152
+ </head>
153
+ <body>
154
+ ${html}${scriptTags}
155
+ </body>
156
+ </html>`;
157
+ }
158
+ // ============================================
159
+ // Asset Generation
160
+ // ============================================
161
+ /**
162
+ * Generate page-specific JavaScript
163
+ */
164
+ function generatePageJS(page) {
165
+ if (!page.pageScript)
166
+ return '';
167
+ // Module imports must be top-level
168
+ return `// Zenith Page: ${page.routePath}
169
+ // Phase 5: ES Module Mode
170
+
171
+ ${page.pageScript}
172
+
173
+ // Trigger hydration after DOM is ready
174
+ (function() {
175
+ function trigger() {
176
+ if (window.__zenith && window.__zenith.triggerMount) {
177
+ window.__zenith.triggerMount();
178
+ }
179
+ }
180
+
181
+ if (document.readyState === 'loading') {
182
+ document.addEventListener('DOMContentLoaded', trigger);
183
+ } else {
184
+ trigger();
185
+ }
186
+ })();
187
+ `;
188
+ }
189
+ // ============================================
190
+ // Main Build Function
191
+ // ============================================
192
+ /**
193
+ * Build all pages using SSG approach
194
+ *
195
+ * Follows the blind orchestrator pattern:
196
+ * - Plugins are initialized unconditionally
197
+ * - Data is collected via hooks
198
+ * - CLI never inspects plugin data
199
+ */
200
+ export async function buildSSG(options) {
201
+ const { pagesDir, outDir, baseDir = path.dirname(pagesDir) } = options;
202
+ console.log('🔨 Zenith SSG Build');
203
+ console.log(` Pages: ${pagesDir}`);
204
+ console.log(` Output: ${outDir}`);
205
+ console.log('');
206
+ // ============================================
207
+ // Plugin Initialization (Unconditional)
208
+ // ============================================
209
+ // Load config and initialize all plugins without checking which ones exist.
210
+ const config = await loadZenithConfig(baseDir);
211
+ const registry = new PluginRegistry();
212
+ const bridgeAPI = createBridgeAPI();
213
+ // Clear any previously registered hooks
214
+ clearHooks();
215
+ // Register ALL plugins unconditionally
216
+ for (const plugin of config.plugins || []) {
217
+ console.log(` Plugin: ${plugin.name}`);
218
+ registry.register(plugin);
219
+ // Let plugin register its CLI hooks
220
+ if (plugin.registerCLI) {
221
+ plugin.registerCLI(bridgeAPI);
222
+ }
223
+ }
224
+ // Initialize all plugins
225
+ await registry.initAll(createPluginContext(baseDir));
226
+ // Create hook context - CLI provides this but NEVER uses getPluginData itself
227
+ const hookCtx = {
228
+ projectRoot: baseDir,
229
+ getPluginData: getPluginDataByNamespace
230
+ };
231
+ // Collect runtime payloads from ALL plugins
232
+ const payloads = await collectHookReturns('cli:runtime:collect', hookCtx);
233
+ const pluginEnvelope = buildRuntimeEnvelope(payloads);
234
+ console.log('');
235
+ // Clean and create output directory
236
+ if (fs.existsSync(outDir)) {
237
+ fs.rmSync(outDir, { recursive: true, force: true });
238
+ }
239
+ fs.mkdirSync(outDir, { recursive: true });
240
+ fs.mkdirSync(path.join(outDir, 'assets'), { recursive: true });
241
+ // Discover pages
242
+ const pageFiles = discoverPages(pagesDir);
243
+ if (pageFiles.length === 0) {
244
+ console.warn('⚠️ No pages found in', pagesDir);
245
+ return;
246
+ }
247
+ console.log(`📄 Found ${pageFiles.length} page(s)`);
248
+ // Compile all pages
249
+ const compiledPages = [];
250
+ let hasHydratedPages = false;
251
+ for (const pageFile of pageFiles) {
252
+ const relativePath = path.relative(pagesDir, pageFile);
253
+ console.log(` Compiling: ${relativePath}`);
254
+ try {
255
+ const compiled = await compilePage(pageFile, pagesDir, baseDir);
256
+ compiledPages.push(compiled);
257
+ if (compiled.analysis.needsHydration) {
258
+ hasHydratedPages = true;
259
+ }
260
+ const outputType = getBuildOutputType(compiled.analysis);
261
+ const summary = getAnalysisSummary(compiled.analysis);
262
+ // Check if it's "forced" hydration (analysis missed it, but compiler found JS)
263
+ const logType = outputType.toUpperCase();
264
+ console.log(` → ${logType} [${summary}]`);
265
+ }
266
+ catch (error) {
267
+ console.error(` ❌ Error: ${error.message}`);
268
+ throw error;
269
+ }
270
+ }
271
+ console.log('');
272
+ // Compile global styles (Tailwind CSS)
273
+ let globalStyles = '';
274
+ const globalsCssPath = resolveGlobalsCss(baseDir);
275
+ if (globalsCssPath) {
276
+ console.log('📦 Compiling CSS:', path.relative(baseDir, globalsCssPath));
277
+ const cssOutputPath = path.join(outDir, 'assets', 'styles.css');
278
+ const result = compileCss({
279
+ input: globalsCssPath,
280
+ output: cssOutputPath,
281
+ minify: true
282
+ });
283
+ if (result.success) {
284
+ globalStyles = result.css;
285
+ console.log(`📦 Generated assets/styles.css (${result.duration}ms)`);
286
+ }
287
+ else {
288
+ console.error('❌ CSS compilation failed:', result.error);
289
+ }
290
+ }
291
+ // Write bundle.js if any pages need hydration
292
+ if (hasHydratedPages) {
293
+ const bundleJS = generateBundleJS(pluginEnvelope);
294
+ fs.writeFileSync(path.join(outDir, 'assets', 'bundle.js'), bundleJS);
295
+ console.log('📦 Generated assets/bundle.js (with plugin data)');
296
+ }
297
+ // Write each page
298
+ for (const page of compiledPages) {
299
+ // Create output directory
300
+ const pageOutDir = path.join(outDir, page.outputDir);
301
+ fs.mkdirSync(pageOutDir, { recursive: true });
302
+ // Generate and write HTML
303
+ const html = generatePageHTML(page, globalStyles, pluginEnvelope);
304
+ fs.writeFileSync(path.join(pageOutDir, 'index.html'), html);
305
+ // Write page-specific JS if needed
306
+ if (page.pageScript) {
307
+ const pageJsName = page.routePath === '/'
308
+ ? 'page_index.js'
309
+ : `page_${page.routePath.replace(/^\//, '').replace(/\//g, '_')}.js`;
310
+ const pageJS = generatePageJS(page);
311
+ // Bundle ONLY if compiler emitted a BundlePlan (no inference)
312
+ let bundledJS = pageJS;
313
+ if (page.bundlePlan) {
314
+ const plan = {
315
+ ...page.bundlePlan,
316
+ entry: pageJS,
317
+ resolveRoots: [path.join(baseDir, 'node_modules'), 'node_modules']
318
+ };
319
+ bundledJS = await bundlePageScript(plan);
320
+ }
321
+ fs.writeFileSync(path.join(outDir, 'assets', pageJsName), bundledJS);
322
+ }
323
+ console.log(`✅ ${page.outputDir}/index.html`);
324
+ }
325
+ // Copy favicon if exists
326
+ const faviconPath = path.join(baseDir, 'favicon.ico');
327
+ if (fs.existsSync(faviconPath)) {
328
+ fs.copyFileSync(faviconPath, path.join(outDir, 'favicon.ico'));
329
+ console.log('📦 Copied favicon.ico');
330
+ }
331
+ // Generate 404 page
332
+ const custom404Candidates = ['404.zen', '+404.zen', 'not-found.zen'];
333
+ let has404 = false;
334
+ for (const candidate of custom404Candidates) {
335
+ const custom404Path = path.join(pagesDir, candidate);
336
+ if (fs.existsSync(custom404Path)) {
337
+ try {
338
+ const compiled = await compilePage(custom404Path, pagesDir, baseDir);
339
+ const html = generatePageHTML(compiled, globalStyles, pluginEnvelope);
340
+ fs.writeFileSync(path.join(outDir, '404.html'), html);
341
+ console.log('📦 Generated 404.html (custom)');
342
+ has404 = true;
343
+ if (compiled.pageScript) {
344
+ const pageJS = generatePageJS(compiled);
345
+ // Bundle ONLY if compiler emitted a BundlePlan (no inference)
346
+ let bundledJS = pageJS;
347
+ if (compiled.bundlePlan) {
348
+ const plan = {
349
+ ...compiled.bundlePlan,
350
+ entry: pageJS,
351
+ resolveRoots: [path.join(baseDir, 'node_modules'), 'node_modules']
352
+ };
353
+ bundledJS = await bundlePageScript(plan);
354
+ }
355
+ fs.writeFileSync(path.join(outDir, 'assets', 'page_404.js'), bundledJS);
356
+ }
357
+ }
358
+ catch (error) {
359
+ console.warn(` ⚠️ Could not compile ${candidate}: ${error.message}`);
360
+ }
361
+ break;
362
+ }
363
+ }
364
+ if (!has404) {
365
+ const default404HTML = `<!DOCTYPE html>
366
+ <html lang="en">
367
+ <head>
368
+ <meta charset="UTF-8">
369
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
370
+ <title>Page Not Found | Zenith</title>
371
+ <style>
372
+ * { box-sizing: border-box; margin: 0; padding: 0; }
373
+ body { font-family: system-ui, sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); color: #f1f5f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
374
+ .container { text-align: center; padding: 2rem; }
375
+ .error-code { font-size: 8rem; font-weight: 800; background: linear-gradient(135deg, #3b82f6, #06b6d4); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1; margin-bottom: 1rem; }
376
+ h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #e2e8f0; }
377
+ .message { color: #94a3b8; margin-bottom: 2rem; }
378
+ a { display: inline-block; background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: 500; }
379
+ </style>
380
+ </head>
381
+ <body>
382
+ <div class="container">
383
+ <div class="error-code">404</div>
384
+ <h1>Page Not Found</h1>
385
+ <p class="message">The page you're looking for doesn't exist.</p>
386
+ <a href="/">← Go Home</a>
387
+ </div>
388
+ </body>
389
+ </html>`;
390
+ fs.writeFileSync(path.join(outDir, '404.html'), default404HTML);
391
+ console.log('📦 Generated 404.html (default)');
392
+ }
393
+ // Summary
394
+ console.log('');
395
+ console.log('✨ Build complete!');
396
+ console.log(` Static pages: ${compiledPages.filter(p => p.analysis.isStatic).length}`);
397
+ console.log(` Hydrated pages: ${compiledPages.filter(p => p.analysis.needsHydration).length}`);
398
+ console.log(` SSR pages: ${compiledPages.filter(p => p.analysis.needsSSR).length}`);
399
+ console.log('');
400
+ // Route manifest
401
+ console.log('📍 Routes:');
402
+ for (const page of compiledPages.sort((a, b) => b.score - a.score)) {
403
+ const type = getBuildOutputType(page.analysis);
404
+ console.log(` ${page.routePath.padEnd(20)} → ${page.outputDir}/index.html (${type})`);
405
+ }
406
+ }
407
+ // Legacy export for backwards compatibility
408
+ export { buildSSG as buildSPA };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { expect, test, describe } from "bun:test";
2
+ import { analyzeAndEmit } from "../runtime/analyzeAndEmit";
3
+ const defaultLoc = { line: 1, column: 1 };
4
+ function createIR(templateNodes = [], expressions = [], script = "") {
5
+ const nodes = templateNodes.map(n => ({
6
+ location: defaultLoc,
7
+ ...n,
8
+ // Recursively add location to children if they are elements or fragments
9
+ children: n.children ? n.children.map((c) => ({ location: defaultLoc, ...c })) : undefined,
10
+ body: n.body ? n.body.map((c) => ({ location: defaultLoc, ...c })) : undefined,
11
+ consequent: n.consequent ? n.consequent.map((c) => ({ location: defaultLoc, ...c })) : undefined,
12
+ alternate: n.alternate ? n.alternate.map((c) => ({ location: defaultLoc, ...c })) : undefined,
13
+ fragment: n.fragment ? n.fragment.map((c) => ({ location: defaultLoc, ...c })) : undefined,
14
+ }));
15
+ return {
16
+ filePath: "test.zen",
17
+ template: {
18
+ raw: "",
19
+ nodes: nodes,
20
+ expressions: expressions.map(e => ({ location: defaultLoc, ...e }))
21
+ },
22
+ script: {
23
+ raw: script,
24
+ attributes: {}
25
+ },
26
+ styles: [],
27
+ componentScripts: []
28
+ };
29
+ }
30
+ describe("Analyze + Emit Pass", () => {
31
+ test("Step 1: Binding Collection includes script variables and loop params", async () => {
32
+ const ir = createIR([
33
+ {
34
+ type: "loop-fragment",
35
+ itemVar: "item",
36
+ indexVar: "i",
37
+ source: "items",
38
+ body: []
39
+ }
40
+ ], [], "const count = 10; function increment() {} state active = true;");
41
+ const result = await analyzeAndEmit(ir);
42
+ // We can't easily inspect the internal binding table without exporting it or adding debug hooks,
43
+ // but we can verify it by checking if expressions using these variables are correctly analyzed.
44
+ });
45
+ test("Step 2: Expressions resolve against script and loop bindings", async () => {
46
+ const ir = createIR([
47
+ {
48
+ type: "loop-fragment",
49
+ itemVar: "item",
50
+ indexVar: "i",
51
+ source: "items",
52
+ body: []
53
+ }
54
+ ], [
55
+ { id: "expr_0", code: "count + 1" },
56
+ { id: "expr_1", code: "item.name" },
57
+ { id: "expr_2", code: "i > 0" },
58
+ { id: "expr_3", code: "active ? 'yes' : 'no'" }
59
+ ], "const count = 10; state active = true;");
60
+ const result = await analyzeAndEmit(ir);
61
+ // Check if expressions were correctly analyzed as using state
62
+ // (Since they resolve against bindings, they should be marked as usesState = true)
63
+ expect(result.expressions).toContain("expr_0");
64
+ expect(result.expressions).toContain("count + 1");
65
+ // Verify dependency metadata in the emitted bundle
66
+ // In the new Rust compiler, state variables are prefixed with state.
67
+ expect(result.expressions).toContain('state.active');
68
+ });
69
+ test("Renamed component symbols are correctly resolved", async () => {
70
+ const ir = createIR([], [
71
+ { id: "expr_comp", code: "val_inst0.value" }
72
+ ], "const val_inst0 = signal(0);");
73
+ const result = await analyzeAndEmit(ir);
74
+ expect(result.expressions).toContain("val_inst0.value");
75
+ });
76
+ test("Strict emission order: symbols before expressions", async () => {
77
+ const ir = createIR([], [{ id: "expr_test", code: "count + 1" }], "state count = 1;");
78
+ const result = await analyzeAndEmit(ir);
79
+ const bundle = result.bundle;
80
+ const countDeclPos = bundle.indexOf("state.count = 1;");
81
+ const statePos = bundle.indexOf("count: undefined");
82
+ const exprPos = bundle.indexOf("function _expr_expr_test");
83
+ // Verify that declaration comes before state initialization, and state comes before expressions
84
+ expect(countDeclPos).toBeGreaterThan(-1);
85
+ expect(statePos).toBeGreaterThan(-1);
86
+ expect(exprPos).toBeGreaterThan(statePos);
87
+ });
88
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
1
+ import { expect, test, describe } from "bun:test";
2
+ import { bundlePageScript } from "../bundler";
3
+ /**
4
+ * Bundler Contract Tests
5
+ *
6
+ * These tests verify the compiler-first bundler architecture:
7
+ * - Plan exists → bundling MUST occur
8
+ * - Bundling failure → hard error thrown (no fallback)
9
+ * - Same plan → deterministic output
10
+ *
11
+ * The bundler performs ZERO inference.
12
+ */
13
+ describe("Bundler Contract", () => {
14
+ test("executes plan with virtual modules", async () => {
15
+ const plan = {
16
+ entry: `
17
+ import { foo } from 'virtual:test';
18
+ console.log(foo);
19
+ `,
20
+ platform: "browser",
21
+ format: "esm",
22
+ resolveRoots: [],
23
+ virtualModules: [{
24
+ id: "virtual:test",
25
+ code: "export const foo = 'bar';"
26
+ }]
27
+ };
28
+ const result = await bundlePageScript(plan);
29
+ expect(result).toContain("bar");
30
+ expect(result).not.toContain("import"); // Should be bundled, no external imports
31
+ });
32
+ test("resolves zenith:content virtual module", async () => {
33
+ const plan = {
34
+ entry: `
35
+ import { zenCollection } from 'zenith:content';
36
+ console.log(zenCollection);
37
+ `,
38
+ platform: "browser",
39
+ format: "esm",
40
+ resolveRoots: [],
41
+ virtualModules: [{
42
+ id: '\0zenith:content',
43
+ code: `export const zenCollection = (typeof globalThis !== 'undefined' ? globalThis : window).zenCollection;`
44
+ }]
45
+ };
46
+ const result = await bundlePageScript(plan);
47
+ expect(result).toContain("zenCollection");
48
+ expect(result).not.toContain("import"); // Should be bundled
49
+ });
50
+ test("deterministic output for same plan", async () => {
51
+ const plan = {
52
+ entry: "const x = 1; const y = 2; console.log(x + y);",
53
+ platform: "browser",
54
+ format: "esm",
55
+ resolveRoots: [],
56
+ virtualModules: []
57
+ };
58
+ const result1 = await bundlePageScript(plan);
59
+ const result2 = await bundlePageScript(plan);
60
+ expect(result1).toBe(result2);
61
+ });
62
+ test("throws on unresolvable module (no fallback)", async () => {
63
+ const plan = {
64
+ entry: "import { nonexistent } from 'this-package-does-not-exist-xyz-123456';",
65
+ platform: "browser",
66
+ format: "esm",
67
+ resolveRoots: [],
68
+ virtualModules: []
69
+ };
70
+ // Bundler must throw - no silent fallback
71
+ await expect(bundlePageScript(plan)).rejects.toThrow();
72
+ });
73
+ test("respects platform setting", async () => {
74
+ const browserPlan = {
75
+ entry: "console.log('browser');",
76
+ platform: "browser",
77
+ format: "esm",
78
+ resolveRoots: [],
79
+ virtualModules: []
80
+ };
81
+ const nodePlan = {
82
+ entry: "console.log('node');",
83
+ platform: "node",
84
+ format: "esm",
85
+ resolveRoots: [],
86
+ virtualModules: []
87
+ };
88
+ const browserResult = await bundlePageScript(browserPlan);
89
+ const nodeResult = await bundlePageScript(nodePlan);
90
+ // Both should execute without error
91
+ expect(browserResult).toContain("browser");
92
+ expect(nodeResult).toContain("node");
93
+ });
94
+ test("respects format setting", async () => {
95
+ const esmPlan = {
96
+ entry: "export const x = 1;",
97
+ platform: "browser",
98
+ format: "esm",
99
+ resolveRoots: [],
100
+ virtualModules: []
101
+ };
102
+ const cjsPlan = {
103
+ entry: "module.exports = { x: 1 };",
104
+ platform: "node",
105
+ format: "cjs",
106
+ resolveRoots: [],
107
+ virtualModules: []
108
+ };
109
+ const esmResult = await bundlePageScript(esmPlan);
110
+ const cjsResult = await bundlePageScript(cjsPlan);
111
+ // ESM should have export, CJS should have exports/module
112
+ expect(esmResult).toBeDefined();
113
+ expect(cjsResult).toBeDefined();
114
+ });
115
+ test("no tree-shaking - unused exports preserved", async () => {
116
+ const plan = {
117
+ entry: `
118
+ import { used } from 'virtual:lib';
119
+ console.log(used);
120
+ `,
121
+ platform: "browser",
122
+ format: "esm",
123
+ resolveRoots: [],
124
+ virtualModules: [{
125
+ id: "virtual:lib",
126
+ code: `
127
+ export const used = 'used';
128
+ export const unused = 'unused';
129
+ `
130
+ }]
131
+ };
132
+ const result = await bundlePageScript(plan);
133
+ // With treeshake: false, unused export should still be in output
134
+ // (This test verifies bundler doesn't infer side effects)
135
+ expect(result).toContain("used");
136
+ });
137
+ });
@@ -0,0 +1 @@
1
+ export {};