@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,866 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith SPA Build System
|
|
3
|
+
*
|
|
4
|
+
* Builds all pages into a single index.html with:
|
|
5
|
+
* - Route manifest
|
|
6
|
+
* - Compiled page modules (inlined)
|
|
7
|
+
* - Runtime router
|
|
8
|
+
* - Shell HTML with router outlet
|
|
9
|
+
*/
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
// Import new compiler
|
|
13
|
+
import { compileZenSource } from "./index";
|
|
14
|
+
import { discoverLayouts } from "./discovery/layouts";
|
|
15
|
+
import { processLayout } from "./transform/layoutProcessor";
|
|
16
|
+
import { discoverPages, generateRouteDefinition, routePathToRegex } from "@zenithbuild/router/manifest";
|
|
17
|
+
/**
|
|
18
|
+
* Compile a single page file
|
|
19
|
+
*/
|
|
20
|
+
async function compilePage(pagePath, pagesDir, baseDir = process.cwd()) {
|
|
21
|
+
try {
|
|
22
|
+
const layoutsDir = path.join(baseDir, 'app', 'layouts');
|
|
23
|
+
const layouts = discoverLayouts(layoutsDir);
|
|
24
|
+
const source = fs.readFileSync(pagePath, 'utf-8');
|
|
25
|
+
// Find suitable layout
|
|
26
|
+
let processedSource = source;
|
|
27
|
+
let layoutToUse = layouts.get('DefaultLayout');
|
|
28
|
+
if (layoutToUse) {
|
|
29
|
+
processedSource = processLayout(source, layoutToUse);
|
|
30
|
+
}
|
|
31
|
+
// Use new compiler pipeline on the processed source
|
|
32
|
+
const result = await compileZenSource(processedSource, pagePath);
|
|
33
|
+
if (!result.finalized) {
|
|
34
|
+
throw new Error(`Compilation failed: No finalized output`);
|
|
35
|
+
}
|
|
36
|
+
// Extract compiled output
|
|
37
|
+
const html = result.finalized.html;
|
|
38
|
+
const js = result.finalized.js;
|
|
39
|
+
const styles = result.finalized.styles;
|
|
40
|
+
// Convert JS bundle to scripts array (for compatibility)
|
|
41
|
+
const scripts = js ? [js] : [];
|
|
42
|
+
// Generate route definition
|
|
43
|
+
const routeDef = generateRouteDefinition(pagePath, pagesDir);
|
|
44
|
+
const regex = routePathToRegex(routeDef.path);
|
|
45
|
+
return {
|
|
46
|
+
routePath: routeDef.path,
|
|
47
|
+
filePath: pagePath,
|
|
48
|
+
html,
|
|
49
|
+
scripts,
|
|
50
|
+
styles,
|
|
51
|
+
score: routeDef.score,
|
|
52
|
+
paramNames: routeDef.paramNames,
|
|
53
|
+
regex
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error(`[Zenith Build] Compilation failed for ${pagePath}:`, error.message);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate the runtime router code (inlined into the bundle)
|
|
63
|
+
*/
|
|
64
|
+
function generateRuntimeRouterCode() {
|
|
65
|
+
return `
|
|
66
|
+
// ============================================
|
|
67
|
+
// Zenith Runtime Router
|
|
68
|
+
// ============================================
|
|
69
|
+
|
|
70
|
+
(function() {
|
|
71
|
+
'use strict';
|
|
72
|
+
|
|
73
|
+
// Current route state
|
|
74
|
+
let currentRoute = {
|
|
75
|
+
path: '/',
|
|
76
|
+
params: {},
|
|
77
|
+
query: {}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Route listeners
|
|
81
|
+
const routeListeners = new Set();
|
|
82
|
+
|
|
83
|
+
// Router outlet element
|
|
84
|
+
let routerOutlet = null;
|
|
85
|
+
|
|
86
|
+
// Page modules registry
|
|
87
|
+
const pageModules = {};
|
|
88
|
+
|
|
89
|
+
// Route manifest
|
|
90
|
+
let routeManifest = [];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse query string
|
|
94
|
+
*/
|
|
95
|
+
function parseQueryString(search) {
|
|
96
|
+
const query = {};
|
|
97
|
+
if (!search || search === '?') return query;
|
|
98
|
+
const params = new URLSearchParams(search);
|
|
99
|
+
params.forEach((value, key) => { query[key] = value; });
|
|
100
|
+
return query;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve route from pathname
|
|
105
|
+
*/
|
|
106
|
+
function resolveRoute(pathname) {
|
|
107
|
+
const normalizedPath = pathname === '' ? '/' : pathname;
|
|
108
|
+
|
|
109
|
+
for (const route of routeManifest) {
|
|
110
|
+
const match = route.regex.exec(normalizedPath);
|
|
111
|
+
if (match) {
|
|
112
|
+
const params = {};
|
|
113
|
+
for (let i = 0; i < route.paramNames.length; i++) {
|
|
114
|
+
const paramValue = match[i + 1];
|
|
115
|
+
if (paramValue !== undefined) {
|
|
116
|
+
params[route.paramNames[i]] = decodeURIComponent(paramValue);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { record: route, params };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clean up previous page
|
|
127
|
+
*/
|
|
128
|
+
function cleanupPreviousPage() {
|
|
129
|
+
// Trigger unmount lifecycle hooks
|
|
130
|
+
if (window.__zenith && window.__zenith.triggerUnmount) {
|
|
131
|
+
window.__zenith.triggerUnmount();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remove previous page styles
|
|
135
|
+
document.querySelectorAll('style[data-zen-page-style]').forEach(s => s.remove());
|
|
136
|
+
|
|
137
|
+
// Clean up window properties (state variables, functions)
|
|
138
|
+
// This is important for proper state isolation between pages
|
|
139
|
+
if (window.__zenith_cleanup) {
|
|
140
|
+
window.__zenith_cleanup.forEach(key => {
|
|
141
|
+
try { delete window[key]; } catch(e) {}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
window.__zenith_cleanup = [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Inject styles
|
|
149
|
+
*/
|
|
150
|
+
function injectStyles(styles) {
|
|
151
|
+
styles.forEach((content, i) => {
|
|
152
|
+
const style = document.createElement('style');
|
|
153
|
+
style.setAttribute('data-zen-page-style', String(i));
|
|
154
|
+
style.textContent = content;
|
|
155
|
+
document.head.appendChild(style);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Execute scripts
|
|
161
|
+
*/
|
|
162
|
+
function executeScripts(scripts) {
|
|
163
|
+
scripts.forEach(content => {
|
|
164
|
+
try {
|
|
165
|
+
const fn = new Function(content);
|
|
166
|
+
fn();
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error('[Zenith Router] Script error:', e);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Render page
|
|
175
|
+
*/
|
|
176
|
+
function renderPage(pageModule) {
|
|
177
|
+
if (!routerOutlet) {
|
|
178
|
+
console.warn('[Zenith Router] No router outlet');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
cleanupPreviousPage();
|
|
183
|
+
routerOutlet.innerHTML = pageModule.html;
|
|
184
|
+
injectStyles(pageModule.styles);
|
|
185
|
+
executeScripts(pageModule.scripts);
|
|
186
|
+
|
|
187
|
+
// Trigger mount lifecycle hooks after scripts are executed
|
|
188
|
+
if (window.__zenith && window.__zenith.triggerMount) {
|
|
189
|
+
window.__zenith.triggerMount();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Notify listeners
|
|
195
|
+
*/
|
|
196
|
+
function notifyListeners(route, prevRoute) {
|
|
197
|
+
routeListeners.forEach(listener => {
|
|
198
|
+
try { listener(route, prevRoute); } catch(e) {}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve and render
|
|
204
|
+
*/
|
|
205
|
+
function resolveAndRender(path, query, updateHistory, replace) {
|
|
206
|
+
replace = replace || false;
|
|
207
|
+
const prevRoute = { ...currentRoute };
|
|
208
|
+
const resolved = resolveRoute(path);
|
|
209
|
+
|
|
210
|
+
if (resolved) {
|
|
211
|
+
currentRoute = {
|
|
212
|
+
path,
|
|
213
|
+
params: resolved.params,
|
|
214
|
+
query,
|
|
215
|
+
matched: resolved.record
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const pageModule = pageModules[resolved.record.path];
|
|
219
|
+
if (pageModule) {
|
|
220
|
+
renderPage(pageModule);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
currentRoute = { path, params: {}, query, matched: undefined };
|
|
224
|
+
console.warn('[Zenith Router] No route matched:', path);
|
|
225
|
+
|
|
226
|
+
// Render 404 if available, otherwise show message
|
|
227
|
+
if (routerOutlet) {
|
|
228
|
+
routerOutlet.innerHTML = '<div style="padding: 2rem; text-align: center;"><h1>404</h1><p>Page not found</p></div>';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (updateHistory) {
|
|
233
|
+
const url = path + (Object.keys(query).length ? '?' + new URLSearchParams(query) : '');
|
|
234
|
+
if (replace) {
|
|
235
|
+
window.history.replaceState(null, '', url);
|
|
236
|
+
} else {
|
|
237
|
+
window.history.pushState(null, '', url);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
notifyListeners(currentRoute, prevRoute);
|
|
242
|
+
window.__zenith_route = currentRoute;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Handle popstate
|
|
247
|
+
*/
|
|
248
|
+
function handlePopState() {
|
|
249
|
+
// Don't update history on popstate - browser already changed it
|
|
250
|
+
resolveAndRender(
|
|
251
|
+
window.location.pathname,
|
|
252
|
+
parseQueryString(window.location.search),
|
|
253
|
+
false,
|
|
254
|
+
false
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Navigate (public API)
|
|
260
|
+
*/
|
|
261
|
+
function navigate(to, options) {
|
|
262
|
+
options = options || {};
|
|
263
|
+
let path, query = {};
|
|
264
|
+
|
|
265
|
+
if (to.includes('?')) {
|
|
266
|
+
const parts = to.split('?');
|
|
267
|
+
path = parts[0];
|
|
268
|
+
query = parseQueryString('?' + parts[1]);
|
|
269
|
+
} else {
|
|
270
|
+
path = to;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!path.startsWith('/')) {
|
|
274
|
+
const currentDir = currentRoute.path.split('/').slice(0, -1).join('/');
|
|
275
|
+
path = currentDir + '/' + path;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Normalize path for comparison
|
|
279
|
+
const normalizedPath = path === '' ? '/' : path;
|
|
280
|
+
const currentPath = currentRoute.path === '' ? '/' : currentRoute.path;
|
|
281
|
+
|
|
282
|
+
// Check if we're already on this path
|
|
283
|
+
const isSamePath = normalizedPath === currentPath;
|
|
284
|
+
|
|
285
|
+
// If same path and same query, don't navigate (idempotent)
|
|
286
|
+
if (isSamePath && JSON.stringify(query) === JSON.stringify(currentRoute.query)) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Resolve and render with replace option if specified
|
|
291
|
+
resolveAndRender(path, query, true, options.replace || false);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get current route
|
|
296
|
+
*/
|
|
297
|
+
function getRoute() {
|
|
298
|
+
return { ...currentRoute };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Subscribe to route changes
|
|
303
|
+
*/
|
|
304
|
+
function onRouteChange(listener) {
|
|
305
|
+
routeListeners.add(listener);
|
|
306
|
+
return () => routeListeners.delete(listener);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if path is active
|
|
311
|
+
*/
|
|
312
|
+
function isActive(path, exact) {
|
|
313
|
+
if (exact) return currentRoute.path === path;
|
|
314
|
+
return currentRoute.path.startsWith(path);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Prefetch a route (preload page module)
|
|
319
|
+
*/
|
|
320
|
+
const prefetchedRoutes = new Set();
|
|
321
|
+
function prefetch(path) {
|
|
322
|
+
const normalizedPath = path === '' ? '/' : path;
|
|
323
|
+
console.log('[Zenith Router] Prefetch requested for:', normalizedPath);
|
|
324
|
+
|
|
325
|
+
if (prefetchedRoutes.has(normalizedPath)) {
|
|
326
|
+
console.log('[Zenith Router] Route already prefetched:', normalizedPath);
|
|
327
|
+
return Promise.resolve();
|
|
328
|
+
}
|
|
329
|
+
prefetchedRoutes.add(normalizedPath);
|
|
330
|
+
|
|
331
|
+
// Find matching route
|
|
332
|
+
const resolved = resolveRoute(normalizedPath);
|
|
333
|
+
if (!resolved) {
|
|
334
|
+
console.warn('[Zenith Router] Prefetch: No route found for:', normalizedPath);
|
|
335
|
+
return Promise.resolve();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log('[Zenith Router] Prefetch: Route resolved:', resolved.record.path);
|
|
339
|
+
|
|
340
|
+
// Preload the module if it exists
|
|
341
|
+
if (pageModules[resolved.record.path]) {
|
|
342
|
+
console.log('[Zenith Router] Prefetch: Module already loaded:', resolved.record.path);
|
|
343
|
+
// Module already loaded
|
|
344
|
+
return Promise.resolve();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log('[Zenith Router] Prefetch: Module not yet loaded (all modules are pre-loaded in SPA build)');
|
|
348
|
+
// In SPA build, all modules are already loaded, so this is a no-op
|
|
349
|
+
// Could prefetch here if we had a way to load modules dynamically
|
|
350
|
+
return Promise.resolve();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Initialize router
|
|
355
|
+
*/
|
|
356
|
+
function initRouter(manifest, modules, outlet) {
|
|
357
|
+
routeManifest = manifest;
|
|
358
|
+
Object.assign(pageModules, modules);
|
|
359
|
+
|
|
360
|
+
if (outlet) {
|
|
361
|
+
routerOutlet = typeof outlet === 'string'
|
|
362
|
+
? document.querySelector(outlet)
|
|
363
|
+
: outlet;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
window.addEventListener('popstate', handlePopState);
|
|
367
|
+
|
|
368
|
+
// Initial route resolution
|
|
369
|
+
resolveAndRender(
|
|
370
|
+
window.location.pathname,
|
|
371
|
+
parseQueryString(window.location.search),
|
|
372
|
+
false
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Expose router API globally
|
|
377
|
+
window.__zenith_router = {
|
|
378
|
+
navigate,
|
|
379
|
+
getRoute,
|
|
380
|
+
onRouteChange,
|
|
381
|
+
isActive,
|
|
382
|
+
prefetch,
|
|
383
|
+
initRouter
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Also expose navigate directly for convenience
|
|
387
|
+
window.navigate = navigate;
|
|
388
|
+
|
|
389
|
+
})();
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Generate the Zen primitives runtime code
|
|
394
|
+
* This makes zen* primitives available globally for auto-imports
|
|
395
|
+
*/
|
|
396
|
+
function generateZenPrimitivesRuntime() {
|
|
397
|
+
return `
|
|
398
|
+
// ============================================
|
|
399
|
+
// Zenith Reactivity Primitives Runtime
|
|
400
|
+
// ============================================
|
|
401
|
+
// Auto-imported zen* primitives are resolved from window.__zenith
|
|
402
|
+
|
|
403
|
+
(function() {
|
|
404
|
+
'use strict';
|
|
405
|
+
|
|
406
|
+
// ============================================
|
|
407
|
+
// Dependency Tracking System
|
|
408
|
+
// ============================================
|
|
409
|
+
|
|
410
|
+
let currentEffect = null;
|
|
411
|
+
const effectStack = [];
|
|
412
|
+
let batchDepth = 0;
|
|
413
|
+
const pendingEffects = new Set();
|
|
414
|
+
|
|
415
|
+
function pushContext(effect) {
|
|
416
|
+
effectStack.push(currentEffect);
|
|
417
|
+
currentEffect = effect;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function popContext() {
|
|
421
|
+
currentEffect = effectStack.pop() || null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function trackDependency(subscribers) {
|
|
425
|
+
if (currentEffect) {
|
|
426
|
+
subscribers.add(currentEffect);
|
|
427
|
+
currentEffect.dependencies.add(subscribers);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function notifySubscribers(subscribers) {
|
|
432
|
+
const effects = [...subscribers];
|
|
433
|
+
for (const effect of effects) {
|
|
434
|
+
if (batchDepth > 0) {
|
|
435
|
+
pendingEffects.add(effect);
|
|
436
|
+
} else {
|
|
437
|
+
effect.run();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function cleanupEffect(effect) {
|
|
443
|
+
for (const deps of effect.dependencies) {
|
|
444
|
+
deps.delete(effect);
|
|
445
|
+
}
|
|
446
|
+
effect.dependencies.clear();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============================================
|
|
450
|
+
// zenSignal - Atomic reactive value
|
|
451
|
+
// ============================================
|
|
452
|
+
|
|
453
|
+
function zenSignal(initialValue) {
|
|
454
|
+
let value = initialValue;
|
|
455
|
+
const subscribers = new Set();
|
|
456
|
+
|
|
457
|
+
function signal(newValue) {
|
|
458
|
+
if (arguments.length === 0) {
|
|
459
|
+
trackDependency(subscribers);
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
if (newValue !== value) {
|
|
463
|
+
value = newValue;
|
|
464
|
+
notifySubscribers(subscribers);
|
|
465
|
+
// Bridge to Phase 5 Hydration: Trigger global update
|
|
466
|
+
if (typeof window !== 'undefined' && window.__zenith_update && window.__ZENITH_STATE__) {
|
|
467
|
+
window.__zenith_update(window.__ZENITH_STATE__);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Add .value property for Vue/Ref-like usage
|
|
474
|
+
Object.defineProperty(signal, 'value', {
|
|
475
|
+
get() {
|
|
476
|
+
return signal();
|
|
477
|
+
},
|
|
478
|
+
set(newValue) {
|
|
479
|
+
signal(newValue);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return signal;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ============================================
|
|
487
|
+
// zenState - Deep reactive object
|
|
488
|
+
// ============================================
|
|
489
|
+
|
|
490
|
+
function zenState(initialObj) {
|
|
491
|
+
const subscribers = new Map(); // path -> Set of effects
|
|
492
|
+
|
|
493
|
+
function getSubscribers(path) {
|
|
494
|
+
if (!subscribers.has(path)) {
|
|
495
|
+
subscribers.set(path, new Set());
|
|
496
|
+
}
|
|
497
|
+
return subscribers.get(path);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function createProxy(obj, path = '') {
|
|
501
|
+
if (typeof obj !== 'object' || obj === null) return obj;
|
|
502
|
+
|
|
503
|
+
return new Proxy(obj, {
|
|
504
|
+
get(target, prop) {
|
|
505
|
+
const propPath = path ? path + '.' + String(prop) : String(prop);
|
|
506
|
+
trackDependency(getSubscribers(propPath));
|
|
507
|
+
const value = target[prop];
|
|
508
|
+
if (typeof value === 'object' && value !== null) {
|
|
509
|
+
return createProxy(value, propPath);
|
|
510
|
+
}
|
|
511
|
+
return value;
|
|
512
|
+
},
|
|
513
|
+
set(target, prop, value) {
|
|
514
|
+
const propPath = path ? path + '.' + String(prop) : String(prop);
|
|
515
|
+
target[prop] = value;
|
|
516
|
+
notifySubscribers(getSubscribers(propPath));
|
|
517
|
+
// Also notify parent path for nested updates
|
|
518
|
+
if (path) {
|
|
519
|
+
notifySubscribers(getSubscribers(path));
|
|
520
|
+
}
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return createProxy(initialObj);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ============================================
|
|
530
|
+
// zenEffect - Auto-tracked side effect
|
|
531
|
+
// ============================================
|
|
532
|
+
|
|
533
|
+
function zenEffect(fn) {
|
|
534
|
+
const effect = {
|
|
535
|
+
fn,
|
|
536
|
+
dependencies: new Set(),
|
|
537
|
+
run() {
|
|
538
|
+
cleanupEffect(this);
|
|
539
|
+
pushContext(this);
|
|
540
|
+
try {
|
|
541
|
+
this.fn();
|
|
542
|
+
} finally {
|
|
543
|
+
popContext();
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
dispose() {
|
|
547
|
+
cleanupEffect(this);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
effect.run();
|
|
552
|
+
return () => effect.dispose();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ============================================
|
|
556
|
+
// zenMemo - Cached computed value
|
|
557
|
+
// ============================================
|
|
558
|
+
|
|
559
|
+
function zenMemo(fn) {
|
|
560
|
+
let cachedValue;
|
|
561
|
+
let dirty = true;
|
|
562
|
+
const subscribers = new Set();
|
|
563
|
+
|
|
564
|
+
const effect = {
|
|
565
|
+
dependencies: new Set(),
|
|
566
|
+
run() {
|
|
567
|
+
dirty = true;
|
|
568
|
+
notifySubscribers(subscribers);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
function compute() {
|
|
573
|
+
if (dirty) {
|
|
574
|
+
cleanupEffect(effect);
|
|
575
|
+
pushContext(effect);
|
|
576
|
+
try {
|
|
577
|
+
cachedValue = fn();
|
|
578
|
+
dirty = false;
|
|
579
|
+
} finally {
|
|
580
|
+
popContext();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
trackDependency(subscribers);
|
|
584
|
+
return cachedValue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return compute;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ============================================
|
|
591
|
+
// zenRef - Non-reactive mutable container
|
|
592
|
+
// ============================================
|
|
593
|
+
|
|
594
|
+
function zenRef(initialValue) {
|
|
595
|
+
return { current: initialValue !== undefined ? initialValue : null };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ============================================
|
|
599
|
+
// zenBatch - Batch updates
|
|
600
|
+
// ============================================
|
|
601
|
+
|
|
602
|
+
function zenBatch(fn) {
|
|
603
|
+
batchDepth++;
|
|
604
|
+
try {
|
|
605
|
+
fn();
|
|
606
|
+
} finally {
|
|
607
|
+
batchDepth--;
|
|
608
|
+
if (batchDepth === 0) {
|
|
609
|
+
const effects = [...pendingEffects];
|
|
610
|
+
pendingEffects.clear();
|
|
611
|
+
for (const effect of effects) {
|
|
612
|
+
effect.run();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ============================================
|
|
619
|
+
// zenUntrack - Read without tracking
|
|
620
|
+
// ============================================
|
|
621
|
+
|
|
622
|
+
function zenUntrack(fn) {
|
|
623
|
+
const prevEffect = currentEffect;
|
|
624
|
+
currentEffect = null;
|
|
625
|
+
try {
|
|
626
|
+
return fn();
|
|
627
|
+
} finally {
|
|
628
|
+
currentEffect = prevEffect;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ============================================
|
|
633
|
+
// Lifecycle Hooks
|
|
634
|
+
// ============================================
|
|
635
|
+
|
|
636
|
+
const mountCallbacks = [];
|
|
637
|
+
const unmountCallbacks = [];
|
|
638
|
+
let isMounted = false;
|
|
639
|
+
|
|
640
|
+
function zenOnMount(fn) {
|
|
641
|
+
if (isMounted) {
|
|
642
|
+
// Already mounted, run immediately
|
|
643
|
+
const cleanup = fn();
|
|
644
|
+
if (typeof cleanup === 'function') {
|
|
645
|
+
unmountCallbacks.push(cleanup);
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
mountCallbacks.push(fn);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function zenOnUnmount(fn) {
|
|
653
|
+
unmountCallbacks.push(fn);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Called by router when page mounts
|
|
657
|
+
function triggerMount() {
|
|
658
|
+
isMounted = true;
|
|
659
|
+
for (const cb of mountCallbacks) {
|
|
660
|
+
const cleanup = cb();
|
|
661
|
+
if (typeof cleanup === 'function') {
|
|
662
|
+
unmountCallbacks.push(cleanup);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
mountCallbacks.length = 0;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Called by router when page unmounts
|
|
669
|
+
function triggerUnmount() {
|
|
670
|
+
isMounted = false;
|
|
671
|
+
for (const cb of unmountCallbacks) {
|
|
672
|
+
try { cb(); } catch(e) { console.error('[Zenith] Unmount error:', e); }
|
|
673
|
+
}
|
|
674
|
+
unmountCallbacks.length = 0;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ============================================
|
|
678
|
+
// Export to window.__zenith
|
|
679
|
+
// ============================================
|
|
680
|
+
|
|
681
|
+
window.__zenith = {
|
|
682
|
+
// Reactivity primitives
|
|
683
|
+
signal: zenSignal,
|
|
684
|
+
state: zenState,
|
|
685
|
+
effect: zenEffect,
|
|
686
|
+
memo: zenMemo,
|
|
687
|
+
ref: zenRef,
|
|
688
|
+
batch: zenBatch,
|
|
689
|
+
untrack: zenUntrack,
|
|
690
|
+
// Lifecycle
|
|
691
|
+
onMount: zenOnMount,
|
|
692
|
+
onUnmount: zenOnUnmount,
|
|
693
|
+
// Internal hooks for router
|
|
694
|
+
triggerMount,
|
|
695
|
+
triggerUnmount
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// Also expose with zen* prefix for direct usage
|
|
699
|
+
window.zenSignal = zenSignal;
|
|
700
|
+
window.zenState = zenState;
|
|
701
|
+
window.zenEffect = zenEffect;
|
|
702
|
+
window.zenMemo = zenMemo;
|
|
703
|
+
window.zenRef = zenRef;
|
|
704
|
+
window.zenBatch = zenBatch;
|
|
705
|
+
window.zenUntrack = zenUntrack;
|
|
706
|
+
window.zenOnMount = zenOnMount;
|
|
707
|
+
window.zenOnUnmount = zenOnUnmount;
|
|
708
|
+
|
|
709
|
+
// Clean aliases
|
|
710
|
+
window.signal = zenSignal;
|
|
711
|
+
window.state = zenState;
|
|
712
|
+
window.effect = zenEffect;
|
|
713
|
+
window.memo = zenMemo;
|
|
714
|
+
window.ref = zenRef;
|
|
715
|
+
window.batch = zenBatch;
|
|
716
|
+
window.untrack = zenUntrack;
|
|
717
|
+
window.onMount = zenOnMount;
|
|
718
|
+
window.onUnmount = zenOnUnmount;
|
|
719
|
+
|
|
720
|
+
})();
|
|
721
|
+
`;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Generate the HTML shell
|
|
725
|
+
*/
|
|
726
|
+
function generateHTMLShell(pages, layoutStyles) {
|
|
727
|
+
// Collect all global styles (from layouts)
|
|
728
|
+
const globalStyles = layoutStyles.join("\n");
|
|
729
|
+
// Generate route manifest JavaScript
|
|
730
|
+
const manifestJS = pages.map(page => ({
|
|
731
|
+
path: page.routePath,
|
|
732
|
+
regex: page.regex.toString(),
|
|
733
|
+
paramNames: page.paramNames,
|
|
734
|
+
score: page.score,
|
|
735
|
+
filePath: page.filePath
|
|
736
|
+
}));
|
|
737
|
+
// Generate page modules JavaScript
|
|
738
|
+
const modulesJS = pages.map(page => {
|
|
739
|
+
const escapedHtml = JSON.stringify(page.html);
|
|
740
|
+
const escapedScripts = JSON.stringify(page.scripts);
|
|
741
|
+
const escapedStyles = JSON.stringify(page.styles);
|
|
742
|
+
return `${JSON.stringify(page.routePath)}: {
|
|
743
|
+
html: ${escapedHtml},
|
|
744
|
+
scripts: ${escapedScripts},
|
|
745
|
+
styles: ${escapedStyles}
|
|
746
|
+
}`;
|
|
747
|
+
}).join(",\n ");
|
|
748
|
+
// Generate manifest with actual RegExp objects
|
|
749
|
+
const manifestCode = `[
|
|
750
|
+
${pages.map(page => `{
|
|
751
|
+
path: ${JSON.stringify(page.routePath)},
|
|
752
|
+
regex: ${page.regex.toString()},
|
|
753
|
+
paramNames: ${JSON.stringify(page.paramNames)},
|
|
754
|
+
score: ${page.score}
|
|
755
|
+
}`).join(",\n ")}
|
|
756
|
+
]`;
|
|
757
|
+
return `<!DOCTYPE html>
|
|
758
|
+
<html lang="en">
|
|
759
|
+
<head>
|
|
760
|
+
<meta charset="UTF-8">
|
|
761
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
762
|
+
<title>Zenith App</title>
|
|
763
|
+
<link rel="icon" type="image/x-icon" href="./favicon.ico">
|
|
764
|
+
<style>
|
|
765
|
+
/* Global/Layout Styles */
|
|
766
|
+
${globalStyles}
|
|
767
|
+
</style>
|
|
768
|
+
</head>
|
|
769
|
+
<body>
|
|
770
|
+
<!-- Router Outlet -->
|
|
771
|
+
<div id="app"></div>
|
|
772
|
+
|
|
773
|
+
<!-- Zenith Primitives Runtime -->
|
|
774
|
+
<script>
|
|
775
|
+
${generateZenPrimitivesRuntime()}
|
|
776
|
+
</script>
|
|
777
|
+
|
|
778
|
+
<!-- Zenith Runtime Router -->
|
|
779
|
+
<script>
|
|
780
|
+
${generateRuntimeRouterCode()}
|
|
781
|
+
</script>
|
|
782
|
+
|
|
783
|
+
<!-- Route Manifest & Page Modules -->
|
|
784
|
+
<script>
|
|
785
|
+
(function() {
|
|
786
|
+
// Route manifest (sorted by score, highest first)
|
|
787
|
+
const manifest = ${manifestCode};
|
|
788
|
+
|
|
789
|
+
// Page modules keyed by route path
|
|
790
|
+
const modules = {
|
|
791
|
+
${modulesJS}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// Initialize router when DOM is ready
|
|
795
|
+
if (document.readyState === 'loading') {
|
|
796
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
797
|
+
window.__zenith_router.initRouter(manifest, modules, '#app');
|
|
798
|
+
});
|
|
799
|
+
} else {
|
|
800
|
+
window.__zenith_router.initRouter(manifest, modules, '#app');
|
|
801
|
+
}
|
|
802
|
+
})();
|
|
803
|
+
</script>
|
|
804
|
+
</body>
|
|
805
|
+
</html>`;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Build SPA from pages directory
|
|
809
|
+
*/
|
|
810
|
+
export async function buildSPA(options) {
|
|
811
|
+
const { pagesDir, outDir, baseDir } = options;
|
|
812
|
+
// Clean output directory
|
|
813
|
+
if (fs.existsSync(outDir)) {
|
|
814
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
815
|
+
}
|
|
816
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
817
|
+
// Discover all pages
|
|
818
|
+
const pageFiles = discoverPages(pagesDir);
|
|
819
|
+
if (pageFiles.length === 0) {
|
|
820
|
+
console.warn("[Zenith Build] No pages found in", pagesDir);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
console.log(`[Zenith Build] Found ${pageFiles.length} page(s)`);
|
|
824
|
+
// Compile all pages
|
|
825
|
+
const compiledPages = [];
|
|
826
|
+
const layoutStyles = [];
|
|
827
|
+
for (const pageFile of pageFiles) {
|
|
828
|
+
console.log(`[Zenith Build] Compiling: ${path.relative(pagesDir, pageFile)}`);
|
|
829
|
+
try {
|
|
830
|
+
const compiled = await compilePage(pageFile, pagesDir);
|
|
831
|
+
compiledPages.push(compiled);
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
console.error(`[Zenith Build] Error compiling ${pageFile}:`, error);
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Sort pages by score (highest first)
|
|
839
|
+
compiledPages.sort((a, b) => b.score - a.score);
|
|
840
|
+
// Extract layout styles (they should be global)
|
|
841
|
+
// For now, we'll include any styles from the first page that uses a layout
|
|
842
|
+
// TODO: Better layout handling
|
|
843
|
+
// Generate HTML shell
|
|
844
|
+
const htmlShell = generateHTMLShell(compiledPages, layoutStyles);
|
|
845
|
+
// Write index.html
|
|
846
|
+
fs.writeFileSync(path.join(outDir, "index.html"), htmlShell);
|
|
847
|
+
// Copy favicon if it exists
|
|
848
|
+
const faviconPath = path.join(path.dirname(pagesDir), "favicon.ico");
|
|
849
|
+
if (fs.existsSync(faviconPath)) {
|
|
850
|
+
fs.copyFileSync(faviconPath, path.join(outDir, "favicon.ico"));
|
|
851
|
+
}
|
|
852
|
+
console.log(`[Zenith Build] Successfully built ${compiledPages.length} page(s)`);
|
|
853
|
+
console.log(`[Zenith Build] Output: ${outDir}/index.html`);
|
|
854
|
+
// Log route manifest
|
|
855
|
+
console.log("\n[Zenith Build] Route Manifest:");
|
|
856
|
+
for (const page of compiledPages) {
|
|
857
|
+
console.log(` ${page.routePath.padEnd(25)} → ${path.relative(pagesDir, page.filePath)} (score: ${page.score})`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Watch mode for development (future)
|
|
862
|
+
*/
|
|
863
|
+
export function watchSPA(_options) {
|
|
864
|
+
// TODO: Implement file watching
|
|
865
|
+
console.log("[Zenith Build] Watch mode not yet implemented");
|
|
866
|
+
}
|