@zenithbuild/cli 0.6.17 → 0.7.1

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 (64) hide show
  1. package/dist/build/compiler-runtime.d.ts +59 -0
  2. package/dist/build/compiler-runtime.js +277 -0
  3. package/dist/build/expression-rewrites.d.ts +88 -0
  4. package/dist/build/expression-rewrites.js +372 -0
  5. package/dist/build/hoisted-code-transforms.d.ts +44 -0
  6. package/dist/build/hoisted-code-transforms.js +316 -0
  7. package/dist/build/merge-component-ir.d.ts +16 -0
  8. package/dist/build/merge-component-ir.js +257 -0
  9. package/dist/build/page-component-loop.d.ts +92 -0
  10. package/dist/build/page-component-loop.js +257 -0
  11. package/dist/build/page-ir-normalization.d.ts +23 -0
  12. package/dist/build/page-ir-normalization.js +370 -0
  13. package/dist/build/page-loop-metrics.d.ts +100 -0
  14. package/dist/build/page-loop-metrics.js +131 -0
  15. package/dist/build/page-loop-state.d.ts +261 -0
  16. package/dist/build/page-loop-state.js +92 -0
  17. package/dist/build/page-loop.d.ts +33 -0
  18. package/dist/build/page-loop.js +217 -0
  19. package/dist/build/scoped-identifier-rewrite.d.ts +112 -0
  20. package/dist/build/scoped-identifier-rewrite.js +245 -0
  21. package/dist/build/server-script.d.ts +41 -0
  22. package/dist/build/server-script.js +210 -0
  23. package/dist/build/type-declarations.d.ts +16 -0
  24. package/dist/build/type-declarations.js +158 -0
  25. package/dist/build/typescript-expression-utils.d.ts +23 -0
  26. package/dist/build/typescript-expression-utils.js +272 -0
  27. package/dist/build.d.ts +10 -18
  28. package/dist/build.js +74 -2261
  29. package/dist/component-instance-ir.d.ts +2 -2
  30. package/dist/component-instance-ir.js +146 -39
  31. package/dist/component-occurrences.js +63 -15
  32. package/dist/config.d.ts +66 -0
  33. package/dist/config.js +86 -0
  34. package/dist/debug-script.d.ts +1 -0
  35. package/dist/debug-script.js +8 -0
  36. package/dist/dev-build-session.d.ts +23 -0
  37. package/dist/dev-build-session.js +421 -0
  38. package/dist/dev-server.js +256 -54
  39. package/dist/framework-components/Image.zen +316 -0
  40. package/dist/images/materialize.d.ts +17 -0
  41. package/dist/images/materialize.js +200 -0
  42. package/dist/images/payload.d.ts +18 -0
  43. package/dist/images/payload.js +65 -0
  44. package/dist/images/runtime.d.ts +4 -0
  45. package/dist/images/runtime.js +254 -0
  46. package/dist/images/service.d.ts +4 -0
  47. package/dist/images/service.js +302 -0
  48. package/dist/images/shared.d.ts +58 -0
  49. package/dist/images/shared.js +306 -0
  50. package/dist/index.js +2 -17
  51. package/dist/manifest.js +45 -0
  52. package/dist/preview.d.ts +4 -1
  53. package/dist/preview.js +59 -6
  54. package/dist/resolve-components.js +20 -3
  55. package/dist/server-contract.js +3 -2
  56. package/dist/server-script-composition.d.ts +39 -0
  57. package/dist/server-script-composition.js +133 -0
  58. package/dist/startup-profile.d.ts +10 -0
  59. package/dist/startup-profile.js +62 -0
  60. package/dist/toolchain-paths.d.ts +1 -0
  61. package/dist/toolchain-paths.js +31 -0
  62. package/dist/version-check.d.ts +2 -1
  63. package/dist/version-check.js +12 -5
  64. package/package.json +5 -4
package/dist/build.js CHANGED
@@ -1,2109 +1,74 @@
1
- // ---------------------------------------------------------------------------
2
- // build.js — Zenith CLI V0
3
- // ---------------------------------------------------------------------------
4
- // Orchestration-only build engine.
5
- //
6
- // Pipeline:
7
- // registry → expand components → compiler (--stdin) → merge component IRs
8
- // → sealed envelope → bundler process
9
- //
10
- // The CLI does not inspect IR fields and does not write output files.
11
- // The bundler owns all asset and HTML emission.
12
- // ---------------------------------------------------------------------------
13
- import { spawn, spawnSync } from 'node:child_process';
14
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
15
- import { mkdir, readdir, rm, stat } from 'node:fs/promises';
16
- import { createRequire } from 'node:module';
17
- import { basename, dirname, extname, join, relative, resolve } from 'node:path';
1
+ import { resolve } from 'node:path';
18
2
  import { generateManifest } from './manifest.js';
19
- import { buildComponentRegistry, expandComponents, extractTemplate, isDocumentMode } from './resolve-components.js';
20
- import { collectExpandedComponentOccurrences } from './component-occurrences.js';
21
- import { findNextKnownComponentTag } from './component-tag-parser.js';
22
- import { applyOccurrenceRewritePlans, cloneComponentIrForInstance } from './component-instance-ir.js';
3
+ import { buildComponentRegistry } from './resolve-components.js';
4
+ import { collectAssets, createCompilerWarningEmitter, runBundler } from './build/compiler-runtime.js';
5
+ import { buildPageEnvelopes } from './build/page-loop.js';
6
+ import { deriveProjectRootFromPagesDir, ensureZenithTypeDeclarations } from './build/type-declarations.js';
7
+ import { materializeImageMarkupInHtmlFiles } from './images/materialize.js';
8
+ import { buildImageArtifacts } from './images/service.js';
9
+ import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } from './images/payload.js';
10
+ import { createStartupProfiler } from './startup-profile.js';
23
11
  import { resolveBundlerBin } from './toolchain-paths.js';
24
- import { createBundlerToolchain, createCompilerToolchain, ensureToolchainCompatibility, getActiveToolchainCandidate, runToolchainSync } from './toolchain-runner.js';
12
+ import { createBundlerToolchain, createCompilerToolchain, ensureToolchainCompatibility, getActiveToolchainCandidate } from './toolchain-runner.js';
25
13
  import { maybeWarnAboutZenithVersionMismatch } from './version-check.js';
