@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.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/build-analyzer.d.ts +44 -0
- package/dist/build-analyzer.js +87 -0
- package/dist/bundler.d.ts +31 -0
- package/dist/bundler.js +86 -0
- package/dist/core/components/index.d.ts +9 -0
- package/dist/core/components/index.js +13 -0
- package/dist/core/config/index.d.ts +11 -0
- package/dist/core/config/index.js +10 -0
- package/dist/core/config/loader.d.ts +17 -0
- package/dist/core/config/loader.js +60 -0
- package/dist/core/config/types.d.ts +98 -0
- package/dist/core/config/types.js +32 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +6 -0
- package/dist/core/lifecycle/index.d.ts +16 -0
- package/dist/core/lifecycle/index.js +19 -0
- package/dist/core/lifecycle/zen-mount.d.ts +66 -0
- package/dist/core/lifecycle/zen-mount.js +151 -0
- package/dist/core/lifecycle/zen-unmount.d.ts +54 -0
- package/dist/core/lifecycle/zen-unmount.js +76 -0
- package/dist/core/plugins/bridge.d.ts +116 -0
- package/dist/core/plugins/bridge.js +121 -0
- package/dist/core/plugins/index.d.ts +6 -0
- package/dist/core/plugins/index.js +6 -0
- package/dist/core/plugins/registry.d.ts +67 -0
- package/dist/core/plugins/registry.js +113 -0
- package/dist/core/reactivity/index.d.ts +30 -0
- package/dist/core/reactivity/index.js +33 -0
- package/dist/core/reactivity/tracking.d.ts +74 -0
- package/dist/core/reactivity/tracking.js +136 -0
- package/dist/core/reactivity/zen-batch.d.ts +45 -0
- package/dist/core/reactivity/zen-batch.js +54 -0
- package/dist/core/reactivity/zen-effect.d.ts +48 -0
- package/dist/core/reactivity/zen-effect.js +98 -0
- package/dist/core/reactivity/zen-memo.d.ts +43 -0
- package/dist/core/reactivity/zen-memo.js +100 -0
- package/dist/core/reactivity/zen-ref.d.ts +44 -0
- package/dist/core/reactivity/zen-ref.js +34 -0
- package/dist/core/reactivity/zen-signal.d.ts +48 -0
- package/dist/core/reactivity/zen-signal.js +84 -0
- package/dist/core/reactivity/zen-state.d.ts +35 -0
- package/dist/core/reactivity/zen-state.js +147 -0
- package/dist/core/reactivity/zen-untrack.d.ts +38 -0
- package/dist/core/reactivity/zen-untrack.js +41 -0
- package/dist/css/index.d.ts +73 -0
- package/dist/css/index.js +246 -0
- package/dist/discovery/componentDiscovery.d.ts +42 -0
- package/dist/discovery/componentDiscovery.js +56 -0
- package/dist/discovery/layouts.d.ts +13 -0
- package/dist/discovery/layouts.js +41 -0
- package/dist/errors/compilerError.d.ts +31 -0
- package/dist/errors/compilerError.js +51 -0
- package/dist/finalize/finalizeOutput.d.ts +32 -0
- package/dist/finalize/finalizeOutput.js +62 -0
- package/dist/finalize/generateFinalBundle.d.ts +24 -0
- package/dist/finalize/generateFinalBundle.js +68 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +51 -0
- package/dist/ir/types.d.ts +181 -0
- package/dist/ir/types.js +8 -0
- package/dist/output/types.d.ts +30 -0
- package/dist/output/types.js +6 -0
- package/dist/parse/detectMapExpressions.d.ts +45 -0
- package/dist/parse/detectMapExpressions.js +77 -0
- package/dist/parse/parseScript.d.ts +8 -0
- package/dist/parse/parseScript.js +36 -0
- package/dist/parse/parseTemplate.d.ts +11 -0
- package/dist/parse/parseTemplate.js +487 -0
- package/dist/parse/parseZenFile.d.ts +11 -0
- package/dist/parse/parseZenFile.js +50 -0
- package/dist/parse/scriptAnalysis.d.ts +25 -0
- package/dist/parse/scriptAnalysis.js +60 -0
- package/dist/parse/trackLoopContext.d.ts +20 -0
- package/dist/parse/trackLoopContext.js +62 -0
- package/dist/parseZenFile.d.ts +10 -0
- package/dist/parseZenFile.js +55 -0
- package/dist/runtime/analyzeAndEmit.d.ts +20 -0
- package/dist/runtime/analyzeAndEmit.js +70 -0
- package/dist/runtime/build.d.ts +6 -0
- package/dist/runtime/build.js +13 -0
- package/dist/runtime/bundle-generator.d.ts +27 -0
- package/dist/runtime/bundle-generator.js +1263 -0
- package/dist/runtime/client-runtime.d.ts +41 -0
- package/dist/runtime/client-runtime.js +397 -0
- package/dist/runtime/dataExposure.d.ts +52 -0
- package/dist/runtime/dataExposure.js +227 -0
- package/dist/runtime/generateDOM.d.ts +21 -0
- package/dist/runtime/generateDOM.js +194 -0
- package/dist/runtime/generateHydrationBundle.d.ts +15 -0
- package/dist/runtime/generateHydrationBundle.js +399 -0
- package/dist/runtime/hydration.d.ts +53 -0
- package/dist/runtime/hydration.js +271 -0
- package/dist/runtime/navigation.d.ts +58 -0
- package/dist/runtime/navigation.js +372 -0
- package/dist/runtime/serve.d.ts +13 -0
- package/dist/runtime/serve.js +76 -0
- package/dist/runtime/thinRuntime.d.ts +23 -0
- package/dist/runtime/thinRuntime.js +158 -0
- package/dist/runtime/transformIR.d.ts +19 -0
- package/dist/runtime/transformIR.js +285 -0
- package/dist/runtime/wrapExpression.d.ts +24 -0
- package/dist/runtime/wrapExpression.js +76 -0
- package/dist/runtime/wrapExpressionWithLoop.d.ts +17 -0
- package/dist/runtime/wrapExpressionWithLoop.js +75 -0
- package/dist/spa-build.d.ts +26 -0
- package/dist/spa-build.js +866 -0
- package/dist/ssg-build.d.ts +32 -0
- package/dist/ssg-build.js +408 -0
- package/dist/test/analyze-emit.test.d.ts +1 -0
- package/dist/test/analyze-emit.test.js +88 -0
- package/dist/test/bundler-contract.test.d.ts +1 -0
- package/dist/test/bundler-contract.test.js +137 -0
- package/dist/test/compiler-authority.test.d.ts +1 -0
- package/dist/test/compiler-authority.test.js +90 -0
- package/dist/test/component-instance-test.d.ts +1 -0
- package/dist/test/component-instance-test.js +115 -0
- package/dist/test/error-native-bridge.test.d.ts +1 -0
- package/dist/test/error-native-bridge.test.js +51 -0
- package/dist/test/error-serialization.test.d.ts +1 -0
- package/dist/test/error-serialization.test.js +38 -0
- package/dist/test/macro-inlining.test.d.ts +1 -0
- package/dist/test/macro-inlining.test.js +178 -0
- package/dist/test/validate-test.d.ts +6 -0
- package/dist/test/validate-test.js +95 -0
- package/dist/transform/classifyExpression.d.ts +46 -0
- package/dist/transform/classifyExpression.js +354 -0
- package/dist/transform/componentResolver.d.ts +15 -0
- package/dist/transform/componentResolver.js +30 -0
- package/dist/transform/expressionTransformer.d.ts +19 -0
- package/dist/transform/expressionTransformer.js +333 -0
- package/dist/transform/fragmentLowering.d.ts +25 -0
- package/dist/transform/fragmentLowering.js +468 -0
- package/dist/transform/layoutProcessor.d.ts +5 -0
- package/dist/transform/layoutProcessor.js +34 -0
- package/dist/transform/transformTemplate.d.ts +11 -0
- package/dist/transform/transformTemplate.js +33 -0
- package/dist/validate/invariants.d.ts +23 -0
- package/dist/validate/invariants.js +55 -0
- package/native/compiler-native/compiler-native.node +0 -0
- package/native/compiler-native/index.d.ts +113 -0
- package/native/compiler-native/index.js +19 -0
- package/native/compiler-native/package.json +19 -0
- 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 {};
|