@zenithbuild/cli 0.5.0-beta.2.6 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/build.js +284 -126
- package/dist/dev-server.js +607 -55
- package/dist/index.js +84 -23
- package/dist/preview.js +332 -41
- package/dist/resolve-components.js +108 -0
- package/dist/server-contract.js +150 -11
- package/dist/ui/env.js +17 -1
- package/dist/ui/format.js +131 -54
- package/dist/ui/logger.js +239 -74
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
The command-line interface for developing and building Zenith applications.
|
|
7
7
|
|
|
8
|
+
## Canonical Docs
|
|
9
|
+
|
|
10
|
+
- CLI contract: `../zenith-docs/documentation/cli-contract.md`
|
|
11
|
+
- Script server/data contract: `../zenith-docs/documentation/contracts/server-data.md`
|
|
12
|
+
|
|
8
13
|
## Overview
|
|
9
14
|
|
|
10
15
|
`@zenithbuild/cli` provides the toolchain needed to manage Zenith projects. While `create-zenith` is for scaffolding, this CLI is for the daily development loop: serving apps, building for production, and managing plugins.
|
package/dist/build.js
CHANGED
|
@@ -15,7 +15,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
|
-
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
18
|
+
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { generateManifest } from './manifest.js';
|
|
21
21
|
import { buildComponentRegistry, expandComponents, extractTemplate, isDocumentMode } from './resolve-components.js';
|
|
@@ -63,10 +63,63 @@ const COMPILER_BIN = resolveBinary([
|
|
|
63
63
|
resolve(CLI_ROOT, '../zenith-compiler/target/release/zenith-compiler')
|
|
64
64
|
]);
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
function getBundlerBin() {
|
|
67
|
+
const envBin = process.env.ZENITH_BUNDLER_BIN;
|
|
68
|
+
if (envBin && typeof envBin === 'string' && existsSync(envBin)) {
|
|
69
|
+
return envBin;
|
|
70
|
+
}
|
|
71
|
+
return resolveBinary([
|
|
72
|
+
resolve(CLI_ROOT, '../bundler/target/release/zenith-bundler'),
|
|
73
|
+
resolve(CLI_ROOT, '../zenith-bundler/target/release/zenith-bundler')
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a per-build warning emitter that deduplicates repeated compiler lines.
|
|
79
|
+
*
|
|
80
|
+
* @param {(line: string) => void} sink
|
|
81
|
+
* @returns {(line: string) => void}
|
|
82
|
+
*/
|
|
83
|
+
export function createCompilerWarningEmitter(sink = (line) => console.warn(line)) {
|
|
84
|
+
const emitted = new Set();
|
|
85
|
+
return (line) => {
|
|
86
|
+
const text = String(line || '').trim();
|
|
87
|
+
if (!text || emitted.has(text)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
emitted.add(text);
|
|
91
|
+
sink(text);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Forward child-process output line-by-line through the structured logger.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('node:stream').Readable | null | undefined} stream
|
|
99
|
+
* @param {(line: string) => void} onLine
|
|
100
|
+
*/
|
|
101
|
+
function forwardStreamLines(stream, onLine) {
|
|
102
|
+
if (!stream || typeof stream.on !== 'function') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let pending = '';
|
|
106
|
+
stream.setEncoding?.('utf8');
|
|
107
|
+
stream.on('data', (chunk) => {
|
|
108
|
+
pending += String(chunk || '');
|
|
109
|
+
const lines = pending.split(/\r?\n/);
|
|
110
|
+
pending = lines.pop() || '';
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (line.trim().length > 0) {
|
|
113
|
+
onLine(line);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
stream.on('end', () => {
|
|
118
|
+
if (pending.trim().length > 0) {
|
|
119
|
+
onLine(pending);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
70
123
|
|
|
71
124
|
/**
|
|
72
125
|
* Run the compiler process and parse its JSON stdout.
|
|
@@ -77,15 +130,21 @@ const BUNDLER_BIN = resolveBinary([
|
|
|
77
130
|
*
|
|
78
131
|
* @param {string} filePath — path for diagnostics (and file reading when no stdinSource)
|
|
79
132
|
* @param {string} [stdinSource] — if provided, piped to compiler via stdin
|
|
133
|
+
* @param {object} compilerRunOptions
|
|
134
|
+
* @param {(warning: string) => void} [compilerRunOptions.onWarning]
|
|
135
|
+
* @param {boolean} [compilerRunOptions.suppressWarnings]
|
|
80
136
|
* @returns {object}
|
|
81
137
|
*/
|
|
82
|
-
function runCompiler(filePath, stdinSource, compilerOpts = {}) {
|
|
138
|
+
function runCompiler(filePath, stdinSource, compilerOpts = {}, compilerRunOptions = {}) {
|
|
83
139
|
const args = stdinSource !== undefined
|
|
84
140
|
? ['--stdin', filePath]
|
|
85
141
|
: [filePath];
|
|
86
142
|
if (compilerOpts?.experimentalEmbeddedMarkup) {
|
|
87
143
|
args.push('--embedded-markup-expressions');
|
|
88
144
|
}
|
|
145
|
+
if (compilerOpts?.strictDomLints) {
|
|
146
|
+
args.push('--strict-dom-lints');
|
|
147
|
+
}
|
|
89
148
|
const opts = { encoding: 'utf8' };
|
|
90
149
|
if (stdinSource !== undefined) {
|
|
91
150
|
opts.input = stdinSource;
|
|
@@ -102,6 +161,20 @@ function runCompiler(filePath, stdinSource, compilerOpts = {}) {
|
|
|
102
161
|
);
|
|
103
162
|
}
|
|
104
163
|
|
|
164
|
+
if (result.stderr && result.stderr.trim().length > 0 && compilerRunOptions.suppressWarnings !== true) {
|
|
165
|
+
const lines = String(result.stderr)
|
|
166
|
+
.split('\n')
|
|
167
|
+
.map((line) => line.trim())
|
|
168
|
+
.filter((line) => line.length > 0);
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
if (typeof compilerRunOptions.onWarning === 'function') {
|
|
171
|
+
compilerRunOptions.onWarning(line);
|
|
172
|
+
} else {
|
|
173
|
+
console.warn(line);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
105
178
|
try {
|
|
106
179
|
return JSON.parse(result.stdout);
|
|
107
180
|
} catch (err) {
|
|
@@ -143,7 +216,7 @@ function buildComponentExpressionRewrite(compPath, componentSource, compIr, comp
|
|
|
143
216
|
|
|
144
217
|
let templateIr;
|
|
145
218
|
try {
|
|
146
|
-
templateIr = runCompiler(compPath, templateOnly, compilerOpts);
|
|
219
|
+
templateIr = runCompiler(compPath, templateOnly, compilerOpts, { suppressWarnings: true });
|
|
147
220
|
} catch {
|
|
148
221
|
return out;
|
|
149
222
|
}
|
|
@@ -200,6 +273,57 @@ function mergeExpressionRewriteMaps(pageMap, pageAmbiguous, componentRewrite) {
|
|
|
200
273
|
}
|
|
201
274
|
}
|
|
202
275
|
|
|
276
|
+
function resolveStateKeyFromBindings(identifier, stateBindings, preferredKeys = null) {
|
|
277
|
+
const ident = String(identifier || '').trim();
|
|
278
|
+
if (!ident) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const exact = stateBindings.find((entry) => String(entry?.key || '') === ident);
|
|
283
|
+
if (exact && typeof exact.key === 'string') {
|
|
284
|
+
return exact.key;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const suffix = `_${ident}`;
|
|
288
|
+
const matches = stateBindings
|
|
289
|
+
.map((entry) => String(entry?.key || ''))
|
|
290
|
+
.filter((key) => key.endsWith(suffix));
|
|
291
|
+
|
|
292
|
+
if (preferredKeys instanceof Set && preferredKeys.size > 0) {
|
|
293
|
+
const preferredMatches = matches.filter((key) => preferredKeys.has(key));
|
|
294
|
+
if (preferredMatches.length === 1) {
|
|
295
|
+
return preferredMatches[0];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (matches.length === 1) {
|
|
300
|
+
return matches[0];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function rewriteRefBindingIdentifiers(pageIr, preferredKeys = null) {
|
|
307
|
+
if (!Array.isArray(pageIr?.ref_bindings) || pageIr.ref_bindings.length === 0) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const stateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
|
|
312
|
+
if (stateBindings.length === 0) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const binding of pageIr.ref_bindings) {
|
|
317
|
+
if (!binding || typeof binding !== 'object' || typeof binding.identifier !== 'string') {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const resolved = resolveStateKeyFromBindings(binding.identifier, stateBindings, preferredKeys);
|
|
321
|
+
if (resolved) {
|
|
322
|
+
binding.identifier = resolved;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
203
327
|
/**
|
|
204
328
|
* Rewrite unresolved page expressions using component script-aware mappings.
|
|
205
329
|
*
|
|
@@ -232,6 +356,9 @@ function applyExpressionRewrites(pageIr, expressionMap, ambiguous) {
|
|
|
232
356
|
bindings[index].literal === current
|
|
233
357
|
) {
|
|
234
358
|
bindings[index].literal = rewritten;
|
|
359
|
+
if (bindings[index].compiled_expr === current) {
|
|
360
|
+
bindings[index].compiled_expr = rewritten;
|
|
361
|
+
}
|
|
235
362
|
}
|
|
236
363
|
}
|
|
237
364
|
}
|
|
@@ -271,6 +398,15 @@ function rewriteLegacyMarkupIdentifiers(pageIr) {
|
|
|
271
398
|
_LEGACY_MARKUP_RE.lastIndex = 0;
|
|
272
399
|
bindings[i].literal = bindings[i].literal.replace(_LEGACY_MARKUP_RE, '__ZENITH_INTERNAL_ZENHTML');
|
|
273
400
|
}
|
|
401
|
+
if (
|
|
402
|
+
bindings[i] &&
|
|
403
|
+
typeof bindings[i] === 'object' &&
|
|
404
|
+
typeof bindings[i].compiled_expr === 'string' &&
|
|
405
|
+
bindings[i].compiled_expr.includes(_LEGACY_MARKUP_IDENT)
|
|
406
|
+
) {
|
|
407
|
+
_LEGACY_MARKUP_RE.lastIndex = 0;
|
|
408
|
+
bindings[i].compiled_expr = bindings[i].compiled_expr.replace(_LEGACY_MARKUP_RE, '__ZENITH_INTERNAL_ZENHTML');
|
|
409
|
+
}
|
|
274
410
|
}
|
|
275
411
|
}
|
|
276
412
|
|
|
@@ -452,7 +588,7 @@ function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
|
452
588
|
const scriptRe = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
453
589
|
const serverMatches = [];
|
|
454
590
|
const reservedServerExportRe =
|
|
455
|
-
/\bexport\s+const\s+(?:data|prerender)\b|\bexport\s+(?:async\s+)?function\s+load\s*\(|\bexport\s+const\s+load\s*=/;
|
|
591
|
+
/\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*=/;
|
|
456
592
|
|
|
457
593
|
for (const match of source.matchAll(scriptRe)) {
|
|
458
594
|
const attrs = String(match[1] || '');
|
|
@@ -463,8 +599,8 @@ function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
|
463
599
|
throw new Error(
|
|
464
600
|
`Zenith server script contract violation:\n` +
|
|
465
601
|
` File: ${sourceFile}\n` +
|
|
466
|
-
` Reason:
|
|
467
|
-
` Example: move
|
|
602
|
+
` Reason: guard/load/data exports are only allowed in <script server lang="ts"> or adjacent .guard.ts / .load.ts files\n` +
|
|
603
|
+
` Example: move the export into <script server lang="ts">`
|
|
468
604
|
);
|
|
469
605
|
}
|
|
470
606
|
|
|
@@ -535,6 +671,25 @@ function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
|
535
671
|
);
|
|
536
672
|
}
|
|
537
673
|
|
|
674
|
+
const guardFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+guard\s*\(([^)]*)\)/);
|
|
675
|
+
const guardConstParenMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
|
|
676
|
+
const guardConstSingleArgMatch = serverSource.match(
|
|
677
|
+
/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/
|
|
678
|
+
);
|
|
679
|
+
const hasGuard = Boolean(guardFnMatch || guardConstParenMatch || guardConstSingleArgMatch);
|
|
680
|
+
const guardMatchCount =
|
|
681
|
+
Number(Boolean(guardFnMatch)) +
|
|
682
|
+
Number(Boolean(guardConstParenMatch)) +
|
|
683
|
+
Number(Boolean(guardConstSingleArgMatch));
|
|
684
|
+
if (guardMatchCount > 1) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Zenith server script contract violation:\n` +
|
|
687
|
+
` File: ${sourceFile}\n` +
|
|
688
|
+
` Reason: multiple guard exports detected\n` +
|
|
689
|
+
` Example: keep exactly one export const guard = async (ctx) => ({ ... })`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
538
693
|
const hasData = /\bexport\s+const\s+data\b/.test(serverSource);
|
|
539
694
|
const hasSsrData = /\bexport\s+const\s+ssr_data\b/.test(serverSource);
|
|
540
695
|
const hasSsr = /\bexport\s+const\s+ssr\b/.test(serverSource);
|
|
@@ -575,6 +730,24 @@ function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
|
575
730
|
}
|
|
576
731
|
}
|
|
577
732
|
|
|
733
|
+
if (hasGuard) {
|
|
734
|
+
const singleArg = String(guardConstSingleArgMatch?.[1] || '').trim();
|
|
735
|
+
const paramsText = String((guardFnMatch || guardConstParenMatch)?.[1] || '').trim();
|
|
736
|
+
const arity = singleArg
|
|
737
|
+
? 1
|
|
738
|
+
: paramsText.length === 0
|
|
739
|
+
? 0
|
|
740
|
+
: paramsText.split(',').length;
|
|
741
|
+
if (arity !== 1) {
|
|
742
|
+
throw new Error(
|
|
743
|
+
`Zenith server script contract violation:\n` +
|
|
744
|
+
` File: ${sourceFile}\n` +
|
|
745
|
+
` Reason: guard(ctx) must accept exactly one argument\n` +
|
|
746
|
+
` Example: export const guard = async (ctx) => ({ ... })`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
578
751
|
const prerenderMatch = serverSource.match(/\bexport\s+const\s+prerender\s*=\s*([^\n;]+)/);
|
|
579
752
|
let prerender = false;
|
|
580
753
|
if (prerenderMatch) {
|
|
@@ -596,6 +769,8 @@ function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
|
596
769
|
serverScript: {
|
|
597
770
|
source: serverSource,
|
|
598
771
|
prerender,
|
|
772
|
+
has_guard: hasGuard,
|
|
773
|
+
has_load: hasLoad,
|
|
599
774
|
source_path: sourceFile
|
|
600
775
|
}
|
|
601
776
|
};
|
|
@@ -609,6 +784,8 @@ function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
|
609
784
|
serverScript: {
|
|
610
785
|
source: serverSource,
|
|
611
786
|
prerender,
|
|
787
|
+
has_guard: hasGuard,
|
|
788
|
+
has_load: hasLoad,
|
|
612
789
|
source_path: sourceFile
|
|
613
790
|
}
|
|
614
791
|
};
|
|
@@ -654,7 +831,7 @@ function collectComponentUsageAttrs(source, registry) {
|
|
|
654
831
|
* @param {{ includeCode: boolean, cssImportsOnly: boolean, documentMode?: boolean, componentAttrs?: string }} options
|
|
655
832
|
* @param {Set<string>} seenStaticImports
|
|
656
833
|
*/
|
|
657
|
-
function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStaticImports) {
|
|
834
|
+
function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStaticImports, knownRefKeys = null) {
|
|
658
835
|
// Merge components_scripts
|
|
659
836
|
if (compIr.components_scripts) {
|
|
660
837
|
for (const [hoistId, script] of Object.entries(compIr.components_scripts)) {
|
|
@@ -669,6 +846,17 @@ function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStati
|
|
|
669
846
|
pageIr.component_instances.push(...compIr.component_instances);
|
|
670
847
|
}
|
|
671
848
|
|
|
849
|
+
if (knownRefKeys instanceof Set && Array.isArray(compIr.ref_bindings)) {
|
|
850
|
+
const componentStateBindings = Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [];
|
|
851
|
+
for (const binding of compIr.ref_bindings) {
|
|
852
|
+
if (!binding || typeof binding.identifier !== 'string' || binding.identifier.length === 0) {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const resolved = resolveStateKeyFromBindings(binding.identifier, componentStateBindings);
|
|
856
|
+
knownRefKeys.add(resolved || binding.identifier);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
672
860
|
// Merge hoisted imports (deduplicated, rebased to the page file path)
|
|
673
861
|
if (compIr.hoisted?.imports?.length) {
|
|
674
862
|
for (const imp of compIr.hoisted.imports) {
|
|
@@ -741,11 +929,7 @@ function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStati
|
|
|
741
929
|
: rebased;
|
|
742
930
|
const withPropsPrelude = injectPropsPrelude(filteredImports, options.componentAttrs || '');
|
|
743
931
|
const transpiled = transpileTypeScriptToJs(withPropsPrelude, compPath);
|
|
744
|
-
const
|
|
745
|
-
transpiled,
|
|
746
|
-
options.refFallbacks || []
|
|
747
|
-
);
|
|
748
|
-
const deduped = dedupeStaticImportsInSource(withRefFallbacks, seenStaticImports);
|
|
932
|
+
const deduped = dedupeStaticImportsInSource(transpiled, seenStaticImports);
|
|
749
933
|
const deferred = deferComponentRuntimeBlock(deduped);
|
|
750
934
|
if (deferred.trim().length > 0 && !pageIr.hoisted.code.includes(deferred)) {
|
|
751
935
|
pageIr.hoisted.code.push(deferred);
|
|
@@ -868,7 +1052,7 @@ function transpileTypeScriptToJs(source, sourceFile) {
|
|
|
868
1052
|
fileName: sourceFile,
|
|
869
1053
|
compilerOptions: {
|
|
870
1054
|
module: ts.ModuleKind.ESNext,
|
|
871
|
-
target: ts.ScriptTarget.
|
|
1055
|
+
target: ts.ScriptTarget.ES5,
|
|
872
1056
|
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
|
|
873
1057
|
verbatimModuleSyntax: true,
|
|
874
1058
|
newLine: ts.NewLineKind.LineFeed,
|
|
@@ -1103,7 +1287,7 @@ function injectPropsPrelude(source, attrs) {
|
|
|
1103
1287
|
}
|
|
1104
1288
|
|
|
1105
1289
|
const propsLiteral = renderPropsLiteralFromAttrs(attrs);
|
|
1106
|
-
return `
|
|
1290
|
+
return `var props = ${propsLiteral};\n${source}`;
|
|
1107
1291
|
}
|
|
1108
1292
|
|
|
1109
1293
|
/**
|
|
@@ -1155,111 +1339,37 @@ function deferComponentRuntimeBlock(source) {
|
|
|
1155
1339
|
return wrapped;
|
|
1156
1340
|
}
|
|
1157
1341
|
|
|
1158
|
-
/**
|
|
1159
|
-
* @param {string} componentSource
|
|
1160
|
-
* @returns {Array<{ identifier: string, selector: string }>}
|
|
1161
|
-
*/
|
|
1162
|
-
function extractRefFallbackAssignments(componentSource) {
|
|
1163
|
-
const template = extractTemplate(componentSource);
|
|
1164
|
-
const tagRe = /<[^>]*ref=\{([A-Za-z_$][A-Za-z0-9_$]*)\}[^>]*>/g;
|
|
1165
|
-
const out = [];
|
|
1166
|
-
const seen = new Set();
|
|
1167
|
-
|
|
1168
|
-
let match;
|
|
1169
|
-
while ((match = tagRe.exec(template)) !== null) {
|
|
1170
|
-
const tag = match[0];
|
|
1171
|
-
const identifier = match[1];
|
|
1172
|
-
const attrMatch = tag.match(/\b(data-[a-z0-9-]+-runtime)\b/i)
|
|
1173
|
-
|| tag.match(/\b(data-[a-z0-9-]+)\b/i);
|
|
1174
|
-
if (!attrMatch) {
|
|
1175
|
-
continue;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
const selector = `[${attrMatch[1]}]`;
|
|
1179
|
-
const key = `${identifier}:${selector}`;
|
|
1180
|
-
if (seen.has(key)) {
|
|
1181
|
-
continue;
|
|
1182
|
-
}
|
|
1183
|
-
seen.add(key);
|
|
1184
|
-
out.push({ identifier, selector });
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
return out;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* @param {string} source
|
|
1192
|
-
* @param {string} originalIdentifier
|
|
1193
|
-
* @returns {string | null}
|
|
1194
|
-
*/
|
|
1195
|
-
function resolveRenamedRefIdentifier(source, originalIdentifier) {
|
|
1196
|
-
const re = /const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*ref\s*\(/g;
|
|
1197
|
-
let match;
|
|
1198
|
-
while ((match = re.exec(source)) !== null) {
|
|
1199
|
-
const candidate = match[1];
|
|
1200
|
-
if (candidate === originalIdentifier || candidate.endsWith(`_${originalIdentifier}`)) {
|
|
1201
|
-
return candidate;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
return null;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
/**
|
|
1208
|
-
* @param {string} source
|
|
1209
|
-
* @param {Array<{ identifier: string, selector: string }>} refFallbacks
|
|
1210
|
-
* @returns {string}
|
|
1211
|
-
*/
|
|
1212
|
-
function injectRefFallbacksInZenMount(source, refFallbacks) {
|
|
1213
|
-
if (!Array.isArray(refFallbacks) || refFallbacks.length === 0) {
|
|
1214
|
-
return source;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const lines = [];
|
|
1218
|
-
for (let i = 0; i < refFallbacks.length; i++) {
|
|
1219
|
-
const fallback = refFallbacks[i];
|
|
1220
|
-
const refIdentifier = resolveRenamedRefIdentifier(source, fallback.identifier);
|
|
1221
|
-
if (!refIdentifier) {
|
|
1222
|
-
continue;
|
|
1223
|
-
}
|
|
1224
|
-
const selector = JSON.stringify(fallback.selector);
|
|
1225
|
-
const nodeVar = `__zenith_ref_node_${i}`;
|
|
1226
|
-
lines.push(
|
|
1227
|
-
` if (typeof document !== 'undefined' && ${refIdentifier} && !${refIdentifier}.current) {`,
|
|
1228
|
-
` const ${nodeVar} = document.querySelector(${selector});`,
|
|
1229
|
-
` if (${nodeVar}) {`,
|
|
1230
|
-
` ${refIdentifier}.current = ${nodeVar};`,
|
|
1231
|
-
' }',
|
|
1232
|
-
' }'
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
if (lines.length === 0) {
|
|
1237
|
-
return source;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
const mountRe = /zenMount\s*\(\s*\([^)]*\)\s*=>\s*\{/;
|
|
1241
|
-
if (!mountRe.test(source)) {
|
|
1242
|
-
return source;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
return source.replace(mountRe, (match) => `${match}\n${lines.join('\n')}\n`);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
1342
|
/**
|
|
1249
1343
|
* Run bundler process for one page envelope.
|
|
1250
1344
|
*
|
|
1251
1345
|
* @param {object|object[]} envelope
|
|
1252
1346
|
* @param {string} outDir
|
|
1347
|
+
* @param {string} projectRoot
|
|
1348
|
+
* @param {object | null} [logger]
|
|
1349
|
+
* @param {boolean} [showInfo]
|
|
1253
1350
|
* @returns {Promise<void>}
|
|
1254
1351
|
*/
|
|
1255
|
-
function runBundler(envelope, outDir) {
|
|
1352
|
+
function runBundler(envelope, outDir, projectRoot, logger = null, showInfo = true) {
|
|
1256
1353
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
1354
|
+
const useStructuredLogger = Boolean(logger && typeof logger.childLine === 'function');
|
|
1257
1355
|
const child = spawn(
|
|
1258
|
-
|
|
1356
|
+
getBundlerBin(),
|
|
1259
1357
|
['--out-dir', outDir],
|
|
1260
|
-
{
|
|
1358
|
+
{
|
|
1359
|
+
cwd: projectRoot,
|
|
1360
|
+
stdio: useStructuredLogger ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'inherit', 'inherit']
|
|
1361
|
+
}
|
|
1261
1362
|
);
|
|
1262
1363
|
|
|
1364
|
+
if (useStructuredLogger) {
|
|
1365
|
+
forwardStreamLines(child.stdout, (line) => {
|
|
1366
|
+
logger.childLine('bundler', line, { stream: 'stdout', showInfo });
|
|
1367
|
+
});
|
|
1368
|
+
forwardStreamLines(child.stderr, (line) => {
|
|
1369
|
+
logger.childLine('bundler', line, { stream: 'stderr', showInfo: true });
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1263
1373
|
child.on('error', (err) => {
|
|
1264
1374
|
rejectPromise(new Error(`Bundler spawn failed: ${err.message}`));
|
|
1265
1375
|
});
|
|
@@ -1325,15 +1435,17 @@ async function collectAssets(rootDir) {
|
|
|
1325
1435
|
* d. Merge component IRs into page IR
|
|
1326
1436
|
* 3. Send all envelopes to bundler
|
|
1327
1437
|
*
|
|
1328
|
-
* @param {{ pagesDir: string, outDir: string, config?: object }} options
|
|
1438
|
+
* @param {{ pagesDir: string, outDir: string, config?: object, logger?: object | null, showBundlerInfo?: boolean }} options
|
|
1329
1439
|
* @returns {Promise<{ pages: number, assets: string[] }>}
|
|
1330
1440
|
*/
|
|
1331
1441
|
export async function build(options) {
|
|
1332
|
-
const { pagesDir, outDir, config = {} } = options;
|
|
1442
|
+
const { pagesDir, outDir, config = {}, logger = null, showBundlerInfo = true } = options;
|
|
1443
|
+
const projectRoot = deriveProjectRootFromPagesDir(pagesDir);
|
|
1333
1444
|
const softNavigationEnabled = config.softNavigation === true || config.router === true;
|
|
1334
1445
|
const compilerOpts = {
|
|
1335
1446
|
typescriptDefault: config.typescriptDefault === true,
|
|
1336
|
-
experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true || config.experimental?.embeddedMarkupExpressions === true
|
|
1447
|
+
experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true || config.experimental?.embeddedMarkupExpressions === true,
|
|
1448
|
+
strictDomLints: config.strictDomLints === true
|
|
1337
1449
|
};
|
|
1338
1450
|
|
|
1339
1451
|
await rm(outDir, { recursive: true, force: true });
|
|
@@ -1345,7 +1457,13 @@ export async function build(options) {
|
|
|
1345
1457
|
// 1. Build component registry
|
|
1346
1458
|
const registry = buildComponentRegistry(srcDir);
|
|
1347
1459
|
if (registry.size > 0) {
|
|
1348
|
-
|
|
1460
|
+
if (logger && typeof logger.build === 'function') {
|
|
1461
|
+
logger.build(`registry=${registry.size} components`, {
|
|
1462
|
+
onceKey: `component-registry:${registry.size}`
|
|
1463
|
+
});
|
|
1464
|
+
} else {
|
|
1465
|
+
console.log(`[zenith] Component registry: ${registry.size} components`);
|
|
1466
|
+
}
|
|
1349
1467
|
}
|
|
1350
1468
|
|
|
1351
1469
|
const manifest = await generateManifest(pagesDir);
|
|
@@ -1356,10 +1474,15 @@ export async function build(options) {
|
|
|
1356
1474
|
const componentIrCache = new Map();
|
|
1357
1475
|
/** @type {Map<string, boolean>} */
|
|
1358
1476
|
const componentDocumentModeCache = new Map();
|
|
1359
|
-
/** @type {Map<string, Array<{ identifier: string, selector: string }>>} */
|
|
1360
|
-
const componentRefFallbackCache = new Map();
|
|
1361
1477
|
/** @type {Map<string, { map: Map<string, string>, ambiguous: Set<string> }>} */
|
|
1362
1478
|
const componentExpressionRewriteCache = new Map();
|
|
1479
|
+
const emitCompilerWarning = createCompilerWarningEmitter((line) => {
|
|
1480
|
+
if (logger && typeof logger.warn === 'function') {
|
|
1481
|
+
logger.warn(line, { onceKey: `compiler-warning:${line}` });
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
console.warn(line);
|
|
1485
|
+
});
|
|
1363
1486
|
|
|
1364
1487
|
const envelopes = [];
|
|
1365
1488
|
for (const entry of manifest) {
|
|
@@ -1367,6 +1490,14 @@ export async function build(options) {
|
|
|
1367
1490
|
const rawSource = readFileSync(sourceFile, 'utf8');
|
|
1368
1491
|
const componentUsageAttrs = collectComponentUsageAttrs(rawSource, registry);
|
|
1369
1492
|
|
|
1493
|
+
const baseName = sourceFile.slice(0, -extname(sourceFile).length);
|
|
1494
|
+
let adjacentGuard = null;
|
|
1495
|
+
let adjacentLoad = null;
|
|
1496
|
+
for (const ext of ['.ts', '.js']) {
|
|
1497
|
+
if (!adjacentGuard && existsSync(`${baseName}.guard${ext}`)) adjacentGuard = `${baseName}.guard${ext}`;
|
|
1498
|
+
if (!adjacentLoad && existsSync(`${baseName}.load${ext}`)) adjacentLoad = `${baseName}.load${ext}`;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1370
1501
|
// 2a. Expand PascalCase component tags
|
|
1371
1502
|
const { expandedSource, usedComponents } = expandComponents(
|
|
1372
1503
|
rawSource, registry, sourceFile
|
|
@@ -1375,7 +1506,16 @@ export async function build(options) {
|
|
|
1375
1506
|
const compileSource = extractedServer.source;
|
|
1376
1507
|
|
|
1377
1508
|
// 2b. Compile expanded page source via --stdin
|
|
1378
|
-
const pageIr = runCompiler(
|
|
1509
|
+
const pageIr = runCompiler(
|
|
1510
|
+
sourceFile,
|
|
1511
|
+
compileSource,
|
|
1512
|
+
compilerOpts,
|
|
1513
|
+
{ onWarning: emitCompilerWarning }
|
|
1514
|
+
);
|
|
1515
|
+
|
|
1516
|
+
const hasGuard = (extractedServer.serverScript && extractedServer.serverScript.has_guard) || adjacentGuard !== null;
|
|
1517
|
+
const hasLoad = (extractedServer.serverScript && extractedServer.serverScript.has_load) || adjacentLoad !== null;
|
|
1518
|
+
|
|
1379
1519
|
if (extractedServer.serverScript) {
|
|
1380
1520
|
pageIr.server_script = extractedServer.serverScript;
|
|
1381
1521
|
pageIr.prerender = extractedServer.serverScript.prerender === true;
|
|
@@ -1384,6 +1524,20 @@ export async function build(options) {
|
|
|
1384
1524
|
}
|
|
1385
1525
|
}
|
|
1386
1526
|
|
|
1527
|
+
// Static Build Route Protection Policy
|
|
1528
|
+
if (pageIr.prerender === true && (hasGuard || hasLoad)) {
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
`[zenith] Build failed for ${entry.file}: protected routes require SSR/runtime. ` +
|
|
1531
|
+
`Cannot prerender a static route with a \`guard\` or \`load\` function.`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Apply metadata to IR
|
|
1536
|
+
pageIr.has_guard = hasGuard;
|
|
1537
|
+
pageIr.has_load = hasLoad;
|
|
1538
|
+
pageIr.guard_module_ref = adjacentGuard ? relative(srcDir, adjacentGuard).replaceAll('\\', '/') : null;
|
|
1539
|
+
pageIr.load_module_ref = adjacentLoad ? relative(srcDir, adjacentLoad).replaceAll('\\', '/') : null;
|
|
1540
|
+
|
|
1387
1541
|
// Ensure IR has required array fields for merging
|
|
1388
1542
|
pageIr.components_scripts = pageIr.components_scripts || {};
|
|
1389
1543
|
pageIr.component_instances = pageIr.component_instances || [];
|
|
@@ -1397,6 +1551,7 @@ export async function build(options) {
|
|
|
1397
1551
|
const seenStaticImports = new Set();
|
|
1398
1552
|
const pageExpressionRewriteMap = new Map();
|
|
1399
1553
|
const pageAmbiguousExpressionMap = new Set();
|
|
1554
|
+
const knownRefKeys = new Set();
|
|
1400
1555
|
|
|
1401
1556
|
// 2c. Compile each used component separately for its script IR
|
|
1402
1557
|
for (const compName of usedComponents) {
|
|
@@ -1409,17 +1564,19 @@ export async function build(options) {
|
|
|
1409
1564
|
compIr = componentIrCache.get(compPath);
|
|
1410
1565
|
} else {
|
|
1411
1566
|
const componentCompileSource = stripStyleBlocks(componentSource);
|
|
1412
|
-
compIr = runCompiler(
|
|
1567
|
+
compIr = runCompiler(
|
|
1568
|
+
compPath,
|
|
1569
|
+
componentCompileSource,
|
|
1570
|
+
compilerOpts,
|
|
1571
|
+
{ onWarning: emitCompilerWarning }
|
|
1572
|
+
);
|
|
1413
1573
|
componentIrCache.set(compPath, compIr);
|
|
1414
1574
|
}
|
|
1415
1575
|
|
|
1416
1576
|
let isDocMode = componentDocumentModeCache.get(compPath);
|
|
1417
|
-
let refFallbacks = componentRefFallbackCache.get(compPath);
|
|
1418
1577
|
if (isDocMode === undefined) {
|
|
1419
1578
|
isDocMode = isDocumentMode(extractTemplate(componentSource));
|
|
1420
|
-
refFallbacks = extractRefFallbackAssignments(componentSource);
|
|
1421
1579
|
componentDocumentModeCache.set(compPath, isDocMode);
|
|
1422
|
-
componentRefFallbackCache.set(compPath, refFallbacks);
|
|
1423
1580
|
}
|
|
1424
1581
|
|
|
1425
1582
|
let expressionRewrite = componentExpressionRewriteCache.get(compPath);
|
|
@@ -1443,10 +1600,10 @@ export async function build(options) {
|
|
|
1443
1600
|
includeCode: true,
|
|
1444
1601
|
cssImportsOnly: isDocMode,
|
|
1445
1602
|
documentMode: isDocMode,
|
|
1446
|
-
componentAttrs: (componentUsageAttrs.get(compName) || [])[0] || ''
|
|
1447
|
-
refFallbacks: refFallbacks || []
|
|
1603
|
+
componentAttrs: (componentUsageAttrs.get(compName) || [])[0] || ''
|
|
1448
1604
|
},
|
|
1449
|
-
seenStaticImports
|
|
1605
|
+
seenStaticImports,
|
|
1606
|
+
knownRefKeys
|
|
1450
1607
|
);
|
|
1451
1608
|
}
|
|
1452
1609
|
|
|
@@ -1457,6 +1614,7 @@ export async function build(options) {
|
|
|
1457
1614
|
);
|
|
1458
1615
|
|
|
1459
1616
|
rewriteLegacyMarkupIdentifiers(pageIr);
|
|
1617
|
+
rewriteRefBindingIdentifiers(pageIr, knownRefKeys);
|
|
1460
1618
|
|
|
1461
1619
|
envelopes.push({
|
|
1462
1620
|
route: entry.path,
|
|
@@ -1467,7 +1625,7 @@ export async function build(options) {
|
|
|
1467
1625
|
}
|
|
1468
1626
|
|
|
1469
1627
|
if (envelopes.length > 0) {
|
|
1470
|
-
|
|
1628
|
+
await runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo);
|
|
1471
1629
|
}
|
|
1472
1630
|
|
|
1473
1631
|
const assets = await collectAssets(outDir);
|