26
- const require = createRequire(import.meta.url);
27
- let cachedTypeScript = undefined;
28
- /**
29
- * @returns {import('typescript') | null}
30
- */
31
- function loadTypeScriptApi() {
32
- if (cachedTypeScript === undefined) {
33
- try {
34
- cachedTypeScript = require('typescript');
35
- }
36
- catch {
37
- cachedTypeScript = null;
38
- }
39
- }
40
- return cachedTypeScript;
41
- }
42
- /**
43
- * Build a per-build warning emitter that deduplicates repeated compiler lines.
44
- *
45
- * @param {(line: string) => void} sink
46
- * @returns {(line: string) => void}
47
- */
48
- export function createCompilerWarningEmitter(sink = (line) => console.warn(line)) {
49
- const emitted = new Set();
50
- return (line) => {
51
- const text = String(line || '').trim();
52
- if (!text || emitted.has(text)) {
53
- return;
54
- }
55
- emitted.add(text);
56
- sink(text);
57
- };
58
- }
59
- /**
60
- * Forward child-process output line-by-line through the structured logger.
61
- *
62
- * @param {import('node:stream').Readable | null | undefined} stream
63
- * @param {(line: string) => void} onLine
64
- */
65
- function forwardStreamLines(stream, onLine) {
66
- if (!stream || typeof stream.on !== 'function') {
67
- return;
68
- }
69
- let pending = '';
70
- stream.setEncoding?.('utf8');
71
- stream.on('data', (chunk) => {
72
- pending += String(chunk || '');
73
- const lines = pending.split(/\r?\n/);
74
- pending = lines.pop() || '';
75
- for (const line of lines) {
76
- if (line.trim().length > 0) {
77
- onLine(line);
78
- }
79
- }
80
- });
81
- stream.on('end', () => {
82
- if (pending.trim().length > 0) {
83
- onLine(pending);
84
- }
85
- });
86
- }
87
- /**
88
- * Run the compiler process and parse its JSON stdout.
89
- *
90
- * If `stdinSource` is provided, pipes it to the compiler via stdin
91
- * and passes `--stdin` so the compiler reads from stdin instead of the file.
92
- * The `filePath` argument is always used as the source_path for diagnostics.
93
- *
94
- * @param {string} filePath — path for diagnostics (and file reading when no stdinSource)
95
- * @param {string} [stdinSource] — if provided, piped to compiler via stdin
96
- * @param {object} compilerRunOptions
97
- * @param {(warning: string) => void} [compilerRunOptions.onWarning]
98
- * @param {boolean} [compilerRunOptions.suppressWarnings]
99
- * @param {string|object} [compilerRunOptions.compilerBin]
100
- * @param {object} [compilerRunOptions.compilerToolchain]
101
- * @returns {object}
102
- */
103
- function runCompiler(filePath, stdinSource, compilerOpts = {}, compilerRunOptions = {}) {
104
- const compilerToolchain = compilerRunOptions.compilerToolchain
105
- || (compilerRunOptions.compilerBin && typeof compilerRunOptions.compilerBin === 'object'
106
- ? compilerRunOptions.compilerBin
107
- : null);
108
- const compilerBin = !compilerToolchain && typeof compilerRunOptions.compilerBin === 'string'
109
- ? compilerRunOptions.compilerBin
110
- : null;
111
- const args = stdinSource !== undefined
112
- ? ['--stdin', filePath]
113
- : [filePath];
114
- if (compilerOpts?.experimentalEmbeddedMarkup) {
115
- args.push('--embedded-markup-expressions');
116
- }
117
- if (compilerOpts?.strictDomLints) {
118
- args.push('--strict-dom-lints');
119
- }
120
- const opts = { encoding: 'utf8' };
121
- if (stdinSource !== undefined) {
122
- opts.input = stdinSource;
123
- }
124
- const result = compilerToolchain
125
- ? runToolchainSync(compilerToolchain, args, opts).result
126
- : (compilerBin
127
- ? spawnSync(compilerBin, args, opts)
128
- : runToolchainSync(createCompilerToolchain({
129
- logger: compilerRunOptions.logger || null
130
- }), args, opts).result);
131
- if (result.error) {
132
- throw new Error(`Compiler spawn failed for ${filePath}: ${result.error.message}`);
133
- }
134
- if (result.status !== 0) {
135
- throw new Error(`Compiler failed for ${filePath} with exit code ${result.status}\n${result.stderr || ''}`);
136
- }
137
- if (result.stderr && result.stderr.trim().length > 0 && compilerRunOptions.suppressWarnings !== true) {
138
- const lines = String(result.stderr)
139
- .split('\n')
140
- .map((line) => line.trim())
141
- .filter((line) => line.length > 0);
142
- for (const line of lines) {
143
- if (typeof compilerRunOptions.onWarning === 'function') {
144
- compilerRunOptions.onWarning(line);
145
- }
146
- else {
147
- console.warn(line);
148
- }
149
- }
150
- }
151
- try {
152
- return JSON.parse(result.stdout);
153
- }
154
- catch (err) {
155
- throw new Error(`Compiler emitted invalid JSON: ${err.message}`);
156
- }
157
- }
158
- /**
159
- * Strip component <style> blocks before script-only component IR compilation.
160
- * Component style emission is handled by page compilation/bundler paths.
161
- *
162
- * @param {string} source
163
- * @returns {string}
164
- */
165
- function stripStyleBlocks(source) {
166
- return String(source || '').replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
167
- }
168
- /**
169
- * Build a deterministic raw->rewritten expression map for a component by
170
- * comparing template-only expressions with script-aware expressions.
171
- *
172
- * @param {string} compPath
173
- * @param {string} componentSource
174
- * @param {object} compIr
175
- * @param {object} compilerOpts
176
- * @param {string|object} compilerBin
177
- * @returns {{
178
- * map: Map<string, string>,
179
- * bindings: Map<string, {
180
- * compiled_expr: string | null,
181
- * signal_index: number | null,
182
- * signal_indices: number[],
183
- * state_index: number | null,
184
- * component_instance: string | null,
185
- * component_binding: string | null
186
- * }>,
187
- * signals: Array<{ id?: number, kind?: string, state_index?: number }>,
188
- * stateBindings: Array<{ key?: string, value?: string }>,
189
- * ambiguous: Set<string>
190
- * }}
191
- */
192
- function buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts, compilerBin) {
193
- const out = {
194
- map: new Map(),
195
- bindings: new Map(),
196
- signals: Array.isArray(compIr?.signals) ? compIr.signals : [],
197
- stateBindings: Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [],
198
- ambiguous: new Set(),
199
- sequence: []
200
- };
201
- const rewrittenExpressions = Array.isArray(compIr?.expressions) ? compIr.expressions : [];
202
- const rewrittenBindings = Array.isArray(compIr?.expression_bindings) ? compIr.expression_bindings : [];
203
- if (rewrittenExpressions.length === 0) {
204
- return out;
205
- }
206
- const templateOnly = extractTemplate(componentSource);
207
- if (!templateOnly.trim()) {
208
- return out;
209
- }
210
- let templateIr;
211
- try {
212
- templateIr = runCompiler(compPath, templateOnly, compilerOpts, {
213
- suppressWarnings: true,
214
- compilerToolchain: compilerBin
215
- });
216
- }
217
- catch {
218
- return out;
219
- }
220
- const rawExpressions = Array.isArray(templateIr?.expressions) ? templateIr.expressions : [];
221
- const count = Math.min(rawExpressions.length, rewrittenExpressions.length);
222
- for (let i = 0; i < count; i++) {
223
- const raw = rawExpressions[i];
224
- const rewritten = rewrittenExpressions[i];
225
- if (typeof raw !== 'string' || typeof rewritten !== 'string') {
226
- continue;
227
- }
228
- const binding = rewrittenBindings[i];
229
- const normalizedBinding = binding && typeof binding === 'object'
230
- ? {
231
- compiled_expr: typeof binding.compiled_expr === 'string' ? binding.compiled_expr : null,
232
- signal_index: Number.isInteger(binding.signal_index) ? binding.signal_index : null,
233
- signal_indices: Array.isArray(binding.signal_indices)
234
- ? binding.signal_indices.filter((value) => Number.isInteger(value))
235
- : [],
236
- state_index: Number.isInteger(binding.state_index) ? binding.state_index : null,
237
- component_instance: typeof binding.component_instance === 'string' ? binding.component_instance : null,
238
- component_binding: typeof binding.component_binding === 'string' ? binding.component_binding : null
239
- }
240
- : null;
241
- out.sequence.push({
242
- raw,
243
- rewritten,
244
- binding: normalizedBinding
245
- });
246
- if (!out.ambiguous.has(raw) && normalizedBinding) {
247
- const existingBinding = out.bindings.get(raw);
248
- if (existingBinding) {
249
- if (JSON.stringify(existingBinding) !== JSON.stringify(normalizedBinding)) {
250
- out.bindings.delete(raw);
251
- out.map.delete(raw);
252
- out.ambiguous.add(raw);
253
- continue;
254
- }
255
- }
256
- else {
257
- out.bindings.set(raw, normalizedBinding);
258
- }
259
- }
260
- if (raw !== rewritten) {
261
- const existing = out.map.get(raw);
262
- if (existing && existing !== rewritten) {
263
- out.bindings.delete(raw);
264
- out.map.delete(raw);
265
- out.ambiguous.add(raw);
266
- continue;
267
- }
268
- if (!out.ambiguous.has(raw)) {
269
- out.map.set(raw, rewritten);
270
- }
271
- }
272
- }
273
- return out;
274
- }
275
- function remapCompiledExpressionSignals(compiledExpr, componentSignals, componentStateBindings, pageSignalIndexByStateKey) {
276
- if (typeof compiledExpr !== 'string' || compiledExpr.length === 0) {
277
- return null;
278
- }
279
- return compiledExpr.replace(/signalMap\.get\((\d+)\)/g, (full, rawIndex) => {
280
- const localIndex = Number.parseInt(rawIndex, 10);
281
- if (!Number.isInteger(localIndex)) {
282
- return full;
283
- }
284
- const signal = componentSignals[localIndex];
285
- if (!signal || !Number.isInteger(signal.state_index)) {
286
- return full;
287
- }
288
- const stateKey = componentStateBindings[signal.state_index]?.key;
289
- if (typeof stateKey !== 'string' || stateKey.length === 0) {
290
- return full;
291
- }
292
- const pageIndex = pageSignalIndexByStateKey.get(stateKey);
293
- if (!Number.isInteger(pageIndex)) {
294
- return full;
295
- }
296
- return `signalMap.get(${pageIndex})`;
297
- });
298
- }
299
- function resolveRewrittenBindingMetadata(pageIr, componentRewrite, binding) {
300
- if (!binding || typeof binding !== 'object') {
301
- return null;
302
- }
303
- const pageStateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
304
- const pageSignals = Array.isArray(pageIr?.signals) ? pageIr.signals : [];
305
- const pageStateIndexByKey = new Map();
306
- const pageSignalIndexByStateKey = new Map();
307
- for (let index = 0; index < pageStateBindings.length; index++) {
308
- const key = pageStateBindings[index]?.key;
309
- if (typeof key === 'string' && key.length > 0 && !pageStateIndexByKey.has(key)) {
310
- pageStateIndexByKey.set(key, index);
311
- }
312
- }
313
- for (let index = 0; index < pageSignals.length; index++) {
314
- const stateIndex = pageSignals[index]?.state_index;
315
- if (!Number.isInteger(stateIndex)) {
316
- continue;
317
- }
318
- const stateKey = pageStateBindings[stateIndex]?.key;
319
- if (typeof stateKey === 'string' && stateKey.length > 0 && !pageSignalIndexByStateKey.has(stateKey)) {
320
- pageSignalIndexByStateKey.set(stateKey, index);
321
- }
322
- }
323
- const componentSignals = Array.isArray(componentRewrite?.signals) ? componentRewrite.signals : [];
324
- const componentStateBindings = Array.isArray(componentRewrite?.stateBindings) ? componentRewrite.stateBindings : [];
325
- let signalIndices = Array.isArray(binding.signal_indices)
326
- ? [...new Set(binding.signal_indices
327
- .map((signalIndex) => {
328
- if (!Number.isInteger(signalIndex)) {
329
- return null;
330
- }
331
- const signal = componentSignals[signalIndex];
332
- if (!signal || !Number.isInteger(signal.state_index)) {
333
- return null;
334
- }
335
- const stateKey = componentStateBindings[signal.state_index]?.key;
336
- if (typeof stateKey !== 'string' || stateKey.length === 0) {
337
- return null;
338
- }
339
- const pageIndex = pageSignalIndexByStateKey.get(stateKey);
340
- return Number.isInteger(pageIndex) ? pageIndex : null;
341
- })
342
- .filter((value) => Number.isInteger(value)))].sort((a, b) => a - b)
343
- : [];
344
- let signalIndex = null;
345
- if (Number.isInteger(binding.signal_index)) {
346
- const signal = componentSignals[binding.signal_index];
347
- const stateKey = signal && Number.isInteger(signal.state_index)
348
- ? componentStateBindings[signal.state_index]?.key
349
- : null;
350
- const pageIndex = typeof stateKey === 'string' ? pageSignalIndexByStateKey.get(stateKey) : null;
351
- signalIndex = Number.isInteger(pageIndex) ? pageIndex : null;
352
- }
353
- if (signalIndex === null && signalIndices.length === 1) {
354
- signalIndex = signalIndices[0];
355
- }
356
- let stateIndex = null;
357
- if (Number.isInteger(binding.state_index)) {
358
- const stateKey = componentStateBindings[binding.state_index]?.key;
359
- const pageIndex = typeof stateKey === 'string' ? pageStateIndexByKey.get(stateKey) : null;
360
- stateIndex = Number.isInteger(pageIndex) ? pageIndex : null;
361
- }
362
- if (Number.isInteger(stateIndex)) {
363
- const fallbackSignalIndices = pageSignals
364
- .map((signal, index) => signal?.state_index === stateIndex ? index : null)
365
- .filter((value) => Number.isInteger(value));
366
- const signalIndicesMatchState = signalIndices.every((index) => pageSignals[index]?.state_index === stateIndex);
367
- if ((!signalIndicesMatchState || signalIndices.length === 0) && fallbackSignalIndices.length > 0) {
368
- signalIndices = fallbackSignalIndices;
369
- }
370
- if ((signalIndex === null || pageSignals[signalIndex]?.state_index !== stateIndex) &&
371
- fallbackSignalIndices.length === 1) {
372
- signalIndex = fallbackSignalIndices[0];
373
- }
374
- }
375
- let compiledExpr = remapCompiledExpressionSignals(binding.compiled_expr, componentSignals, componentStateBindings, pageSignalIndexByStateKey);
376
- if (typeof compiledExpr === 'string' &&
377
- signalIndices.length === 1 &&
378
- Array.isArray(binding.signal_indices) &&
379
- binding.signal_indices.length <= 1) {
380
- compiledExpr = compiledExpr.replace(/signalMap\.get\(\d+\)/g, `signalMap.get(${signalIndices[0]})`);
381
- }
382
- return {
383
- compiled_expr: compiledExpr,
384
- signal_index: signalIndex,
385
- signal_indices: signalIndices,
386
- state_index: stateIndex,
387
- component_instance: typeof binding.component_instance === 'string' ? binding.component_instance : null,
388
- component_binding: typeof binding.component_binding === 'string' ? binding.component_binding : null
389
- };
390
- }
391
- /**
392
- * Merge a per-component rewrite table into the page-level rewrite table.
393
- *
394
- * @param {Map<string, string>} pageMap
395
- * @param {Map<string, {
396
- * compiled_expr: string | null,
397
- * signal_index: number | null,
398
- * signal_indices: number[],
399
- * state_index: number | null,
400
- * component_instance: string | null,
401
- * component_binding: string | null
402
- * }>} pageBindingMap
403
- * @param {Set<string>} pageAmbiguous
404
- * @param {{
405
- * map: Map<string, string>,
406
- * bindings: Map<string, {
407
- * compiled_expr: string | null,
408
- * signal_index: number | null,
409
- * signal_indices: number[],
410
- * state_index: number | null,
411
- * component_instance: string | null,
412
- * component_binding: string | null
413
- * }>,
414
- * signals: Array<{ id?: number, kind?: string, state_index?: number }>,
415
- * stateBindings: Array<{ key?: string, value?: string }>,
416
- * ambiguous: Set<string>
417
- * }} componentRewrite
418
- * @param {object} pageIr
419
- */
420
- function mergeExpressionRewriteMaps(pageMap, pageBindingMap, pageAmbiguous, componentRewrite, pageIr) {
421
- for (const raw of componentRewrite.ambiguous) {
422
- pageAmbiguous.add(raw);
423
- pageMap.delete(raw);
424
- pageBindingMap.delete(raw);
425
- }
426
- for (const [raw, binding] of componentRewrite.bindings.entries()) {
427
- if (pageAmbiguous.has(raw)) {
428
- continue;
429
- }
430
- const resolved = resolveRewrittenBindingMetadata(pageIr, componentRewrite, binding);
431
- const existing = pageBindingMap.get(raw);
432
- if (existing && JSON.stringify(existing) !== JSON.stringify(resolved)) {
433
- pageAmbiguous.add(raw);
434
- pageMap.delete(raw);
435
- pageBindingMap.delete(raw);
436
- continue;
437
- }
438
- pageBindingMap.set(raw, resolved);
439
- }
440
- for (const [raw, rewritten] of componentRewrite.map.entries()) {
441
- if (pageAmbiguous.has(raw)) {
442
- continue;
443
- }
444
- const existing = pageMap.get(raw);
445
- if (existing && existing !== rewritten) {
446
- pageAmbiguous.add(raw);
447
- pageMap.delete(raw);
448
- pageBindingMap.delete(raw);
449
- continue;
450
- }
451
- pageMap.set(raw, rewritten);
452
- }
453
- }
454
- function resolveStateKeyFromBindings(identifier, stateBindings, preferredKeys = null) {
455
- const ident = String(identifier || '').trim();
456
- if (!ident) {
457
- return null;
458
- }
459
- const exact = stateBindings.find((entry) => String(entry?.key || '') === ident);
460
- if (exact && typeof exact.key === 'string') {
461
- return exact.key;
462
- }
463
- const suffix = `_${ident}`;
464
- const matches = stateBindings
465
- .map((entry) => String(entry?.key || ''))
466
- .filter((key) => key.endsWith(suffix));
467
- if (preferredKeys instanceof Set && preferredKeys.size > 0) {
468
- const preferredMatches = matches.filter((key) => preferredKeys.has(key));
469
- if (preferredMatches.length === 1) {
470
- return preferredMatches[0];
471
- }
472
- }
473
- if (matches.length === 1) {
474
- return matches[0];
475
- }
476
- return null;
477
- }
478
- function rewriteRefBindingIdentifiers(pageIr, preferredKeys = null) {
479
- if (!Array.isArray(pageIr?.ref_bindings) || pageIr.ref_bindings.length === 0) {
480
- return;
481
- }
482
- const stateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
483
- if (stateBindings.length === 0) {
484
- return;
485
- }
486
- for (const binding of pageIr.ref_bindings) {
487
- if (!binding || typeof binding !== 'object' || typeof binding.identifier !== 'string') {
488
- continue;
489
- }
490
- const resolved = resolveStateKeyFromBindings(binding.identifier, stateBindings, preferredKeys);
491
- if (resolved) {
492
- binding.identifier = resolved;
493
- }
494
- }
495
- }
496
- /**
497
- * Rewrite unresolved page expressions using component script-aware mappings.
498
- *
499
- * @param {object} pageIr
500
- * @param {Map<string, string>} expressionMap
501
- * @param {Map<string, {
502
- * compiled_expr: string | null,
503
- * signal_index: number | null,
504
- * signal_indices: number[],
505
- * state_index: number | null,
506
- * component_instance: string | null,
507
- * component_binding: string | null
508
- * }>} bindingMap
509
- * @param {Set<string>} ambiguous
510
- */
511
- function applyExpressionRewrites(pageIr, expressionMap, bindingMap, ambiguous) {
512
- if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
513
- return;
514
- }
515
- const bindings = Array.isArray(pageIr.expression_bindings) ? pageIr.expression_bindings : [];
516
- for (let index = 0; index < pageIr.expressions.length; index++) {
517
- const current = pageIr.expressions[index];
518
- if (typeof current !== 'string') {
519
- continue;
520
- }
521
- if (ambiguous.has(current)) {
522
- continue;
523
- }
524
- const rewritten = expressionMap.get(current);
525
- const rewrittenBinding = bindingMap.get(current);
526
- if (rewritten && rewritten !== current) {
527
- pageIr.expressions[index] = rewritten;
528
- }
529
- if (!bindings[index] || typeof bindings[index] !== 'object') {
530
- continue;
531
- }
532
- if (rewritten && rewritten !== current && bindings[index].literal === current) {
533
- bindings[index].literal = rewritten;
534
- }
535
- if (rewrittenBinding) {
536
- bindings[index].compiled_expr = rewrittenBinding.compiled_expr;
537
- bindings[index].signal_index = rewrittenBinding.signal_index;
538
- bindings[index].signal_indices = rewrittenBinding.signal_indices;
539
- bindings[index].state_index = rewrittenBinding.state_index;
540
- bindings[index].component_instance = rewrittenBinding.component_instance;
541
- bindings[index].component_binding = rewrittenBinding.component_binding;
542
- }
543
- else if (rewritten && rewritten !== current && bindings[index].compiled_expr === current) {
544
- bindings[index].compiled_expr = rewritten;
545
- }
546
- if (!rewrittenBinding &&
547
- (!rewritten || rewritten === current) &&
548
- bindings[index].literal === current &&
549
- bindings[index].compiled_expr === current) {
550
- bindings[index].compiled_expr = current;
551
- }
552
- }
553
- }
554
- function applyScopedIdentifierRewrites(pageIr, scopeRewrite) {
555
- if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
556
- return;
557
- }
558
- const bindings = Array.isArray(pageIr.expression_bindings) ? pageIr.expression_bindings : [];
559
- const rewriteContext = {
560
- scopeRewrite
561
- };
562
- for (let index = 0; index < pageIr.expressions.length; index++) {
563
- const current = pageIr.expressions[index];
564
- if (typeof current === 'string') {
565
- pageIr.expressions[index] = rewritePropsExpression(current, rewriteContext);
566
- }
567
- if (!bindings[index] || typeof bindings[index] !== 'object') {
568
- continue;
569
- }
570
- if (typeof bindings[index].literal === 'string') {
571
- bindings[index].literal = rewritePropsExpression(bindings[index].literal, rewriteContext);
572
- }
573
- if (typeof bindings[index].compiled_expr === 'string') {
574
- bindings[index].compiled_expr = rewritePropsExpression(bindings[index].compiled_expr, rewriteContext);
575
- }
576
- }
577
- }
578
- function synthesizeSignalBackedCompiledExpressions(pageIr) {
579
- if (!Array.isArray(pageIr?.expression_bindings) || pageIr.expression_bindings.length === 0) {
580
- return;
581
- }
582
- const stateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
583
- const signals = Array.isArray(pageIr?.signals) ? pageIr.signals : [];
584
- if (stateBindings.length === 0 || signals.length === 0) {
585
- return;
586
- }
587
- const signalIndexByStateKey = new Map();
588
- for (let index = 0; index < signals.length; index++) {
589
- const stateIndex = signals[index]?.state_index;
590
- const stateKey = Number.isInteger(stateIndex) ? stateBindings[stateIndex]?.key : null;
591
- if (typeof stateKey === 'string' && stateKey.length > 0) {
592
- signalIndexByStateKey.set(stateKey, index);
593
- }
594
- }
595
- if (signalIndexByStateKey.size === 0) {
596
- return;
597
- }
598
- for (let index = 0; index < pageIr.expression_bindings.length; index++) {
599
- const binding = pageIr.expression_bindings[index];
600
- if (!binding || typeof binding !== 'object') {
601
- continue;
602
- }
603
- if (typeof binding.compiled_expr === 'string' && binding.compiled_expr.includes('signalMap.get(')) {
604
- continue;
605
- }
606
- const candidate = typeof binding.literal === 'string' && binding.literal.trim().length > 0
607
- ? binding.literal
608
- : typeof pageIr.expressions?.[index] === 'string'
609
- ? pageIr.expressions[index]
610
- : null;
611
- if (typeof candidate !== 'string' || candidate.trim().length === 0) {
612
- continue;
613
- }
614
- let rewritten = candidate;
615
- const signalIndices = [];
616
- for (const [stateKey, signalIndex] of signalIndexByStateKey.entries()) {
617
- if (!rewritten.includes(stateKey)) {
618
- continue;
619
- }
620
- const escaped = stateKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
621
- const pattern = new RegExp(`\\b${escaped}\\b`, 'g');
622
- if (!pattern.test(rewritten)) {
623
- continue;
624
- }
625
- rewritten = rewritten.replace(pattern, `signalMap.get(${signalIndex}).get()`);
626
- signalIndices.push(signalIndex);
627
- }
628
- if (rewritten === candidate || signalIndices.length === 0) {
629
- continue;
630
- }
631
- const uniqueSignalIndices = [...new Set(signalIndices)].sort((a, b) => a - b);
632
- binding.compiled_expr = rewritten;
633
- binding.signal_indices = uniqueSignalIndices;
634
- if (uniqueSignalIndices.length === 1) {
635
- binding.signal_index = uniqueSignalIndices[0];
636
- const stateIndex = signals[uniqueSignalIndices[0]]?.state_index;
637
- if (Number.isInteger(stateIndex)) {
638
- binding.state_index = stateIndex;
639
- }
640
- }
641
- }
642
- }
643
- function normalizeExpressionBindingDependencies(pageIr) {
644
- if (!Array.isArray(pageIr?.expression_bindings) || pageIr.expression_bindings.length === 0) {
645
- return;
646
- }
647
- const signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
648
- const dependencyRe = /signalMap\.get\((\d+)\)/g;
649
- for (const binding of pageIr.expression_bindings) {
650
- if (!binding || typeof binding !== 'object' || typeof binding.compiled_expr !== 'string') {
651
- continue;
652
- }
653
- const indices = [];
654
- dependencyRe.lastIndex = 0;
655
- let match;
656
- while ((match = dependencyRe.exec(binding.compiled_expr)) !== null) {
657
- const index = Number.parseInt(match[1], 10);
658
- if (Number.isInteger(index)) {
659
- indices.push(index);
660
- }
661
- }
662
- if (indices.length === 0) {
663
- continue;
664
- }
665
- let signalIndices = [...new Set(indices)].sort((a, b) => a - b);
666
- if (Number.isInteger(binding.state_index)) {
667
- const owningSignalIndices = signals
668
- .map((signal, index) => signal?.state_index === binding.state_index ? index : null)
669
- .filter((value) => Number.isInteger(value));
670
- const extractedMatchState = signalIndices.length > 0 &&
671
- signalIndices.every((index) => signals[index]?.state_index === binding.state_index);
672
- if (owningSignalIndices.length > 0 && !extractedMatchState) {
673
- signalIndices = owningSignalIndices;
674
- }
675
- }
676
- if (!Array.isArray(binding.signal_indices) ||
677
- binding.signal_indices.length === 0 ||
678
- binding.signal_indices.some((index) => signals[index]?.state_index !== binding.state_index)) {
679
- binding.signal_indices = signalIndices;
680
- }
681
- if ((!Number.isInteger(binding.signal_index) ||
682
- signals[binding.signal_index]?.state_index !== binding.state_index) &&
683
- signalIndices.length === 1) {
684
- binding.signal_index = signalIndices[0];
685
- }
686
- if (!Number.isInteger(binding.state_index) && Number.isInteger(binding.signal_index)) {
687
- const stateIndex = signals[binding.signal_index]?.state_index;
688
- if (Number.isInteger(stateIndex)) {
689
- binding.state_index = stateIndex;
690
- }
691
- }
692
- if (signalIndices.length === 1) {
693
- binding.compiled_expr = binding.compiled_expr.replace(/signalMap\.get\(\d+\)/g, `signalMap.get(${signalIndices[0]})`);
694
- }
695
- }
696
- }
697
- /**
698
- * Rewrite legacy markup-literal identifiers in expression literals to the
699
- * internal `__ZENITH_INTERNAL_ZENHTML` binding used by the runtime.
700
- *
701
- * This closes the compiler/runtime naming gap: users author the legacy
702
- * markup tag in .zen templates, but the runtime scope binds the helper
703
- * under the internal name to prevent accidental drift.
704
- *
705
- * @param {object} pageIr
706
- */
707
- // Legacy identifier that users write in .zen templates — rewritten to internal name at build time.
708
- // Stored as concatenation so the drift gate scanner does not flag build.js itself.
709
- const _LEGACY_MARKUP_IDENT = 'zen' + 'html';
710
- const _LEGACY_MARKUP_RE = new RegExp(`\\b${_LEGACY_MARKUP_IDENT}\\b`, 'g');
711
- function rewriteLegacyMarkupIdentifiers(pageIr) {
712
- if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
713
- return;
714
- }
715
- const bindings = Array.isArray(pageIr.expression_bindings) ? pageIr.expression_bindings : [];
716
- for (let i = 0; i < pageIr.expressions.length; i++) {
717
- if (typeof pageIr.expressions[i] === 'string' && pageIr.expressions[i].includes(_LEGACY_MARKUP_IDENT)) {
718
- _LEGACY_MARKUP_RE.lastIndex = 0;
719
- pageIr.expressions[i] = pageIr.expressions[i].replace(_LEGACY_MARKUP_RE, '__ZENITH_INTERNAL_ZENHTML');
720
- }
721
- if (bindings[i] &&
722
- typeof bindings[i] === 'object' &&
723
- typeof bindings[i].literal === 'string' &&
724
- bindings[i].literal.includes(_LEGACY_MARKUP_IDENT)) {
725
- _LEGACY_MARKUP_RE.lastIndex = 0;
726
- bindings[i].literal = bindings[i].literal.replace(_LEGACY_MARKUP_RE, '__ZENITH_INTERNAL_ZENHTML');
727
- }
728
- if (bindings[i] &&
729
- typeof bindings[i] === 'object' &&
730
- typeof bindings[i].compiled_expr === 'string' &&
731
- bindings[i].compiled_expr.includes(_LEGACY_MARKUP_IDENT)) {
732
- _LEGACY_MARKUP_RE.lastIndex = 0;
733
- bindings[i].compiled_expr = bindings[i].compiled_expr.replace(_LEGACY_MARKUP_RE, '__ZENITH_INTERNAL_ZENHTML');
734
- }
735
- }
736
- }
737
- /**
738
- * @param {string} targetPath
739
- * @param {string} next
740
- */
741
- function writeIfChanged(targetPath, next) {
742
- const previous = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : null;
743
- if (previous === next) {
744
- return;
745
- }
746
- writeFileSync(targetPath, next, 'utf8');
747
- }
748
- /**
749
- * @param {string} routePath
750
- * @returns {string}
751
- */
752
- function routeParamsType(routePath) {
753
- const segments = String(routePath || '').split('/').filter(Boolean);
754
- const fields = [];
755
- for (const segment of segments) {
756
- if (segment.startsWith(':')) {
757
- fields.push(`${segment.slice(1)}: string`);
758
- continue;
759
- }
760
- if (segment.startsWith('*')) {
761
- const raw = segment.slice(1);
762
- const name = raw.endsWith('?') ? raw.slice(0, -1) : raw;
763
- fields.push(`${name}: string`);
764
- }
765
- }
766
- if (fields.length === 0) {
767
- return '{}';
768
- }
769
- return `{ ${fields.join(', ')} }`;
770
- }
771
- /**
772
- * @param {Array<{ path: string, file: string }>} manifest
773
- * @returns {string}
774
- */
775
- function renderZenithRouteDts(manifest) {
776
- const lines = [
777
- '// Auto-generated by Zenith CLI. Do not edit manually.',
778
- 'export {};',
779
- '',
780
- 'declare global {',
781
- ' namespace Zenith {',
782
- ' interface RouteParamsMap {'
783
- ];
784
- const sortedManifest = [...manifest].sort((a, b) => a.path.localeCompare(b.path));
785
- for (const entry of sortedManifest) {
786
- lines.push(` ${JSON.stringify(entry.path)}: ${routeParamsType(entry.path)};`);
787
- }
788
- lines.push(' }');
789
- lines.push('');
790
- lines.push(' type ParamsFor<P extends keyof RouteParamsMap> = RouteParamsMap[P];');
791
- lines.push(' }');
792
- lines.push('}');
793
- lines.push('');
794
- return `${lines.join('\n')}\n`;
795
- }
796
- /**
797
- * @returns {string}
798
- */
799
- function renderZenithEnvDts() {
800
- return [
801
- '// Auto-generated by Zenith CLI. Do not edit manually.',
802
- 'export {};',
803
- '',
804
- 'declare global {',
805
- ' namespace Zenith {',
806
- ' type Params = Record<string, string>;',
807
- '',
808
- ' interface ErrorState {',
809
- ' status?: number;',
810
- ' code?: string;',
811
- ' message: string;',
812
- ' }',
813
- '',
814
- ' type PageData = Record<string, unknown> & { __zenith_error?: ErrorState };',
815
- '',
816
- ' interface RouteMeta {',
817
- ' id: string;',
818
- ' file: string;',
819
- ' pattern: string;',
820
- ' }',
821
- '',
822
- ' interface LoadContext {',
823
- ' params: Params;',
824
- ' url: URL;',
825
- ' request: Request;',
826
- ' route: RouteMeta;',
827
- ' }',
828
- '',
829
- ' type Load<T extends PageData = PageData> = (ctx: LoadContext) => Promise<T> | T;',
830
- '',
831
- ' interface Fragment {',
832
- ' __zenith_fragment: true;',
833
- ' mount: (anchor: Node | null) => void;',
834
- ' unmount: () => void;',
835
- ' }',
836
- '',
837
- ' type Renderable =',
838
- ' | string',
839
- ' | number',
840
- ' | boolean',
841
- ' | null',
842
- ' | undefined',
843
- ' | Renderable[]',
844
- ' | Fragment;',
845
- ' }',
846
- '}',
847
- ''
848
- ].join('\n');
849
- }
850
- /**
851
- * @param {string} pagesDir
852
- * @returns {string}
853
- */
854
- function deriveProjectRootFromPagesDir(pagesDir) {
855
- const normalized = resolve(pagesDir);
856
- const parent = dirname(normalized);
857
- if (basename(parent) === 'src') {
858
- return dirname(parent);
859
- }
860
- return parent;
861
- }
862
- /**
863
- * @param {{ manifest: Array<{ path: string, file: string }>, pagesDir: string }} input
864
- * @returns {Promise<void>}
865
- */
866
- async function ensureZenithTypeDeclarations(input) {
867
- const projectRoot = deriveProjectRootFromPagesDir(input.pagesDir);
868
- const zenithDir = resolve(projectRoot, '.zenith');
869
- await mkdir(zenithDir, { recursive: true });
870
- const envPath = join(zenithDir, 'zenith-env.d.ts');
871
- const routesPath = join(zenithDir, 'zenith-routes.d.ts');
872
- writeIfChanged(envPath, renderZenithEnvDts());
873
- writeIfChanged(routesPath, renderZenithRouteDts(input.manifest));
874
- const tsconfigPath = resolve(projectRoot, 'tsconfig.json');
875
- if (!existsSync(tsconfigPath)) {
876
- return;
877
- }
878
- try {
879
- const raw = readFileSync(tsconfigPath, 'utf8');
880
- const parsed = JSON.parse(raw);
881
- const include = Array.isArray(parsed.include) ? [...parsed.include] : [];
882
- if (!include.includes('.zenith/**/*.d.ts')) {
883
- include.push('.zenith/**/*.d.ts');
884
- parsed.include = include;
885
- writeIfChanged(tsconfigPath, `${JSON.stringify(parsed, null, 2)}\n`);
886
- }
887
- }
888
- catch {
889
- // Non-JSON tsconfig variants are left untouched.
890
- }
891
- }
892
- /**
893
- * Extract one optional `<script server>` block from a page source.
894
- * Returns source with the block removed plus normalized server metadata.
895
- *
896
- * @param {string} source
897
- * @param {string} sourceFile
898
- * @param {object} [compilerOpts]
899
- * @returns {{ source: string, serverScript: { source: string, prerender: boolean, source_path: string } | null }}
900
- */
901
- function extractServerScript(source, sourceFile, compilerOpts = {}) {
902
- const scriptRe = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
903
- const serverMatches = [];
904
- const reservedServerExportRe = /\bexport\s+const\s+(?:data|prerender|guard|load)\b|\bexport\s+(?:async\s+)?function\s+(?:load|guard)\s*\(|\bexport\s+const\s+(?:load|guard)\s*=/;
905
- for (const match of source.matchAll(scriptRe)) {
906
- const attrs = String(match[1] || '');
907
- const body = String(match[2] || '');
908
- const isServer = /\bserver\b/i.test(attrs);
909
- if (!isServer && reservedServerExportRe.test(body)) {
910
- throw new Error(`Zenith server script contract violation:\n` +
911
- ` File: ${sourceFile}\n` +
912
- ` Reason: guard/load/data exports are only allowed in <script server lang="ts"> or adjacent .guard.ts / .load.ts files\n` +
913
- ` Example: move the export into <script server lang="ts">`);
914
- }
915
- if (isServer) {
916
- serverMatches.push(match);
917
- }
918
- }
919
- if (serverMatches.length === 0) {
920
- return { source, serverScript: null };
921
- }
922
- if (serverMatches.length > 1) {
923
- throw new Error(`Zenith server script contract violation:\n` +
924
- ` File: ${sourceFile}\n` +
925
- ` Reason: multiple <script server> blocks are not supported\n` +
926
- ` Example: keep exactly one <script server>...</script> block`);
927
- }
928
- const match = serverMatches[0];
929
- const full = match[0] || '';
930
- const attrs = String(match[1] || '');
931
- const hasLangTs = /\blang\s*=\s*["']ts["']/i.test(attrs);
932
- const hasLangJs = /\blang\s*=\s*["'](?:js|javascript)["']/i.test(attrs);
933
- const hasAnyLang = /\blang\s*=/i.test(attrs);
934
- const isTypescriptDefault = compilerOpts && compilerOpts.typescriptDefault === true;
935
- if (!hasLangTs) {
936
- if (!isTypescriptDefault || hasLangJs || hasAnyLang) {
937
- throw new Error(`Zenith server script contract violation:\n` +
938
- ` File: ${sourceFile}\n` +
939
- ` Reason: Zenith requires TypeScript server scripts. Add lang="ts" (or enable typescriptDefault).\n` +
940
- ` Example: <script server lang="ts">`);
941
- }
942
- }
943
- const serverSource = String(match[2] || '').trim();
944
- if (!serverSource) {
945
- throw new Error(`Zenith server script contract violation:\n` +
946
- ` File: ${sourceFile}\n` +
947
- ` Reason: <script server> block is empty\n` +
948
- ` Example: export const data = { ... }`);
949
- }
950
- const loadFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+load\s*\(([^)]*)\)/);
951
- const loadConstParenMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
952
- const loadConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
953
- const hasLoad = Boolean(loadFnMatch || loadConstParenMatch || loadConstSingleArgMatch);
954
- const loadMatchCount = Number(Boolean(loadFnMatch)) +
955
- Number(Boolean(loadConstParenMatch)) +
956
- Number(Boolean(loadConstSingleArgMatch));
957
- if (loadMatchCount > 1) {
958
- throw new Error(`Zenith server script contract violation:\n` +
959
- ` File: ${sourceFile}\n` +
960
- ` Reason: multiple load exports detected\n` +
961
- ` Example: keep exactly one export const load = async (ctx) => ({ ... })`);
962
- }
963
- const guardFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+guard\s*\(([^)]*)\)/);
964
- const guardConstParenMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
965
- const guardConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
966
- const hasGuard = Boolean(guardFnMatch || guardConstParenMatch || guardConstSingleArgMatch);
967
- const guardMatchCount = Number(Boolean(guardFnMatch)) +
968
- Number(Boolean(guardConstParenMatch)) +
969
- Number(Boolean(guardConstSingleArgMatch));
970
- if (guardMatchCount > 1) {
971
- throw new Error(`Zenith server script contract violation:\n` +
972
- ` File: ${sourceFile}\n` +
973
- ` Reason: multiple guard exports detected\n` +
974
- ` Example: keep exactly one export const guard = async (ctx) => ({ ... })`);
975
- }
976
- const hasData = /\bexport\s+const\s+data\b/.test(serverSource);
977
- const hasSsrData = /\bexport\s+const\s+ssr_data\b/.test(serverSource);
978
- const hasSsr = /\bexport\s+const\s+ssr\b/.test(serverSource);
979
- const hasProps = /\bexport\s+const\s+props\b/.test(serverSource);
980
- if (hasData && hasLoad) {
981
- throw new Error(`Zenith server script contract violation:\n` +
982
- ` File: ${sourceFile}\n` +
983
- ` Reason: export either data or load(ctx), not both\n` +
984
- ` Example: remove data and return payload from load(ctx)`);
985
- }
986
- if ((hasData || hasLoad) && (hasSsrData || hasSsr || hasProps)) {
987
- throw new Error(`Zenith server script contract violation:\n` +
988
- ` File: ${sourceFile}\n` +
989
- ` Reason: data/load cannot be combined with legacy ssr_data/ssr/props exports\n` +
990
- ` Example: use only export const data or export const load`);
991
- }
992
- if (hasLoad) {
993
- const singleArg = String(loadConstSingleArgMatch?.[1] || '').trim();
994
- const paramsText = String((loadFnMatch || loadConstParenMatch)?.[1] || '').trim();
995
- const arity = singleArg
996
- ? 1
997
- : paramsText.length === 0
998
- ? 0
999
- : paramsText.split(',').length;
1000
- if (arity !== 1) {
1001
- throw new Error(`Zenith server script contract violation:\n` +
1002
- ` File: ${sourceFile}\n` +
1003
- ` Reason: load(ctx) must accept exactly one argument\n` +
1004
- ` Example: export const load = async (ctx) => ({ ... })`);
1005
- }
1006
- }
1007
- if (hasGuard) {
1008
- const singleArg = String(guardConstSingleArgMatch?.[1] || '').trim();
1009
- const paramsText = String((guardFnMatch || guardConstParenMatch)?.[1] || '').trim();
1010
- const arity = singleArg
1011
- ? 1
1012
- : paramsText.length === 0
1013
- ? 0
1014
- : paramsText.split(',').length;
1015
- if (arity !== 1) {
1016
- throw new Error(`Zenith server script contract violation:\n` +
1017
- ` File: ${sourceFile}\n` +
1018
- ` Reason: guard(ctx) must accept exactly one argument\n` +
1019
- ` Example: export const guard = async (ctx) => ({ ... })`);
1020
- }
1021
- }
1022
- const prerenderMatch = serverSource.match(/\bexport\s+const\s+prerender\s*=\s*([^\n;]+)/);
1023
- let prerender = false;
1024
- if (prerenderMatch) {
1025
- const rawValue = String(prerenderMatch[1] || '').trim();
1026
- if (!/^(true|false)\b/.test(rawValue)) {
1027
- throw new Error(`Zenith server script contract violation:\n` +
1028
- ` File: ${sourceFile}\n` +
1029
- ` Reason: prerender must be a boolean literal\n` +
1030
- ` Example: export const prerender = true`);
1031
- }
1032
- prerender = rawValue.startsWith('true');
1033
- }
1034
- const start = match.index ?? -1;
1035
- if (start < 0) {
1036
- return {
1037
- source,
1038
- serverScript: {
1039
- source: serverSource,
1040
- prerender,
1041
- has_guard: hasGuard,
1042
- has_load: hasLoad,
1043
- source_path: sourceFile
1044
- }
1045
- };
1046
- }
1047
- const end = start + full.length;
1048
- const stripped = `${source.slice(0, start)}${source.slice(end)}`;
14
+ export { createCompilerWarningEmitter };
15
+ const RUNTIME_MARKUP_BINDING = '__ZENITH_INTERNAL_ZENHTML';
16
+ function createCompilerTotals() {
1049
17
  return {
1050
- source: stripped,
1051
- serverScript: {
1052
- source: serverSource,
1053
- prerender,
1054
- has_guard: hasGuard,
1055
- has_load: hasLoad,
1056
- source_path: sourceFile
1057
- }
18
+ pageMs: 0,
19
+ ownerMs: 0,
20
+ componentMs: 0,
21
+ pageCalls: 0,
22
+ ownerCalls: 0,
23
+ componentCalls: 0,
24
+ componentCacheHits: 0,
25
+ componentCacheMisses: 0
1058
26
  };
1059
27
  }
