@zenithbuild/compiler 1.3.2 → 1.3.9

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 (73) hide show
  1. package/dist/build-analyzer.d.ts +44 -0
  2. package/dist/build-analyzer.js +87 -0
  3. package/dist/bundler.d.ts +31 -0
  4. package/dist/bundler.js +92 -0
  5. package/dist/core/config/index.d.ts +11 -0
  6. package/dist/core/config/index.js +10 -0
  7. package/dist/core/config/loader.d.ts +17 -0
  8. package/dist/core/config/loader.js +60 -0
  9. package/dist/core/config/types.d.ts +98 -0
  10. package/dist/core/config/types.js +32 -0
  11. package/dist/core/index.d.ts +7 -0
  12. package/dist/core/index.js +6 -0
  13. package/dist/core/plugins/bridge.d.ts +116 -0
  14. package/dist/core/plugins/bridge.js +121 -0
  15. package/dist/core/plugins/index.d.ts +6 -0
  16. package/dist/core/plugins/index.js +6 -0
  17. package/dist/core/plugins/registry.d.ts +67 -0
  18. package/dist/core/plugins/registry.js +112 -0
  19. package/dist/css/index.d.ts +73 -0
  20. package/dist/css/index.js +246 -0
  21. package/dist/discovery/componentDiscovery.d.ts +41 -0
  22. package/dist/discovery/componentDiscovery.js +66 -0
  23. package/dist/discovery/layouts.d.ts +14 -0
  24. package/dist/discovery/layouts.js +36 -0
  25. package/dist/errors/compilerError.d.ts +31 -0
  26. package/dist/errors/compilerError.js +51 -0
  27. package/dist/finalize/generateFinalBundle.d.ts +24 -0
  28. package/dist/finalize/generateFinalBundle.js +68 -0
  29. package/dist/index.d.ts +34 -0
  30. package/dist/index.js +44 -0
  31. package/dist/ir/types.d.ts +206 -0
  32. package/dist/ir/types.js +8 -0
  33. package/dist/output/types.d.ts +39 -0
  34. package/dist/output/types.js +6 -0
  35. package/dist/parseZenFile.d.ts +17 -0
  36. package/dist/parseZenFile.js +52 -0
  37. package/dist/runtime/build.d.ts +6 -0
  38. package/dist/runtime/build.js +13 -0
  39. package/dist/runtime/bundle-generator.d.ts +27 -0
  40. package/dist/runtime/bundle-generator.js +1438 -0
  41. package/dist/runtime/client-runtime.d.ts +41 -0
  42. package/dist/runtime/client-runtime.js +397 -0
  43. package/dist/runtime/hydration.d.ts +53 -0
  44. package/dist/runtime/hydration.js +271 -0
  45. package/dist/runtime/navigation.d.ts +58 -0
  46. package/dist/runtime/navigation.js +372 -0
  47. package/dist/runtime/serve.d.ts +13 -0
  48. package/dist/runtime/serve.js +76 -0
  49. package/dist/runtime/thinRuntime.d.ts +23 -0
  50. package/dist/runtime/thinRuntime.js +158 -0
  51. package/dist/spa-build.d.ts +26 -0
  52. package/dist/spa-build.js +849 -0
  53. package/dist/ssg-build.d.ts +31 -0
  54. package/dist/ssg-build.js +429 -0
  55. package/dist/test/bundler-contract.test.d.ts +1 -0
  56. package/dist/test/bundler-contract.test.js +137 -0
  57. package/dist/test/compiler-entry.test.d.ts +1 -0
  58. package/dist/test/compiler-entry.test.js +28 -0
  59. package/dist/test/error-native-bridge.test.d.ts +1 -0
  60. package/dist/test/error-native-bridge.test.js +31 -0
  61. package/dist/test/error-serialization.test.d.ts +1 -0
  62. package/dist/test/error-serialization.test.js +38 -0
  63. package/dist/test/phase5-boundary.test.d.ts +1 -0
  64. package/dist/test/phase5-boundary.test.js +51 -0
  65. package/dist/transform/layoutProcessor.d.ts +26 -0
  66. package/dist/transform/layoutProcessor.js +34 -0
  67. package/dist/validate/invariants.d.ts +23 -0
  68. package/dist/validate/invariants.js +55 -0
  69. package/native/compiler-native/compiler-native.node +0 -0
  70. package/native/compiler-native/index.d.ts +2 -46
  71. package/native/compiler-native/index.js +1 -16
  72. package/native/compiler-native/package.json +1 -1
  73. package/package.json +15 -5