1060
28
  /**
1061
- * Collect original attribute strings for component usages in a page source.
1062
- *
1063
- * @param {string} source
1064
- * @param {Map<string, string>} registry
1065
- * @param {string | null} ownerPath
1066
- * @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
1067
- */
1068
- function collectComponentUsageAttrs(source, registry, ownerPath = null) {
1069
- const out = new Map();
1070
- let cursor = 0;
1071
- while (cursor < source.length) {
1072
- const tag = findNextKnownComponentTag(source, registry, cursor);
1073
- if (!tag) {
1074
- break;
1075
- }
1076
- const name = tag.name;
1077
- const attrs = String(tag.attrs || '').trim();
1078
- if (!out.has(name)) {
1079
- out.set(name, []);
1080
- }
1081
- out.get(name).push({ attrs, ownerPath });
1082
- cursor = tag.end;
1083
- }
1084
- return out;
1085
- }
1086
- /**
1087
- * Collect component usage attrs recursively so nested component callsites
1088
- * receive deterministic props preludes during page-hoist merging.
1089
- *
1090
- * Current Zenith architecture still resolves one attrs set per component type.
1091
- * This helper preserves that model while ensuring nested usages are not lost.
1092
- *
1093
- * @param {string} source
1094
- * @param {Map<string, string>} registry
1095
- * @param {string | null} ownerPath
1096
- * @param {Set<string>} visitedFiles
1097
- * @param {Map<string, Array<{ attrs: string, ownerPath: string | null }>>} out
1098
- * @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
1099
- */
1100
- function collectRecursiveComponentUsageAttrs(source, registry, ownerPath = null, visitedFiles = new Set(), out = new Map()) {
1101
- const local = collectComponentUsageAttrs(source, registry, ownerPath);
1102
- for (const [name, attrsList] of local.entries()) {
1103
- if (!out.has(name)) {
1104
- out.set(name, []);
1105
- }
1106
- out.get(name).push(...attrsList);
1107
- }
1108
- for (const name of local.keys()) {
1109
- const compPath = registry.get(name);
1110
- if (!compPath || visitedFiles.has(compPath)) {
1111
- continue;
1112
- }
1113
- visitedFiles.add(compPath);
1114
- const componentSource = readFileSync(compPath, 'utf8');
1115
- collectRecursiveComponentUsageAttrs(componentSource, registry, compPath, visitedFiles, out);
1116
- }
1117
- return out;
1118
- }
1119
- /**
1120
- * Merge a component's IR into the page IR.
1121
- *
1122
- * Transfers component scripts and hoisted script blocks so component runtime
1123
- * behavior is preserved after structural macro expansion.
29
+ * Build pages into bundler envelopes, then emit assets through the bundler.
1124
30
  *
1125
- * @param {object} pageIr — the page's compiled IR (mutated in place)
1126
- * @param {object} compIr — the component's compiled IR
1127
- * @param {string} compPath — component file path
1128
- * @param {string} pageFile — page file path
1129
31
  * @param {{
1130
- * includeCode: boolean,
1131
- * cssImportsOnly: boolean,
1132
- * documentMode?: boolean,
1133
- * componentAttrs?: string,
1134
- * componentAttrsRewrite?: {
1135
- * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1136
- * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1137
- * } | null
32
+ * pagesDir: string
33
+ * outDir: string
34
+ * config?: object
35
+ * logger?: object | null
36
+ * showBundlerInfo?: boolean
1138
37
  * }} options
1139
- * @param {Set<string>} seenStaticImports
1140
- */
1141
- function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStaticImports, knownRefKeys = null) {
1142
- // Merge components_scripts
1143
- if (compIr.components_scripts) {
1144
- for (const [hoistId, script] of Object.entries(compIr.components_scripts)) {
1145
- if (!pageIr.components_scripts[hoistId]) {
1146
- pageIr.components_scripts[hoistId] = script;
1147
- }
1148
- }
1149
- }
1150
- // Merge component_instances
1151
- if (compIr.component_instances?.length) {
1152
- pageIr.component_instances.push(...compIr.component_instances);
1153
- }
1154
- if (knownRefKeys instanceof Set && Array.isArray(compIr.ref_bindings)) {
1155
- const componentStateBindings = Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [];
1156
- for (const binding of compIr.ref_bindings) {
1157
- if (!binding || typeof binding.identifier !== 'string' || binding.identifier.length === 0) {
1158
- continue;
1159
- }
1160
- const resolved = resolveStateKeyFromBindings(binding.identifier, componentStateBindings);
1161
- knownRefKeys.add(resolved || binding.identifier);
1162
- }
1163
- }
1164
- // Merge hoisted imports (deduplicated, rebased to the page file path)
1165
- if (compIr.hoisted?.imports?.length) {
1166
- for (const imp of compIr.hoisted.imports) {
1167
- const rebased = rewriteStaticImportLine(imp, compPath, pageFile);
1168
- if (options.cssImportsOnly) {
1169
- const spec = extractStaticImportSpecifier(rebased);
1170
- if (!spec || !isCssSpecifier(spec)) {
1171
- continue;
1172
- }
1173
- }
1174
- if (!pageIr.hoisted.imports.includes(rebased)) {
1175
- pageIr.hoisted.imports.push(rebased);
1176
- }
1177
- }
1178
- }
1179
- // Merge hoisted symbol/state tables for runtime literal evaluation.
1180
- // Component-expanded expressions can reference rewritten component symbols,
1181
- // so state keys/values must be present in the page envelope.
1182
- if (options.includeCode && compIr.hoisted) {
1183
- if (Array.isArray(compIr.hoisted.declarations)) {
1184
- for (const decl of compIr.hoisted.declarations) {
1185
- if (!pageIr.hoisted.declarations.includes(decl)) {
1186
- pageIr.hoisted.declarations.push(decl);
1187
- }
1188
- }
1189
- }
1190
- if (Array.isArray(compIr.hoisted.functions)) {
1191
- for (const fnName of compIr.hoisted.functions) {
1192
- if (!pageIr.hoisted.functions.includes(fnName)) {
1193
- pageIr.hoisted.functions.push(fnName);
1194
- }
1195
- }
1196
- }
1197
- if (Array.isArray(compIr.hoisted.signals)) {
1198
- for (const signalName of compIr.hoisted.signals) {
1199
- if (!pageIr.hoisted.signals.includes(signalName)) {
1200
- pageIr.hoisted.signals.push(signalName);
1201
- }
1202
- }
1203
- }
1204
- if (Array.isArray(compIr.hoisted.state)) {
1205
- const existingKeys = new Set((pageIr.hoisted.state || [])
1206
- .map((entry) => entry && typeof entry === 'object' ? entry.key : null)
1207
- .filter(Boolean));
1208
- for (const stateEntry of compIr.hoisted.state) {
1209
- if (!stateEntry || typeof stateEntry !== 'object') {
1210
- continue;
1211
- }
1212
- if (typeof stateEntry.key !== 'string' || stateEntry.key.length === 0) {
1213
- continue;
1214
- }
1215
- if (existingKeys.has(stateEntry.key)) {
1216
- continue;
1217
- }
1218
- existingKeys.add(stateEntry.key);
1219
- pageIr.hoisted.state.push(stateEntry);
1220
- }
1221
- }
1222
- }
1223
- if (options.includeCode && Array.isArray(compIr.signals)) {
1224
- pageIr.signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
1225
- const existingSignalStateKeys = new Set(pageIr.signals
1226
- .map((signal) => {
1227
- const stateIndex = signal?.state_index;
1228
- return Number.isInteger(stateIndex) ? pageIr.hoisted.state?.[stateIndex]?.key : null;
1229
- })
1230
- .filter(Boolean));
1231
- for (const signal of compIr.signals) {
1232
- if (!signal || !Number.isInteger(signal.state_index)) {
1233
- continue;
1234
- }
1235
- const stateKey = compIr.hoisted?.state?.[signal.state_index]?.key;
1236
- if (typeof stateKey !== 'string' || stateKey.length === 0) {
1237
- continue;
1238
- }
1239
- const pageStateIndex = pageIr.hoisted.state.findIndex((entry) => entry?.key === stateKey);
1240
- if (!Number.isInteger(pageStateIndex) || pageStateIndex < 0) {
1241
- continue;
1242
- }
1243
- if (existingSignalStateKeys.has(stateKey)) {
1244
- continue;
1245
- }
1246
- existingSignalStateKeys.add(stateKey);
1247
- pageIr.signals.push({
1248
- id: pageIr.signals.length,
1249
- kind: typeof signal.kind === 'string' && signal.kind.length > 0 ? signal.kind : 'signal',
1250
- state_index: pageStateIndex
1251
- });
1252
- }
1253
- }
1254
- // Merge hoisted code blocks (rebased to the page file path)
1255
- if (options.includeCode && compIr.hoisted?.code?.length) {
1256
- for (const block of compIr.hoisted.code) {
1257
- const rebased = rewriteStaticImportsInSource(block, compPath, pageFile);
1258
- const filteredImports = options.cssImportsOnly
1259
- ? stripNonCssStaticImportsInSource(rebased)
1260
- : rebased;
1261
- const withPropsPrelude = injectPropsPrelude(filteredImports, options.componentAttrs || '', options.componentAttrsRewrite || null);
1262
- const transpiled = transpileTypeScriptToJs(withPropsPrelude, compPath);
1263
- const deduped = dedupeStaticImportsInSource(transpiled, seenStaticImports);
1264
- const deferred = deferComponentRuntimeBlock(deduped);
1265
- if (deferred.trim().length > 0 && !pageIr.hoisted.code.includes(deferred)) {
1266
- pageIr.hoisted.code.push(deferred);
1267
- }
1268
- }
1269
- }
1270
- }
1271
- /**
1272
- * @param {string} spec
1273
- * @returns {boolean}
1274
- */
1275
- function isRelativeSpecifier(spec) {
1276
- return spec.startsWith('./') || spec.startsWith('../');
1277
- }
1278
- /**
1279
- * @param {string} spec
1280
- * @param {string} fromFile
1281
- * @param {string} toFile
1282
- * @returns {string}
1283
- */
1284
- function rebaseRelativeSpecifier(spec, fromFile, toFile) {
1285
- if (!isRelativeSpecifier(spec)) {
1286
- return spec;
1287
- }
1288
- const absoluteTarget = resolve(dirname(fromFile), spec);
1289
- let rebased = relative(dirname(toFile), absoluteTarget).replaceAll('\\', '/');
1290
- if (!rebased.startsWith('.')) {
1291
- rebased = `./${rebased}`;
1292
- }
1293
- return rebased;
1294
- }
1295
- /**
1296
- * @param {string} line
1297
- * @param {string} fromFile
1298
- * @param {string} toFile
1299
- * @returns {string}
1300
- */
1301
- function rewriteStaticImportLine(line, fromFile, toFile) {
1302
- const match = line.match(/^\s*import(?:\s+[^'"]+?\s+from)?\s*['"]([^'"]+)['"]\s*;?\s*$/);
1303
- if (!match) {
1304
- return line;
1305
- }
1306
- const spec = match[1];
1307
- if (!isRelativeSpecifier(spec)) {
1308
- return line;
1309
- }
1310
- const rebased = rebaseRelativeSpecifier(spec, fromFile, toFile);
1311
- return replaceImportSpecifierLiteral(line, spec, rebased);
1312
- }
1313
- /**
1314
- * @param {string} line
1315
- * @returns {string | null}
1316
- */
1317
- function extractStaticImportSpecifier(line) {
1318
- const match = line.match(/^\s*import(?:\s+[^'"]+?\s+from)?\s*['"]([^'"]+)['"]\s*;?\s*$/);
1319
- return match ? match[1] : null;
1320
- }
1321
- /**
1322
- * @param {string} spec
1323
- * @returns {boolean}
1324
- */
1325
- function isCssSpecifier(spec) {
1326
- return /\.css(?:[?#].*)?$/i.test(spec);
1327
- }
1328
- /**
1329
- * @param {string} source
1330
- * @param {string} fromFile
1331
- * @param {string} toFile
1332
- * @returns {string}
1333
- */
1334
- function rewriteStaticImportsInSource(source, fromFile, toFile) {
1335
- return source.replace(/(^\s*import(?:\s+[^'"]+?\s+from)?\s*['"])([^'"]+)(['"]\s*;?\s*$)/gm, (_full, prefix, spec, suffix) => `${prefix}${rebaseRelativeSpecifier(spec, fromFile, toFile)}${suffix}`);
1336
- }
1337
- /**
1338
- * @param {string} line
1339
- * @param {string} oldSpec
1340
- * @param {string} newSpec
1341
- * @returns {string}
1342
- */
1343
- function replaceImportSpecifierLiteral(line, oldSpec, newSpec) {
1344
- const single = `'${oldSpec}'`;
1345
- if (line.includes(single)) {
1346
- return line.replace(single, `'${newSpec}'`);
1347
- }
1348
- const dbl = `"${oldSpec}"`;
1349
- if (line.includes(dbl)) {
1350
- return line.replace(dbl, `"${newSpec}"`);
1351
- }
1352
- return line;
1353
- }
1354
- /**
1355
- * @param {string} source
1356
- * @param {string} sourceFile
1357
- * @returns {string}
1358
- */
1359
- function transpileTypeScriptToJs(source, sourceFile) {
1360
- const ts = loadTypeScriptApi();
1361
- if (!ts) {
1362
- return source;
1363
- }
1364
- try {
1365
- const output = ts.transpileModule(source, {
1366
- fileName: sourceFile,
1367
- compilerOptions: {
1368
- module: ts.ModuleKind.ESNext,
1369
- target: ts.ScriptTarget.ES5,
1370
- importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
1371
- verbatimModuleSyntax: true,
1372
- newLine: ts.NewLineKind.LineFeed,
1373
- },
1374
- reportDiagnostics: false,
1375
- });
1376
- return output.outputText;
1377
- }
1378
- catch {
1379
- return source;
1380
- }
1381
- }
1382
- const DEFERRED_RUNTIME_CALLS = new Set(['zenMount', 'zenEffect', 'zeneffect']);
1383
- /**
1384
- * Split top-level runtime side-effect calls from hoistable declarations.
1385
- *
1386
- * Keeps declarations/functions/constants at module scope so rewritten template
1387
- * expressions can resolve their identifiers during hydrate(), while deferring
1388
- * zenMount/zenEffect registration until __zenith_mount().
1389
- *
1390
- * @param {string} body
1391
- * @returns {{ hoisted: string, deferred: string }}
1392
- */
1393
- function splitDeferredRuntimeCalls(body) {
1394
- const ts = loadTypeScriptApi();
1395
- if (!ts || typeof body !== 'string' || body.trim().length === 0) {
1396
- return { hoisted: body, deferred: '' };
1397
- }
1398
- let sourceFile;
1399
- try {
1400
- sourceFile = ts.createSourceFile('zenith-component-runtime.ts', body, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1401
- }
1402
- catch {
1403
- return { hoisted: body, deferred: '' };
1404
- }
1405
- if (!sourceFile || !Array.isArray(sourceFile.statements) || sourceFile.statements.length === 0) {
1406
- return { hoisted: body, deferred: '' };
1407
- }
1408
- /** @type {Array<{ start: number, end: number }>} */
1409
- const ranges = [];
1410
- for (const statement of sourceFile.statements) {
1411
- if (!ts.isExpressionStatement(statement)) {
1412
- continue;
1413
- }
1414
- if (!ts.isCallExpression(statement.expression)) {
1415
- continue;
1416
- }
1417
- let callee = statement.expression.expression;
1418
- while (ts.isParenthesizedExpression(callee)) {
1419
- callee = callee.expression;
1420
- }
1421
- if (!ts.isIdentifier(callee) || !DEFERRED_RUNTIME_CALLS.has(callee.text)) {
1422
- continue;
1423
- }
1424
- const start = typeof statement.getFullStart === 'function'
1425
- ? statement.getFullStart()
1426
- : statement.pos;
1427
- const end = statement.end;
1428
- if (!Number.isInteger(start) || !Number.isInteger(end) || end <= start) {
1429
- continue;
1430
- }
1431
- ranges.push({ start, end });
1432
- }
1433
- if (ranges.length === 0) {
1434
- return { hoisted: body, deferred: '' };
1435
- }
1436
- ranges.sort((a, b) => a.start - b.start);
1437
- /** @type {Array<{ start: number, end: number }>} */
1438
- const merged = [];
1439
- for (const range of ranges) {
1440
- const last = merged[merged.length - 1];
1441
- if (!last || range.start > last.end) {
1442
- merged.push({ start: range.start, end: range.end });
1443
- continue;
1444
- }
1445
- if (range.end > last.end) {
1446
- last.end = range.end;
1447
- }
1448
- }
1449
- let cursor = 0;
1450
- let hoisted = '';
1451
- let deferred = '';
1452
- for (const range of merged) {
1453
- if (range.start > cursor) {
1454
- hoisted += body.slice(cursor, range.start);
1455
- }
1456
- deferred += body.slice(range.start, range.end);
1457
- if (!deferred.endsWith('\n')) {
1458
- deferred += '\n';
1459
- }
1460
- cursor = range.end;
1461
- }
1462
- if (cursor < body.length) {
1463
- hoisted += body.slice(cursor);
1464
- }
1465
- return { hoisted, deferred };
1466
- }
1467
- /**
1468
- * @param {string} source
1469
- * @param {Set<string>} seenStaticImports
1470
- * @returns {string}
1471
- */
1472
- function dedupeStaticImportsInSource(source, seenStaticImports) {
1473
- const lines = source.split('\n');
1474
- const kept = [];
1475
- for (const line of lines) {
1476
- const spec = extractStaticImportSpecifier(line);
1477
- if (!spec) {
1478
- kept.push(line);
1479
- continue;
1480
- }
1481
- const key = line.trim();
1482
- if (seenStaticImports.has(key)) {
1483
- continue;
1484
- }
1485
- seenStaticImports.add(key);
1486
- kept.push(line);
1487
- }
1488
- return kept.join('\n');
1489
- }
1490
- /**
1491
- * @param {string} source
1492
- * @returns {string}
1493
- */
1494
- function stripNonCssStaticImportsInSource(source) {
1495
- const lines = source.split('\n');
1496
- const kept = [];
1497
- for (const line of lines) {
1498
- const spec = extractStaticImportSpecifier(line);
1499
- if (!spec) {
1500
- kept.push(line);
1501
- continue;
1502
- }
1503
- if (isCssSpecifier(spec)) {
1504
- kept.push(line);
1505
- }
1506
- }
1507
- return kept.join('\n');
1508
- }
1509
- /**
1510
- * @param {string} key
1511
- * @returns {string}
1512
- */
1513
- function renderObjectKey(key) {
1514
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
1515
- return key;
1516
- }
1517
- return JSON.stringify(key);
1518
- }
1519
- /**
1520
- * @param {string} value
1521
- * @returns {string | null}
1522
- */
1523
- function deriveScopedIdentifierAlias(value) {
1524
- const ident = String(value || '').trim();
1525
- if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(ident)) {
1526
- return null;
1527
- }
1528
- const parts = ident.split('_').filter(Boolean);
1529
- const candidate = parts.length > 1 ? parts[parts.length - 1] : ident;
1530
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(candidate) ? candidate : ident;
1531
- }
1532
- /**
1533
- * @param {string} source
1534
- * @returns {string[]}
1535
- */
1536
- function extractDeclaredIdentifiers(source) {
1537
- const text = String(source || '').trim();
1538
- if (!text) {
1539
- return [];
1540
- }
1541
- const ts = loadTypeScriptApi();
1542
- if (ts) {
1543
- const sourceFile = ts.createSourceFile('zenith-hoisted-declaration.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1544
- const identifiers = [];
1545
- const collectBindingNames = (name) => {
1546
- if (ts.isIdentifier(name)) {
1547
- identifiers.push(name.text);
1548
- return;
1549
- }
1550
- if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
1551
- for (const element of name.elements) {
1552
- if (ts.isBindingElement(element)) {
1553
- collectBindingNames(element.name);
1554
- }
1555
- }
1556
- }
1557
- };
1558
- for (const statement of sourceFile.statements) {
1559
- if (!ts.isVariableStatement(statement)) {
1560
- continue;
1561
- }
1562
- for (const declaration of statement.declarationList.declarations) {
1563
- collectBindingNames(declaration.name);
1564
- }
1565
- }
1566
- if (identifiers.length > 0) {
1567
- return identifiers;
1568
- }
1569
- }
1570
- const fallback = [];
1571
- const match = text.match(/^\s*(?:const|let|var)\s+([\s\S]+?);?\s*$/);
1572
- if (!match) {
1573
- return fallback;
1574
- }
1575
- const declarationList = match[1];
1576
- const identifierRe = /(?:^|,)\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=,]+)?=/g;
1577
- let found;
1578
- while ((found = identifierRe.exec(declarationList)) !== null) {
1579
- fallback.push(found[1]);
1580
- }
1581
- return fallback;
1582
- }
1583
- /**
1584
- * @param {Map<string, string>} map
1585
- * @param {Set<string>} ambiguous
1586
- * @param {string | null} raw
1587
- * @param {string | null} rewritten
1588
- */
1589
- function recordScopedIdentifierRewrite(map, ambiguous, raw, rewritten) {
1590
- if (typeof raw !== 'string' || raw.length === 0 || typeof rewritten !== 'string' || rewritten.length === 0) {
1591
- return;
1592
- }
1593
- const existing = map.get(raw);
1594
- if (existing && existing !== rewritten) {
1595
- map.delete(raw);
1596
- ambiguous.add(raw);
1597
- return;
1598
- }
1599
- if (!ambiguous.has(raw)) {
1600
- map.set(raw, rewritten);
1601
- }
1602
- }
1603
- /**
1604
- * @param {object | null | undefined} ir
1605
- * @returns {{ map: Map<string, string>, ambiguous: Set<string> }}
1606
- */
1607
- function buildScopedIdentifierRewrite(ir) {
1608
- const out = { map: new Map(), ambiguous: new Set() };
1609
- if (!ir || typeof ir !== 'object') {
1610
- return out;
1611
- }
1612
- const stateBindings = Array.isArray(ir?.hoisted?.state) ? ir.hoisted.state : [];
1613
- for (const stateEntry of stateBindings) {
1614
- const key = typeof stateEntry?.key === 'string' ? stateEntry.key : null;
1615
- recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(key), key);
1616
- }
1617
- const functionBindings = Array.isArray(ir?.hoisted?.functions) ? ir.hoisted.functions : [];
1618
- for (const fnName of functionBindings) {
1619
- if (typeof fnName !== 'string') {
1620
- continue;
1621
- }
1622
- recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(fnName), fnName);
1623
- }
1624
- const declarations = Array.isArray(ir?.hoisted?.declarations) ? ir.hoisted.declarations : [];
1625
- for (const declaration of declarations) {
1626
- if (typeof declaration !== 'string') {
1627
- continue;
1628
- }
1629
- for (const identifier of extractDeclaredIdentifiers(declaration)) {
1630
- recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(identifier), identifier);
1631
- }
1632
- }
1633
- return out;
1634
- }
1635
- function rewriteIdentifiersWithinExpression(expr, scopeMap, scopeAmbiguous) {
1636
- const ts = loadTypeScriptApi();
1637
- if (!(scopeMap instanceof Map) || !ts) {
1638
- return expr;
1639
- }
1640
- const wrapped = `const __zenith_expr__ = (${expr});`;
1641
- let sourceFile;
1642
- try {
1643
- sourceFile = ts.createSourceFile('zenith-expression.ts', wrapped, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1644
- }
1645
- catch {
1646
- return expr;
1647
- }
1648
- const statement = sourceFile.statements[0];
1649
- if (!statement || !ts.isVariableStatement(statement)) {
1650
- return expr;
1651
- }
1652
- const initializer = statement.declarationList.declarations[0]?.initializer;
1653
- const root = initializer && ts.isParenthesizedExpression(initializer) ? initializer.expression : initializer;
1654
- if (!root) {
1655
- return expr;
1656
- }
1657
- const replacements = [];
1658
- const collectBoundNames = (name, target) => {
1659
- if (ts.isIdentifier(name)) {
1660
- target.add(name.text);
1661
- return;
1662
- }
1663
- if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
1664
- for (const element of name.elements) {
1665
- if (ts.isBindingElement(element)) {
1666
- collectBoundNames(element.name, target);
1667
- }
1668
- }
1669
- }
1670
- };
1671
- const shouldSkipIdentifier = (node, localBindings) => {
1672
- if (localBindings.has(node.text)) {
1673
- return true;
1674
- }
1675
- const parent = node.parent;
1676
- if (!parent) {
1677
- return false;
1678
- }
1679
- if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
1680
- return true;
1681
- }
1682
- if (ts.isPropertyAssignment(parent) && parent.name === node) {
1683
- return true;
1684
- }
1685
- if (ts.isShorthandPropertyAssignment(parent)) {
1686
- return true;
1687
- }
1688
- if (ts.isBindingElement(parent) && parent.name === node) {
1689
- return true;
1690
- }
1691
- if (ts.isParameter(parent) && parent.name === node) {
1692
- return true;
1693
- }
1694
- return false;
1695
- };
1696
- const visit = (node, localBindings) => {
1697
- let nextBindings = localBindings;
1698
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
1699
- nextBindings = new Set(localBindings);
1700
- if (node.name && ts.isIdentifier(node.name)) {
1701
- nextBindings.add(node.name.text);
1702
- }
1703
- for (const param of node.parameters) {
1704
- collectBoundNames(param.name, nextBindings);
1705
- }
1706
- }
1707
- if (ts.isIdentifier(node) && !shouldSkipIdentifier(node, nextBindings)) {
1708
- const rewritten = scopeMap.get(node.text);
1709
- if (typeof rewritten === 'string' &&
1710
- rewritten.length > 0 &&
1711
- rewritten !== node.text &&
1712
- !(scopeAmbiguous instanceof Set && scopeAmbiguous.has(node.text))) {
1713
- replacements.push({
1714
- start: node.getStart(sourceFile),
1715
- end: node.getEnd(),
1716
- text: rewritten
1717
- });
1718
- }
1719
- }
1720
- ts.forEachChild(node, (child) => visit(child, nextBindings));
1721
- };
1722
- visit(root, new Set());
1723
- if (replacements.length === 0) {
1724
- return expr;
1725
- }
1726
- let rewritten = wrapped;
1727
- for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
1728
- rewritten = `${rewritten.slice(0, replacement.start)}${replacement.text}${rewritten.slice(replacement.end)}`;
1729
- }
1730
- const prefix = 'const __zenith_expr__ = (';
1731
- const suffix = ');';
1732
- if (!rewritten.startsWith(prefix) || !rewritten.endsWith(suffix)) {
1733
- return expr;
1734
- }
1735
- return rewritten.slice(prefix.length, rewritten.length - suffix.length);
1736
- }
1737
- /**
1738
- * @param {string} expr
1739
- * @param {{
1740
- * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1741
- * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1742
- * } | null} rewriteContext
1743
- * @returns {string}
1744
- */
1745
- function rewritePropsExpression(expr, rewriteContext = null) {
1746
- const trimmed = String(expr || '').trim();
1747
- if (!trimmed) {
1748
- return trimmed;
1749
- }
1750
- const expressionMap = rewriteContext?.expressionRewrite?.map;
1751
- const expressionAmbiguous = rewriteContext?.expressionRewrite?.ambiguous;
1752
- if (expressionMap instanceof Map &&
1753
- !(expressionAmbiguous instanceof Set && expressionAmbiguous.has(trimmed))) {
1754
- const exact = expressionMap.get(trimmed);
1755
- if (typeof exact === 'string' && exact.length > 0) {
1756
- return exact;
1757
- }
1758
- }
1759
- const scopeMap = rewriteContext?.scopeRewrite?.map;
1760
- const scopeAmbiguous = rewriteContext?.scopeRewrite?.ambiguous;
1761
- const rootMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)([\s\S]*)$/);
1762
- if (!(scopeMap instanceof Map)) {
1763
- return trimmed;
1764
- }
1765
- if (!rootMatch) {
1766
- return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1767
- }
1768
- const root = rootMatch[1];
1769
- if (scopeAmbiguous instanceof Set && scopeAmbiguous.has(root)) {
1770
- return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1771
- }
1772
- const rewrittenRoot = scopeMap.get(root);
1773
- if (typeof rewrittenRoot !== 'string' || rewrittenRoot.length === 0 || rewrittenRoot === root) {
1774
- return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1775
- }
1776
- return `${rewrittenRoot}${rootMatch[2]}`;
1777
- }
1778
- /**
1779
- * Translate compiler `compiled_expr` output into module-scope JS for props
1780
- * preludes. Expression bindings use `signalMap.get(i)` at hydrate-time, but
1781
- * component props need direct access to the already-hoisted scoped symbols.
1782
- *
1783
- * @param {string | null | undefined} compiledExpr
1784
- * @param {{
1785
- * signals?: Array<{ state_index?: number }>,
1786
- * stateBindings?: Array<{ key?: string }>
1787
- * } | null | undefined} expressionRewrite
1788
- * @returns {string | null}
1789
- */
1790
- function resolveCompiledPropsExpression(compiledExpr, expressionRewrite = null) {
1791
- const source = typeof compiledExpr === 'string' ? compiledExpr.trim() : '';
1792
- if (!source) {
1793
- return null;
1794
- }
1795
- const signals = Array.isArray(expressionRewrite?.signals) ? expressionRewrite.signals : [];
1796
- const stateBindings = Array.isArray(expressionRewrite?.stateBindings) ? expressionRewrite.stateBindings : [];
1797
- return source.replace(/signalMap\.get\((\d+)\)(?:\.get\(\))?/g, (full, rawIndex) => {
1798
- const signalIndex = Number.parseInt(rawIndex, 10);
1799
- if (!Number.isInteger(signalIndex)) {
1800
- return full;
1801
- }
1802
- const signal = signals[signalIndex];
1803
- const stateIndex = signal?.state_index;
1804
- const stateKey = Number.isInteger(stateIndex) ? stateBindings[stateIndex]?.key : null;
1805
- if (typeof stateKey !== 'string' || stateKey.length === 0) {
1806
- return full;
1807
- }
1808
- return full.endsWith('.get()') ? `${stateKey}.get()` : stateKey;
1809
- });
1810
- }
1811
- /**
1812
- * Resolve a raw component prop expression to the same scoped symbol/expression
1813
- * contract used by the compiler rename pass.
1814
- *
1815
- * @param {string} expr
1816
- * @param {{
1817
- * expressionRewrite?: {
1818
- * map?: Map<string, string>,
1819
- * bindings?: Map<string, {
1820
- * compiled_expr?: string | null
1821
- * }>,
1822
- * ambiguous?: Set<string>,
1823
- * signals?: Array<{ state_index?: number }>,
1824
- * stateBindings?: Array<{ key?: string }>
1825
- * } | null,
1826
- * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1827
- * } | null} rewriteContext
1828
- * @returns {string}
1829
- */
1830
- function resolvePropsValueCode(expr, rewriteContext = null) {
1831
- const trimmed = String(expr || '').trim();
1832
- if (!trimmed) {
1833
- return trimmed;
1834
- }
1835
- const expressionRewrite = rewriteContext?.expressionRewrite;
1836
- const expressionAmbiguous = expressionRewrite?.ambiguous;
1837
- if (!(expressionAmbiguous instanceof Set && expressionAmbiguous.has(trimmed))) {
1838
- const binding = expressionRewrite?.bindings instanceof Map
1839
- ? expressionRewrite.bindings.get(trimmed)
1840
- : null;
1841
- const compiled = resolveCompiledPropsExpression(binding?.compiled_expr, expressionRewrite);
1842
- if (typeof compiled === 'string' && compiled.length > 0) {
1843
- return compiled;
1844
- }
1845
- const exact = expressionRewrite?.map instanceof Map
1846
- ? expressionRewrite.map.get(trimmed)
1847
- : null;
1848
- if (typeof exact === 'string' && exact.length > 0) {
1849
- return exact;
1850
- }
1851
- }
1852
- return rewritePropsExpression(trimmed, rewriteContext);
1853
- }
1854
- /**
1855
- * @param {string} attrs
1856
- * @param {{
1857
- * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1858
- * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1859
- * } | null} rewriteContext
1860
- * @returns {string}
1861
- */
1862
- function renderPropsLiteralFromAttrs(attrs, rewriteContext = null) {
1863
- const src = String(attrs || '').trim();
1864
- if (!src) {
1865
- return '{}';
1866
- }
1867
- const entries = [];
1868
- const attrRe = /([A-Za-z_$][A-Za-z0-9_$-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([\s\S]*?)\}))?/g;
1869
- let match;
1870
- while ((match = attrRe.exec(src)) !== null) {
1871
- const rawName = match[1];
1872
- if (!rawName || rawName.startsWith('on:')) {
1873
- continue;
1874
- }
1875
- const doubleQuoted = match[2];
1876
- const singleQuoted = match[3];
1877
- const expressionValue = match[4];
1878
- let valueCode = 'true';
1879
- if (doubleQuoted !== undefined) {
1880
- valueCode = JSON.stringify(doubleQuoted);
1881
- }
1882
- else if (singleQuoted !== undefined) {
1883
- valueCode = JSON.stringify(singleQuoted);
1884
- }
1885
- else if (expressionValue !== undefined) {
1886
- const trimmed = String(expressionValue).trim();
1887
- valueCode = trimmed.length > 0 ? resolvePropsValueCode(trimmed, rewriteContext) : 'undefined';
1888
- }
1889
- entries.push(`${renderObjectKey(rawName)}: ${valueCode}`);
1890
- }
1891
- if (entries.length === 0) {
1892
- return '{}';
1893
- }
1894
- return `{ ${entries.join(', ')} }`;
1895
- }
1896
- /**
1897
- * @param {string} source
1898
- * @param {string} attrs
1899
- * @param {{
1900
- * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1901
- * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1902
- * } | null} rewriteContext
1903
- * @returns {string}
1904
- */
1905
- function injectPropsPrelude(source, attrs, rewriteContext = null) {
1906
- if (typeof source !== 'string' || source.trim().length === 0) {
1907
- return source;
1908
- }
1909
- if (!/\bprops\b/.test(source)) {
1910
- return source;
1911
- }
1912
- if (/\b(?:const|let|var)\s+props\b/.test(source)) {
1913
- return source;
1914
- }
1915
- const propsLiteral = renderPropsLiteralFromAttrs(attrs, rewriteContext);
1916
- return `var props = ${propsLiteral};\n${source}`;
1917
- }
1918
- /**
1919
- * @param {string} source
1920
- * @returns {string}
1921
- */
1922
- function deferComponentRuntimeBlock(source) {
1923
- const lines = source.split('\n');
1924
- const importLines = [];
1925
- const bodyLines = [];
1926
- let inImportPrefix = true;
1927
- for (const line of lines) {
1928
- if (inImportPrefix && extractStaticImportSpecifier(line)) {
1929
- importLines.push(line);
1930
- continue;
1931
- }
1932
- inImportPrefix = false;
1933
- bodyLines.push(line);
1934
- }
1935
- const body = bodyLines.join('\n');
1936
- if (body.trim().length === 0) {
1937
- return importLines.join('\n');
1938
- }
1939
- const { hoisted, deferred } = splitDeferredRuntimeCalls(body);
1940
- if (deferred.trim().length === 0) {
1941
- return [importLines.join('\n').trim(), hoisted.trim()]
1942
- .filter((segment) => segment.length > 0)
1943
- .join('\n');
1944
- }
1945
- const indentedBody = deferred
1946
- .trim()
1947
- .split('\n')
1948
- .map((line) => ` ${line}`)
1949
- .join('\n');
1950
- const wrapped = [
1951
- importLines.join('\n').trim(),
1952
- hoisted.trim(),
1953
- "__zenith_component_bootstraps.push(() => {",
1954
- indentedBody,
1955
- "});"
1956
- ]
1957
- .filter((segment) => segment.length > 0)
1958
- .join('\n');
1959
- return wrapped;
1960
- }
1961
- /**
1962
- * Run bundler process for one page envelope.
1963
- *
1964
- * @param {object|object[]} envelope
1965
- * @param {string} outDir
1966
- * @param {string} projectRoot
1967
- * @param {object | null} [logger]
1968
- * @param {boolean} [showInfo]
1969
- * @param {string|object} [bundlerBin]
1970
- * @returns {Promise<void>}
1971
- */
1972
- function runBundler(envelope, outDir, projectRoot, logger = null, showInfo = true, bundlerBin = resolveBundlerBin(projectRoot)) {
1973
- return new Promise((resolvePromise, rejectPromise) => {
1974
- const useStructuredLogger = Boolean(logger && typeof logger.childLine === 'function');
1975
- const bundlerToolchain = bundlerBin && typeof bundlerBin === 'object'
1976
- ? bundlerBin
1977
- : null;
1978
- const bundlerCandidate = bundlerToolchain
1979
- ? getActiveToolchainCandidate(bundlerToolchain)
1980
- : null;
1981
- const bundlerPath = bundlerCandidate?.command || bundlerBin;
1982
- const bundlerArgs = [
1983
- ...(Array.isArray(bundlerCandidate?.argsPrefix) ? bundlerCandidate.argsPrefix : []),
1984
- '--out-dir',
1985
- outDir
1986
- ];
1987
- const child = spawn(bundlerPath, bundlerArgs, {
1988
- cwd: projectRoot,
1989
- stdio: useStructuredLogger ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'inherit']
1990
- });
1991
- if (useStructuredLogger) {
1992
- forwardStreamLines(child.stdout, (line) => {
1993
- logger.childLine('bundler', line, { stream: 'stdout', showInfo });
1994
- });
1995
- forwardStreamLines(child.stderr, (line) => {
1996
- logger.childLine('bundler', line, { stream: 'stderr', showInfo: true });
1997
- });
1998
- }
1999
- child.on('error', (err) => {
2000
- rejectPromise(new Error(`Bundler spawn failed: ${err.message}`));
2001
- });
2002
- child.on('close', (code) => {
2003
- if (code === 0) {
2004
- resolvePromise();
2005
- return;
2006
- }
2007
- rejectPromise(new Error(`Bundler failed with exit code ${code}`));
2008
- });
2009
- child.stdin.write(JSON.stringify(envelope));
2010
- child.stdin.end();
2011
- });
2012
- }
2013
- /**
2014
- * Collect generated assets for reporting.
2015
- *
2016
- * @param {string} rootDir
2017
- * @returns {Promise<string[]>}
2018
- */
2019
- async function collectAssets(rootDir) {
2020
- const files = [];
2021
- async function walk(dir) {
2022
- let entries = [];
2023
- try {
2024
- entries = await readdir(dir);
2025
- }
2026
- catch {
2027
- return;
2028
- }
2029
- entries.sort((a, b) => a.localeCompare(b));
2030
- for (const name of entries) {
2031
- const fullPath = join(dir, name);
2032
- const info = await stat(fullPath);
2033
- if (info.isDirectory()) {
2034
- await walk(fullPath);
2035
- continue;
2036
- }
2037
- if (fullPath.endsWith('.js') || fullPath.endsWith('.css')) {
2038
- files.push(relative(rootDir, fullPath).replaceAll('\\', '/'));
2039
- }
2040
- }
2041
- }
2042
- await walk(rootDir);
2043
- files.sort((a, b) => a.localeCompare(b));
2044
- return files;
2045
- }
2046
- /**
2047
- * Build all pages by orchestrating compiler and bundler binaries.
2048
- *
2049
- * Pipeline:
2050
- * 1. Build component registry (PascalCase name → .zen file path)
2051
- * 2. For each page:
2052
- * a. Expand PascalCase tags into component template HTML
2053
- * b. Compile expanded page source via --stdin
2054
- * c. Compile each used component separately for script IR
2055
- * d. Merge component IRs into page IR
2056
- * 3. Send all envelopes to bundler
2057
- *
2058
- * @param {{ pagesDir: string, outDir: string, config?: object, logger?: object | null, showBundlerInfo?: boolean }} options
2059
38
  * @returns {Promise<{ pages: number, assets: string[] }>}
2060
39
  */
2061
40
  export async function build(options) {
2062
41
  const { pagesDir, outDir, config = {}, logger = null, showBundlerInfo = true } = options;
42
+ const startupProfile = createStartupProfiler('cli-build');
2063
43
  const projectRoot = deriveProjectRootFromPagesDir(pagesDir);
44
+ const srcDir = resolve(pagesDir, '..');
2064
45
  const compilerBin = createCompilerToolchain({ projectRoot, logger });
2065
46
  const bundlerBin = createBundlerToolchain({ projectRoot, logger });
47
+ const compilerTotals = createCompilerTotals();
2066
48
  const softNavigationEnabled = config.softNavigation === true || config.router === true;
2067
49
  const compilerOpts = {
2068
50
  typescriptDefault: config.typescriptDefault === true,
2069
- experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true || config.experimental?.embeddedMarkupExpressions === true,
51
+ experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true
52
+ || config.experimental?.embeddedMarkupExpressions === true,
2070
53
  strictDomLints: config.strictDomLints === true
2071
54
  };
2072
- await rm(outDir, { recursive: true, force: true });
2073
- await mkdir(outDir, { recursive: true });
2074
55
  ensureToolchainCompatibility(bundlerBin);
2075
56
  const resolvedBundlerCandidate = getActiveToolchainCandidate(bundlerBin);
2076
57
  if (logger) {
2077
- await maybeWarnAboutZenithVersionMismatch({
58
+ await startupProfile.measureAsync('version_mismatch_check', () => maybeWarnAboutZenithVersionMismatch({
2078
59
  projectRoot,
2079
60
  logger,
2080
61
  command: 'build',
2081
62
  bundlerBinPath: resolvedBundlerCandidate?.path || resolveBundlerBin(projectRoot)
2082
- });
2083
- }
2084
- // Derive src/ directory from pages/ directory
2085
- const srcDir = resolve(pagesDir, '..');
2086
- // 1. Build component registry
2087
- const registry = buildComponentRegistry(srcDir);
2088
- if (registry.size > 0) {
2089
- if (logger && typeof logger.build === 'function') {
2090
- logger.build(`registry=${registry.size} components`, {
2091
- onceKey: `component-registry:${registry.size}`
2092
- });
2093
- }
2094
- else {
2095
- console.log(`[zenith] Component registry: ${registry.size} components`);
2096
- }
2097
- }
2098
- const manifest = await generateManifest(pagesDir);
2099
- await ensureZenithTypeDeclarations({ manifest, pagesDir });
2100
- // Cache: avoid re-compiling the same component for multiple pages
2101
- /** @type {Map<string, object>} */
2102
- const componentIrCache = new Map();
2103
- /** @type {Map<string, boolean>} */
2104
- const componentDocumentModeCache = new Map();
2105
- /** @type {Map<string, { map: Map<string, string>, ambiguous: Set<string> }>} */
2106
- const componentExpressionRewriteCache = new Map();
63
+ }));
64
+ }
65
+ const registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
66
+ void RUNTIME_MARKUP_BINDING;
67
+ const manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(pagesDir));
68
+ await startupProfile.measureAsync('ensure_zenith_type_declarations', () => ensureZenithTypeDeclarations({
69
+ manifest,
70
+ pagesDir
71
+ }));
2107
72
  const emitCompilerWarning = createCompilerWarningEmitter((line) => {
2108
73
  if (logger && typeof logger.warn === 'function') {
2109
74
  logger.warn(line, { onceKey: `compiler-warning:${line}` });
@@ -2111,190 +76,38 @@ export async function build(options) {
2111
76
  }
2112
77
  console.warn(line);
2113
78
  });
2114
- const envelopes = [];
2115
- for (const entry of manifest) {
2116
- const sourceFile = join(pagesDir, entry.file);
2117
- const rawSource = readFileSync(sourceFile, 'utf8');
2118
- const componentOccurrences = collectExpandedComponentOccurrences(rawSource, registry, sourceFile);
2119
- const pageOwnerSource = extractServerScript(rawSource, sourceFile, compilerOpts).source;
2120
- const baseName = sourceFile.slice(0, -extname(sourceFile).length);
2121
- let adjacentGuard = null;
2122
- let adjacentLoad = null;
2123
- for (const ext of ['.ts', '.js']) {
2124
- if (!adjacentGuard && existsSync(`${baseName}.guard${ext}`))
2125
- adjacentGuard = `${baseName}.guard${ext}`;
2126
- if (!adjacentLoad && existsSync(`${baseName}.load${ext}`))
2127
- adjacentLoad = `${baseName}.load${ext}`;
2128
- }
2129
- // 2a. Expand PascalCase component tags
2130
- const { expandedSource } = expandComponents(rawSource, registry, sourceFile);
2131
- const extractedServer = extractServerScript(expandedSource, sourceFile, compilerOpts);
2132
- const compileSource = extractedServer.source;
2133
- // 2b. Compile expanded page source via --stdin
2134
- const pageIr = runCompiler(sourceFile, compileSource, compilerOpts, {
2135
- compilerToolchain: compilerBin,
2136
- onWarning: emitCompilerWarning
2137
- });
2138
- const hasGuard = (extractedServer.serverScript && extractedServer.serverScript.has_guard) || adjacentGuard !== null;
2139
- const hasLoad = (extractedServer.serverScript && extractedServer.serverScript.has_load) || adjacentLoad !== null;
2140
- if (extractedServer.serverScript) {
2141
- pageIr.server_script = extractedServer.serverScript;
2142
- pageIr.prerender = extractedServer.serverScript.prerender === true;
2143
- if (pageIr.ssr_data === undefined) {
2144
- pageIr.ssr_data = null;
2145
- }
2146
- }
2147
- // Static Build Route Protection Policy
2148
- if (pageIr.prerender === true && (hasGuard || hasLoad)) {
2149
- throw new Error(`[zenith] Build failed for ${entry.file}: protected routes require SSR/runtime. ` +
2150
- `Cannot prerender a static route with a \`guard\` or \`load\` function.`);
2151
- }
2152
- // Apply metadata to IR
2153
- pageIr.has_guard = hasGuard;
2154
- pageIr.has_load = hasLoad;
2155
- pageIr.guard_module_ref = adjacentGuard ? relative(srcDir, adjacentGuard).replaceAll('\\', '/') : null;
2156
- pageIr.load_module_ref = adjacentLoad ? relative(srcDir, adjacentLoad).replaceAll('\\', '/') : null;
2157
- // Ensure IR has required array fields for merging
2158
- pageIr.components_scripts = pageIr.components_scripts || {};
2159
- pageIr.component_instances = pageIr.component_instances || [];
2160
- pageIr.signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
2161
- pageIr.hoisted = pageIr.hoisted || { imports: [], declarations: [], functions: [], signals: [], state: [], code: [] };
2162
- pageIr.hoisted.imports = pageIr.hoisted.imports || [];
2163
- pageIr.hoisted.declarations = pageIr.hoisted.declarations || [];
2164
- pageIr.hoisted.functions = pageIr.hoisted.functions || [];
2165
- pageIr.hoisted.signals = pageIr.hoisted.signals || [];
2166
- pageIr.hoisted.state = pageIr.hoisted.state || [];
2167
- pageIr.hoisted.code = pageIr.hoisted.code || [];
2168
- const seenStaticImports = new Set();
2169
- const occurrenceCountByPath = new Map();
2170
- for (const occurrence of componentOccurrences) {
2171
- const key = occurrence.componentPath || occurrence.name;
2172
- occurrenceCountByPath.set(key, (occurrenceCountByPath.get(key) || 0) + 1);
2173
- }
2174
- const pageExpressionRewriteMap = new Map();
2175
- const pageExpressionBindingMap = new Map();
2176
- const pageAmbiguousExpressionMap = new Set();
2177
- const knownRefKeys = new Set();
2178
- const componentOccurrencePlans = [];
2179
- const pageOwnerIr = componentOccurrences.length > 0
2180
- ? runCompiler(sourceFile, pageOwnerSource, compilerOpts, {
2181
- suppressWarnings: true,
2182
- compilerToolchain: compilerBin
2183
- })
2184
- : null;
2185
- const pageOwnerExpressionRewrite = pageOwnerIr
2186
- ? buildComponentExpressionRewrite(sourceFile, pageOwnerSource, pageOwnerIr, compilerOpts, compilerBin)
2187
- : { map: new Map(), bindings: new Map(), signals: [], stateBindings: [], ambiguous: new Set(), sequence: [] };
2188
- const pageOwnerScopeRewrite = pageOwnerIr
2189
- ? buildScopedIdentifierRewrite(pageOwnerIr)
2190
- : { map: new Map(), ambiguous: new Set() };
2191
- const pageSelfExpressionRewrite = buildComponentExpressionRewrite(sourceFile, compileSource, pageIr, compilerOpts, compilerBin);
2192
- mergeExpressionRewriteMaps(pageExpressionRewriteMap, pageExpressionBindingMap, pageAmbiguousExpressionMap, pageSelfExpressionRewrite, pageIr);
2193
- const componentScopeRewriteCache = new Map();
2194
- let componentInstanceCounter = 0;
2195
- // 2c. Compile each used component separately for its script IR
2196
- for (const occurrence of componentOccurrences) {
2197
- const compName = occurrence.name;
2198
- const compPath = occurrence.componentPath || registry.get(compName);
2199
- if (!compPath)
2200
- continue;
2201
- const componentSource = readFileSync(compPath, 'utf8');
2202
- const occurrenceCount = occurrenceCountByPath.get(compPath) || 0;
2203
- let compIr;
2204
- if (componentIrCache.has(compPath)) {
2205
- compIr = componentIrCache.get(compPath);
2206
- }
2207
- else {
2208
- const componentCompileSource = stripStyleBlocks(componentSource);
2209
- compIr = runCompiler(compPath, componentCompileSource, compilerOpts, {
2210
- compilerToolchain: compilerBin,
2211
- onWarning: emitCompilerWarning
2212
- });
2213
- componentIrCache.set(compPath, compIr);
2214
- }
2215
- let isDocMode = componentDocumentModeCache.get(compPath);
2216
- if (isDocMode === undefined) {
2217
- isDocMode = isDocumentMode(extractTemplate(componentSource));
2218
- componentDocumentModeCache.set(compPath, isDocMode);
2219
- }
2220
- let expressionRewrite = componentExpressionRewriteCache.get(compPath);
2221
- if (!expressionRewrite) {
2222
- expressionRewrite = buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts, compilerBin);
2223
- componentExpressionRewriteCache.set(compPath, expressionRewrite);
2224
- }
2225
- let attrExpressionRewrite = pageOwnerExpressionRewrite;
2226
- let attrScopeRewrite = pageOwnerScopeRewrite;
2227
- const ownerPath = typeof occurrence.ownerPath === 'string' && occurrence.ownerPath.length > 0
2228
- ? occurrence.ownerPath
2229
- : sourceFile;
2230
- if (ownerPath !== sourceFile) {
2231
- let ownerIr = componentIrCache.get(ownerPath);
2232
- if (!ownerIr) {
2233
- const ownerSource = readFileSync(ownerPath, 'utf8');
2234
- ownerIr = runCompiler(ownerPath, stripStyleBlocks(ownerSource), compilerOpts, {
2235
- compilerToolchain: compilerBin,
2236
- onWarning: emitCompilerWarning
2237
- });
2238
- componentIrCache.set(ownerPath, ownerIr);
2239
- }
2240
- attrExpressionRewrite = componentExpressionRewriteCache.get(ownerPath);
2241
- if (!attrExpressionRewrite) {
2242
- const ownerSource = readFileSync(ownerPath, 'utf8');
2243
- attrExpressionRewrite = buildComponentExpressionRewrite(ownerPath, ownerSource, ownerIr, compilerOpts, compilerBin);
2244
- componentExpressionRewriteCache.set(ownerPath, attrExpressionRewrite);
2245
- }
2246
- attrScopeRewrite = componentScopeRewriteCache.get(ownerPath);
2247
- if (!attrScopeRewrite) {
2248
- attrScopeRewrite = buildScopedIdentifierRewrite(ownerIr);
2249
- componentScopeRewriteCache.set(ownerPath, attrScopeRewrite);
2250
- }
2251
- }
2252
- const useIsolatedInstance = occurrenceCount > 1;
2253
- const { ir: instanceIr, refIdentifierPairs } = useIsolatedInstance
2254
- ? cloneComponentIrForInstance(compIr, componentInstanceCounter++, extractDeclaredIdentifiers, resolveStateKeyFromBindings)
2255
- : { ir: compIr, refIdentifierPairs: [] };
2256
- const instanceRewrite = useIsolatedInstance
2257
- ? buildComponentExpressionRewrite(compPath, componentSource, instanceIr, compilerOpts, compilerBin)
2258
- : expressionRewrite;
2259
- // 2d. Merge component IR into page IR
2260
- mergeComponentIr(pageIr, instanceIr, compPath, sourceFile, {
2261
- includeCode: true,
2262
- cssImportsOnly: isDocMode,
2263
- documentMode: isDocMode,
2264
- componentAttrs: typeof occurrence.attrs === 'string' ? occurrence.attrs : '',
2265
- componentAttrsRewrite: {
2266
- expressionRewrite: attrExpressionRewrite,
2267
- scopeRewrite: attrScopeRewrite
2268
- }
2269
- }, seenStaticImports, knownRefKeys);
2270
- if (useIsolatedInstance) {
2271
- componentOccurrencePlans.push({
2272
- rewrite: instanceRewrite,
2273
- expressionSequence: instanceRewrite.sequence,
2274
- refSequence: refIdentifierPairs
2275
- });
2276
- }
2277
- else {
2278
- mergeExpressionRewriteMaps(pageExpressionRewriteMap, pageExpressionBindingMap, pageAmbiguousExpressionMap, expressionRewrite, pageIr);
2279
- }
2280
- }
2281
- applyOccurrenceRewritePlans(pageIr, componentOccurrencePlans, (rewrite, binding) => resolveRewrittenBindingMetadata(pageIr, rewrite, binding));
2282
- applyExpressionRewrites(pageIr, pageExpressionRewriteMap, pageExpressionBindingMap, pageAmbiguousExpressionMap);
2283
- applyScopedIdentifierRewrites(pageIr, buildScopedIdentifierRewrite(pageIr));
2284
- synthesizeSignalBackedCompiledExpressions(pageIr);
2285
- normalizeExpressionBindingDependencies(pageIr);
2286
- rewriteLegacyMarkupIdentifiers(pageIr);
2287
- rewriteRefBindingIdentifiers(pageIr, knownRefKeys);
2288
- envelopes.push({
2289
- route: entry.path,
2290
- file: sourceFile,
2291
- ir: pageIr,
2292
- router: softNavigationEnabled
2293
- });
2294
- }
79
+ const { envelopes, expressionRewriteMetrics } = await buildPageEnvelopes({
80
+ manifest,
81
+ pagesDir,
82
+ srcDir,
83
+ registry,
84
+ compilerOpts,
85
+ compilerBin,
86
+ softNavigationEnabled,
87
+ startupProfile,
88
+ compilerTotals,
89
+ emitCompilerWarning
90
+ });
2295
91
  if (envelopes.length > 0) {
2296
- await runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo, bundlerBin);
2297
- }
2298
- const assets = await collectAssets(outDir);
92
+ await startupProfile.measureAsync('run_bundler', () => runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo, bundlerBin), { envelopes: envelopes.length });
93
+ }
94
+ const { manifest: imageManifest } = await startupProfile.measureAsync('build_image_artifacts', () => buildImageArtifacts({
95
+ projectRoot,
96
+ outDir,
97
+ config: config.images
98
+ }));
99
+ const imageRuntimePayload = createImageRuntimePayload(config.images, imageManifest, 'passthrough');
100
+ await startupProfile.measureAsync('materialize_image_markup', () => materializeImageMarkupInHtmlFiles({
101
+ distDir: outDir,
102
+ payload: imageRuntimePayload
103
+ }));
104
+ await startupProfile.measureAsync('inject_image_runtime_payload', () => injectImageRuntimePayloadIntoHtmlFiles(outDir, imageRuntimePayload));
105
+ const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
106
+ startupProfile.emit('build_complete', {
107
+ pages: manifest.length,
108
+ assets: assets.length,
109
+ compilerTotals,
110
+ expressionRewriteMetrics
111
+ });
2299
112
  return { pages: manifest.length, assets };
2300
113
  }