@@ -0,0 +1,31 @@
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>;
@@ -0,0 +1,429 @@
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 { compile } from "./index";
27
+ import { discoverComponents } from "./discovery/componentDiscovery";
28
+ // discoverLayouts removed - layouts are now components
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
+ // Layout discovery removed in Phase A1
50
+ // const layouts = discoverLayouts(layoutsDir)
51
+ // Discover components & layouts
52
+ const componentsDir = path.join(srcDir, 'components');
53
+ const layoutsDir = path.join(srcDir, 'layouts');
54
+ const components = new Map();
55
+ if (fs.existsSync(componentsDir)) {
56
+ const comps = discoverComponents(componentsDir);
57
+ for (const [k, v] of comps)
58
+ components.set(k, v);
59
+ }
60
+ if (fs.existsSync(layoutsDir)) {
61
+ const layoutComps = discoverComponents(layoutsDir);
62
+ for (const [k, v] of layoutComps) {
63
+ // Start with uppercase = component
64
+ if (k[0] === k[0]?.toUpperCase()) {
65
+ components.set(k, v);
66
+ }
67
+ }
68
+ }
69
+ // Compile with unified pipeline
70
+ // const layoutToUse = layouts.get('DefaultLayout')
71
+ const result = await compile(source, pagePath, {
72
+ components,
73
+ // layout: layoutToUse
74
+ });
75
+ if (!result.finalized) {
76
+ throw new Error(`Compilation failed for ${pagePath}: No finalized output`);
77
+ }
78
+ // Extract compiled output
79
+ const html = result.finalized.html;
80
+ const js = result.finalized.js || '';
81
+ const imports = result.finalized.npmImports || '';
82
+ const styles = result.finalized.styles || '';
83
+ // Generate route definition
84
+ const routeDef = generateRouteDefinition(pagePath, pagesDir);
85
+ // Determine output directory from route path
86
+ // "/" -> "index", "/about" -> "about", "/blog/post" -> "blog/post"
87
+ let outputDir = routeDef.path === '/' ? 'index' : routeDef.path.replace(/^\//, '');
88
+ // Handle dynamic routes - they'll be placeholders for now
89
+ // [id] segments become _id_ for folder names
90
+ outputDir = outputDir.replace(/\[([^\]]+)\]/g, '_$1_');
91
+ // Force hydration if we have compiled JS or if top-level analysis detected it
92
+ const needsHydration = analysis.needsHydration || js.trim().length > 0;
93
+ return {
94
+ routePath: routeDef.path,
95
+ filePath: pagePath,
96
+ html,
97
+ pageScript: needsHydration ? js : '',
98
+ pageImports: needsHydration ? imports : '',
99
+ styles,
100
+ score: routeDef.score,
101
+ paramNames: routeDef.paramNames,
102
+ analysis: {
103
+ ...analysis,
104
+ needsHydration,
105
+ isStatic: !needsHydration && !analysis.needsSSR
106
+ },
107
+ outputDir,
108
+ bundlePlan: result.finalized.bundlePlan
109
+ };
110
+ }
111
+ // ============================================
112
+ // HTML Generation
113
+ // ============================================
114
+ /**
115
+ * Generate the final HTML for a page
116
+ * Static pages: no JS references
117
+ * Hydrated pages: bundle.js + page-specific JS
118
+ *
119
+ * Uses the neutral __ZENITH_PLUGIN_DATA__ envelope - CLI never inspects contents.
120
+ */
121
+ function generatePageHTML(page, globalStyles, pluginEnvelope) {
122
+ const { html, styles, analysis, routePath, pageScript } = page;
123
+ // Combine styles
124
+ const pageStyles = styles;
125
+ const allStyles = globalStyles + '\n' + pageStyles;
126
+ // Build script tags only if needed
127
+ let scriptTags = '';
128
+ if (analysis.needsHydration) {
129
+ scriptTags = `
130
+ <script src="/assets/bundle.js"></script>`;
131
+ if (pageScript) {
132
+ // Generate a safe filename from route path
133
+ const pageJsName = routePath === '/'
134
+ ? 'page_index.js'
135
+ : `page_${routePath.replace(/^\//, '').replace(/\//g, '_')}.js`;
136
+ scriptTags += `
137
+ <script type="module" src="/assets/${pageJsName}"></script>`;
138
+ }
139
+ }
140
+ // Check if HTML already has full document structure
141
+ const hasHtmlTag = /<html[^>]*>/i.test(html);
142
+ if (hasHtmlTag) {
143
+ // HTML already has structure from layout - inject styles and scripts
144
+ let finalHtml = html;
145
+ // Inject styles into <head> if not already there
146
+ if (!/<style[^>]*>/.test(finalHtml)) {
147
+ finalHtml = finalHtml.replace('</head>', ` <style>\n${allStyles}\n </style>\n</head>`);
148
+ }
149
+ // Inject scripts before </body>
150
+ if (scriptTags) {
151
+ finalHtml = finalHtml.replace('</body>', `${scriptTags}\n</body>`);
152
+ }
153
+ return finalHtml;
154
+ }
155
+ // Generate full HTML document for pages without layout
156
+ return `<!DOCTYPE html>
157
+ <html lang="en">
158
+ <head>
159
+ <meta charset="UTF-8">
160
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
161
+ <title>Zenith App</title>
162
+ <style>
163
+ ${allStyles}
164
+ </style>
165
+ </head>
166
+ <body>
167
+ ${html}${scriptTags}
168
+ </body>
169
+ </html>`;
170
+ }
171
+ // ============================================
172
+ // Asset Generation
173
+ // ============================================
174
+ /**
175
+ * Generate page-specific JavaScript
176
+ */
177
+ function generatePageJS(page) {
178
+ if (!page.pageScript)
179
+ return '';
180
+ // Module imports must be top-level
181
+ return `// Zenith Page: ${page.routePath}
182
+ // Phase 5: ES Module Mode
183
+
184
+ ${page.pageScript}
185
+
186
+ // Trigger hydration after DOM is ready
187
+ (function() {
188
+ function trigger() {
189
+ if (window.__zenith && window.__zenith.triggerMount) {
190
+ window.__zenith.triggerMount();
191
+ }
192
+ }
193
+
194
+ if (document.readyState === 'loading') {
195
+ document.addEventListener('DOMContentLoaded', trigger);
196
+ } else {
197
+ trigger();
198
+ }
199
+ })();
200
+ `;
201
+ }
202
+ // ============================================
203
+ // Main Build Function
204
+ // ============================================
205
+ /**
206
+ * Build all pages using SSG approach
207
+ *
208
+ * Follows the blind orchestrator pattern:
209
+ * - Plugins are initialized unconditionally
210
+ * - Data is collected via hooks
211
+ * - CLI never inspects plugin data
212
+ */
213
+ export async function buildSSG(options) {
214
+ const { pagesDir, outDir, baseDir = path.dirname(pagesDir) } = options;
215
+ console.log('🔨 Zenith SSG Build');
216
+ console.log(` Pages: ${pagesDir}`);
217
+ console.log(` Output: ${outDir}`);
218
+ console.log('');
219
+ // ============================================
220
+ // Plugin Initialization (Unconditional)
221
+ // ============================================
222
+ // Load config and initialize all plugins without checking which ones exist.
223
+ const config = await loadZenithConfig(baseDir);
224
+ const registry = new PluginRegistry();
225
+ const bridgeAPI = createBridgeAPI();
226
+ // Clear any previously registered hooks
227
+ clearHooks();
228
+ // Register ALL plugins unconditionally
229
+ for (const plugin of config.plugins || []) {
230
+ console.log(` Plugin: ${plugin.name}`);
231
+ registry.register(plugin);
232
+ // Let plugin register its CLI hooks
233
+ if (plugin.registerCLI) {
234
+ plugin.registerCLI(bridgeAPI);
235
+ }
236
+ }
237
+ // Initialize all plugins
238
+ await registry.initAll(createPluginContext(baseDir));
239
+ // Create hook context - CLI provides this but NEVER uses getPluginData itself
240
+ const hookCtx = {
241
+ projectRoot: baseDir,
242
+ getPluginData: getPluginDataByNamespace
243
+ };
244
+ // Collect runtime payloads from ALL plugins
245
+ const payloads = await collectHookReturns('cli:runtime:collect', hookCtx);
246
+ const pluginEnvelope = buildRuntimeEnvelope(payloads);
247
+ console.log('');
248
+ // Clean and create output directory
249
+ if (fs.existsSync(outDir)) {
250
+ fs.rmSync(outDir, { recursive: true, force: true });
251
+ }
252
+ fs.mkdirSync(outDir, { recursive: true });
253
+ fs.mkdirSync(path.join(outDir, 'assets'), { recursive: true });
254
+ // Discover pages
255
+ const pageFiles = discoverPages(pagesDir);
256
+ if (pageFiles.length === 0) {
257
+ console.warn('⚠️ No pages found in', pagesDir);
258
+ return;
259
+ }
260
+ console.log(`📄 Found ${pageFiles.length} page(s)`);
261
+ // Compile all pages
262
+ const compiledPages = [];
263
+ let hasHydratedPages = false;
264
+ for (const pageFile of pageFiles) {
265
+ const relativePath = path.relative(pagesDir, pageFile);
266
+ console.log(` Compiling: ${relativePath}`);
267
+ try {
268
+ const compiled = await compilePage(pageFile, pagesDir, baseDir);
269
+ compiledPages.push(compiled);
270
+ if (compiled.analysis.needsHydration) {
271
+ hasHydratedPages = true;
272
+ }
273
+ const outputType = getBuildOutputType(compiled.analysis);
274
+ const summary = getAnalysisSummary(compiled.analysis);
275
+ // Check if it's "forced" hydration (analysis missed it, but compiler found JS)
276
+ const logType = outputType.toUpperCase();
277
+ console.log(` → ${logType} [${summary}]`);
278
+ }
279
+ catch (error) {
280
+ console.error(` ❌ Error: ${error.message}`);
281
+ throw error;
282
+ }
283
+ }
284
+ console.log('');
285
+ // Compile global styles (Tailwind CSS)
286
+ let globalStyles = '';
287
+ const globalsCssPath = resolveGlobalsCss(baseDir);
288
+ if (globalsCssPath) {
289
+ console.log('📦 Compiling CSS:', path.relative(baseDir, globalsCssPath));
290
+ const cssOutputPath = path.join(outDir, 'assets', 'styles.css');
291
+ const result = compileCss({
292
+ input: globalsCssPath,
293
+ output: cssOutputPath,
294
+ minify: true
295
+ });
296
+ if (result.success) {
297
+ globalStyles = result.css;
298
+ console.log(`📦 Generated assets/styles.css (${result.duration}ms)`);
299
+ }
300
+ else {
301
+ console.error('❌ CSS compilation failed:', result.error);
302
+ }
303
+ }
304
+ // Write bundle.js if any pages need hydration
305
+ if (hasHydratedPages) {
306
+ const bundleJS = generateBundleJS(pluginEnvelope);
307
+ fs.writeFileSync(path.join(outDir, 'assets', 'bundle.js'), bundleJS);
308
+ console.log('📦 Generated assets/bundle.js (with plugin data)');
309
+ }
310
+ // Write each page
311
+ for (const page of compiledPages) {
312
+ // Create output directory
313
+ const pageOutDir = path.join(outDir, page.outputDir);
314
+ fs.mkdirSync(pageOutDir, { recursive: true });
315
+ // Generate and write HTML
316
+ const html = generatePageHTML(page, globalStyles, pluginEnvelope);
317
+ fs.writeFileSync(path.join(pageOutDir, 'index.html'), html);
318
+ // Write page-specific JS if needed
319
+ if (page.pageScript) {
320
+ const pageJsName = page.routePath === '/'
321
+ ? 'page_index.js'
322
+ : `page_${page.routePath.replace(/^\//, '').replace(/\//g, '_')}.js`;
323
+ const pageJS = generatePageJS(page);
324
+ if (page.routePath === '/' && pageJS.includes('</a>')) {
325
+ console.log('🚨 LEAKED JSX DETECTED IN INDEX.ZEN:');
326
+ // print relevant lines
327
+ const lines = pageJS.split('\n');
328
+ lines.forEach((line, i) => {
329
+ if (line.includes('</a>')) {
330
+ console.log(`${i + 1}: ${line.trim()}`);
331
+ }
332
+ });
333
+ }
334
+ // Bundle ONLY if compiler emitted a BundlePlan (no inference)
335
+ let bundledJS = pageJS;
336
+ if (page.bundlePlan) {
337
+ const plan = {
338
+ ...page.bundlePlan,
339
+ entry: pageJS,
340
+ resolveRoots: [path.join(baseDir, 'node_modules'), 'node_modules']
341
+ };
342
+ bundledJS = await bundlePageScript(plan);
343
+ }
344
+ fs.writeFileSync(path.join(outDir, 'assets', pageJsName), bundledJS);
345
+ }
346
+ console.log(`✅ ${page.outputDir}/index.html`);
347
+ }
348
+ // Copy favicon if exists
349
+ const faviconPath = path.join(baseDir, 'favicon.ico');
350
+ if (fs.existsSync(faviconPath)) {
351
+ fs.copyFileSync(faviconPath, path.join(outDir, 'favicon.ico'));
352
+ console.log('📦 Copied favicon.ico');
353
+ }
354
+ // Generate 404 page
355
+ const custom404Candidates = ['404.zen', '+404.zen', 'not-found.zen'];
356
+ let has404 = false;
357
+ for (const candidate of custom404Candidates) {
358
+ const custom404Path = path.join(pagesDir, candidate);
359
+ if (fs.existsSync(custom404Path)) {
360
+ try {
361
+ const compiled = await compilePage(custom404Path, pagesDir, baseDir);
362
+ const html = generatePageHTML(compiled, globalStyles, pluginEnvelope);
363
+ fs.writeFileSync(path.join(outDir, '404.html'), html);
364
+ console.log('📦 Generated 404.html (custom)');
365
+ has404 = true;
366
+ if (compiled.pageScript) {
367
+ const pageJS = generatePageJS(compiled);
368
+ // Bundle ONLY if compiler emitted a BundlePlan (no inference)
369
+ let bundledJS = pageJS;
370
+ if (compiled.bundlePlan) {
371
+ const plan = {
372
+ ...compiled.bundlePlan,
373
+ entry: pageJS,
374
+ resolveRoots: [path.join(baseDir, 'node_modules'), 'node_modules']
375
+ };
376
+ bundledJS = await bundlePageScript(plan);
377
+ }
378
+ fs.writeFileSync(path.join(outDir, 'assets', 'page_404.js'), bundledJS);
379
+ }
380
+ }
381
+ catch (error) {
382
+ console.warn(` ⚠️ Could not compile ${candidate}: ${error.message}`);
383
+ }
384
+ break;
385
+ }
386
+ }
387
+ if (!has404) {
388
+ const default404HTML = `<!DOCTYPE html>
389
+ <html lang="en">
390
+ <head>
391
+ <meta charset="UTF-8">
392
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
393
+ <title>Page Not Found | Zenith</title>
394
+ <style>
395
+ * { box-sizing: border-box; margin: 0; padding: 0; }
396
+ 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; }
397
+ .container { text-align: center; padding: 2rem; }
398
+ .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; }
399
+ h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #e2e8f0; }
400
+ .message { color: #94a3b8; margin-bottom: 2rem; }
401
+ 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; }
402
+ </style>
403
+ </head>
404
+ <body>
405
+ <div class="container">
406
+ <div class="error-code">404</div>
407
+ <h1>Page Not Found</h1>
408
+ <p class="message">The page you're looking for doesn't exist.</p>
409
+ <a href="/">← Go Home</a>
410
+ </div>
411
+ </body>
412
+ </html>`;
413
+ fs.writeFileSync(path.join(outDir, '404.html'), default404HTML);
414
+ console.log('📦 Generated 404.html (default)');
415
+ }
416
+ // Summary
417
+ console.log('');
418
+ console.log('✨ Build complete!');
419
+ console.log(` Static pages: ${compiledPages.filter(p => p.analysis.isStatic).length}`);
420
+ console.log(` Hydrated pages: ${compiledPages.filter(p => p.analysis.needsHydration).length}`);
421
+ console.log(` SSR pages: ${compiledPages.filter(p => p.analysis.needsSSR).length}`);
422
+ console.log('');
423
+ // Route manifest
424
+ console.log('📍 Routes:');
425
+ for (const page of compiledPages.sort((a, b) => b.score - a.score)) {
426
+ const type = getBuildOutputType(page.analysis);
427
+ console.log(` ${page.routePath.padEnd(20)} → ${page.outputDir}/index.html (${type})`);
428
+ }
429
+ }
@@ -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 {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import * as Compiler from '../index';
3
+ describe('Compiler Entry Surface', () => {
4
+ it('exports only compile and types', () => {
5
+ const exports = Object.keys(Compiler);
6
+ // Primary export
7
+ expect(exports).toContain('compile');
8
+ // Strict Ban List (Orchestration/FS logic)
9
+ expect(exports).not.toContain('buildSSG');
10
+ expect(exports).not.toContain('buildSPA');
11
+ expect(exports).not.toContain('discoverComponents');
12
+ expect(exports).not.toContain('bundlePageScript');
13
+ expect(exports).not.toContain('compileZen'); // The file-reader one
14
+ });
15
+ it('compiles a simple component', async () => {
16
+ const source = `<script setup lang="ts">
17
+ prop title: string
18
+ </script>
19
+ <h1>{title}</h1>`;
20
+ // pure compile(source, path)
21
+ const result = await Compiler.compile(source, 'test.zen');
22
+ expect(result.ir).toBeDefined();
23
+ expect(result.compiled).toBeDefined();
24
+ expect(result.finalized).toBeDefined();
25
+ // Output structure check
26
+ expect(result.finalized?.html).toBeTruthy();
27
+ });
28
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { expect, test, describe } from 'bun:test';
2
+ import { compile } from '../index';
3
+ describe('Native Error Bridge (One True Bridge)', () => {
4
+ test('INV005: Rejects <template> tag as invalid Zenith syntax', async () => {
5
+ const source = `
6
+ <script>
7
+ const name = "Zenith";
8
+ </script>
9
+
10
+ <template>
11
+ <div>This is invalid because of the template tag</div>
12
+ </template>
13
+ `;
14
+ try {
15
+ await compile(source, 'test-invalid.zen');
16
+ throw new Error('Should have thrown an InvariantError for <template> tag');
17
+ }
18
+ catch (e) {
19
+ expect(e.code).toBe('INV005');
20
+ expect(e.context).toBe('<template>');
21
+ }
22
+ });
23
+ test('Syscall Protocol: Returns structured errors results', async () => {
24
+ const source = `<script> props="" </script> <div></div>`;
25
+ const result = await compile(source, 'malformed-props.zen');
26
+ // If it didn't throw, it should be a successful result or held error
27
+ if (result.finalized) {
28
+ // result.finalized might have hasErrors: true if not a fatal invariant
29
+ }
30
+ });
31
+ });
@@ -0,0 +1 @@
1
+ export {};