@zenithbuild/cli 0.4.11 → 0.5.0-beta.2.15
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 +8 -0
- package/dist/build.js +1521 -0
- package/dist/dev-server.js +278 -0
- package/dist/index.js +175 -0
- package/dist/manifest.js +273 -0
- package/dist/preview.js +866 -0
- package/dist/resolve-components.js +490 -0
- package/dist/server/resolve-request-route.js +169 -0
- package/dist/server-contract.js +278 -0
- package/dist/types/generate-env-dts.js +52 -0
- package/dist/types/generate-routes-dts.js +22 -0
- package/dist/types/index.js +34 -0
- package/dist/ui/env.js +41 -0
- package/dist/ui/format.js +172 -0
- package/dist/ui/logger.js +105 -0
- package/package.json +21 -49
- package/bin/zen-build.ts +0 -2
- package/bin/zen-dev.ts +0 -2
- package/bin/zen-preview.ts +0 -2
- package/bin/zenith.ts +0 -2
- package/dist/zen-build.js +0 -9622
- package/dist/zen-dev.js +0 -9622
- package/dist/zen-preview.js +0 -9622
- package/dist/zenith.js +0 -9622
- package/src/commands/add.ts +0 -37
- package/src/commands/build.ts +0 -36
- package/src/commands/create.ts +0 -702
- package/src/commands/dev.ts +0 -472
- package/src/commands/index.ts +0 -112
- package/src/commands/preview.ts +0 -62
- package/src/commands/remove.ts +0 -33
- package/src/index.ts +0 -10
- package/src/main.ts +0 -101
- package/src/utils/branding.ts +0 -178
- package/src/utils/logger.ts +0 -52
- package/src/utils/plugin-manager.ts +0 -114
- package/src/utils/project.ts +0 -77
package/dist/build.js
ADDED
|
@@ -0,0 +1,1521 @@
|
|
|
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
|
+
|
|
14
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { generateManifest } from './manifest.js';
|
|
21
|
+
import { buildComponentRegistry, expandComponents, extractTemplate, isDocumentMode } from './resolve-components.js';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
const CLI_ROOT = resolve(__dirname, '..');
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
let cachedTypeScript = undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @returns {import('typescript') | null}
|
|
31
|
+
*/
|
|
32
|
+
function loadTypeScriptApi() {
|
|
33
|
+
if (cachedTypeScript === undefined) {
|
|
34
|
+
try {
|
|
35
|
+
cachedTypeScript = require('typescript');
|
|
36
|
+
} catch {
|
|
37
|
+
cachedTypeScript = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return cachedTypeScript;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a binary path from deterministic candidates.
|
|
45
|
+
*
|
|
46
|
+
* Supports both repository layout (../zenith-*) and installed package layout
|
|
47
|
+
* under node_modules/@zenithbuild (../compiler, ../bundler).
|
|
48
|
+
*
|
|
49
|
+
* @param {string[]} candidates
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function resolveBinary(candidates) {
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
if (existsSync(candidate)) {
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return candidates[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const COMPILER_BIN = resolveBinary([
|
|
62
|
+
resolve(CLI_ROOT, '../compiler/target/release/zenith-compiler'),
|
|
63
|
+
resolve(CLI_ROOT, '../zenith-compiler/target/release/zenith-compiler')
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const BUNDLER_BIN = resolveBinary([
|
|
67
|
+
resolve(CLI_ROOT, '../bundler/target/release/zenith-bundler'),
|
|
68
|
+
resolve(CLI_ROOT, '../zenith-bundler/target/release/zenith-bundler')
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a per-build warning emitter that deduplicates repeated compiler lines.
|
|
73
|
+
*
|
|
74
|
+
* @param {(line: string) => void} sink
|
|
75
|
+
* @returns {(line: string) => void}
|
|
76
|
+
*/
|
|
77
|
+
export function createCompilerWarningEmitter(sink = (line) => console.warn(line)) {
|
|
78
|
+
const emitted = new Set();
|
|
79
|
+
return (line) => {
|
|
80
|
+
const text = String(line || '').trim();
|
|
81
|
+
if (!text || emitted.has(text)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
emitted.add(text);
|
|
85
|
+
sink(text);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run the compiler process and parse its JSON stdout.
|
|
91
|
+
*
|
|
92
|
+
* If `stdinSource` is provided, pipes it to the compiler via stdin
|
|
93
|
+
* and passes `--stdin` so the compiler reads from stdin instead of the file.
|
|
94
|
+
* The `filePath` argument is always used as the source_path for diagnostics.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} filePath — path for diagnostics (and file reading when no stdinSource)
|
|
97
|
+
* @param {string} [stdinSource] — if provided, piped to compiler via stdin
|
|
98
|
+
* @param {object} compilerRunOptions
|
|
99
|
+
* @param {(warning: string) => void} [compilerRunOptions.onWarning]
|
|
100
|
+
* @param {boolean} [compilerRunOptions.suppressWarnings]
|
|
101
|
+
* @returns {object}
|
|
102
|
+
*/
|
|
103
|
+
function runCompiler(filePath, stdinSource, compilerOpts = {}, compilerRunOptions = {}) {
|
|
104
|
+
const args = stdinSource !== undefined
|
|
105
|
+
? ['--stdin', filePath]
|
|
106
|
+
: [filePath];
|
|
107
|
+
if (compilerOpts?.experimentalEmbeddedMarkup) {
|
|
108
|
+
args.push('--embedded-markup-expressions');
|
|
109
|
+
}
|
|
110
|
+
const opts = { encoding: 'utf8' };
|
|
111
|
+
if (stdinSource !== undefined) {
|
|
112
|
+
opts.input = stdinSource;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = spawnSync(COMPILER_BIN, args, opts);
|
|
116
|
+
|
|
117
|
+
if (result.error) {
|
|
118
|
+
throw new Error(`Compiler spawn failed for ${filePath}: ${result.error.message}`);
|
|
119
|
+
}
|
|
120
|
+
if (result.status !== 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Compiler failed for ${filePath} with exit code ${result.status}\n${result.stderr || ''}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (result.stderr && result.stderr.trim().length > 0 && compilerRunOptions.suppressWarnings !== true) {
|
|
127
|
+
const lines = String(result.stderr)
|
|
128
|
+
.split('\n')
|
|
129
|
+
.map((line) => line.trim())
|
|
130
|
+
.filter((line) => line.length > 0);
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (typeof compilerRunOptions.onWarning === 'function') {
|
|
133
|
+
compilerRunOptions.onWarning(line);
|
|
134
|
+
} else {
|
|
135
|
+
console.warn(line);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(result.stdout);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new Error(`Compiler emitted invalid JSON: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Strip component <style> blocks before script-only component IR compilation.
|
|
149
|
+
* Component style emission is handled by page compilation/bundler paths.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} source
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
function stripStyleBlocks(source) {
|
|
155
|
+
return String(source || '').replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a deterministic raw->rewritten expression map for a component by
|
|
160
|
+
* comparing template-only expressions with script-aware expressions.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} compPath
|
|
163
|
+
* @param {string} componentSource
|
|
164
|
+
* @param {object} compIr
|
|
165
|
+
* @returns {{ map: Map<string, string>, ambiguous: Set<string> }}
|
|
166
|
+
*/
|
|
167
|
+
function buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts) {
|
|
168
|
+
const out = { map: new Map(), ambiguous: new Set() };
|
|
169
|
+
const rewrittenExpressions = Array.isArray(compIr?.expressions) ? compIr.expressions : [];
|
|
170
|
+
if (rewrittenExpressions.length === 0) {
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const templateOnly = extractTemplate(componentSource);
|
|
175
|
+
if (!templateOnly.trim()) {
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let templateIr;
|
|
180
|
+
try {
|
|
181
|
+
templateIr = runCompiler(compPath, templateOnly, compilerOpts, { suppressWarnings: true });
|
|
182
|
+
} catch {
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rawExpressions = Array.isArray(templateIr?.expressions) ? templateIr.expressions : [];
|
|
187
|
+
const count = Math.min(rawExpressions.length, rewrittenExpressions.length);
|
|
188
|
+
for (let i = 0; i < count; i++) {
|
|
189
|
+
const raw = rawExpressions[i];
|
|
190
|
+
const rewritten = rewrittenExpressions[i];
|
|
191
|
+
if (typeof raw !== 'string' || typeof rewritten !== 'string') {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (raw === rewritten) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const existing = out.map.get(raw);
|
|
198
|
+
if (existing && existing !== rewritten) {
|
|
199
|
+
out.map.delete(raw);
|
|
200
|
+
out.ambiguous.add(raw);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!out.ambiguous.has(raw)) {
|
|
204
|
+
out.map.set(raw, rewritten);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Merge a per-component rewrite table into the page-level rewrite table.
|
|
213
|
+
*
|
|
214
|
+
* @param {Map<string, string>} pageMap
|
|
215
|
+
* @param {Set<string>} pageAmbiguous
|
|
216
|
+
* @param {{ map: Map<string, string>, ambiguous: Set<string> }} componentRewrite
|
|
217
|
+
*/
|
|
218
|
+
function mergeExpressionRewriteMaps(pageMap, pageAmbiguous, componentRewrite) {
|
|
219
|
+
for (const raw of componentRewrite.ambiguous) {
|
|
220
|
+
pageAmbiguous.add(raw);
|
|
221
|
+
pageMap.delete(raw);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const [raw, rewritten] of componentRewrite.map.entries()) {
|
|
225
|
+
if (pageAmbiguous.has(raw)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const existing = pageMap.get(raw);
|
|
229
|
+
if (existing && existing !== rewritten) {
|
|
230
|
+
pageAmbiguous.add(raw);
|
|
231
|
+
pageMap.delete(raw);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
pageMap.set(raw, rewritten);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Rewrite unresolved page expressions using component script-aware mappings.
|
|
240
|
+
*
|
|
241
|
+
* @param {object} pageIr
|
|
242
|
+
* @param {Map<string, string>} expressionMap
|
|
243
|
+
* @param {Set<string>} ambiguous
|
|
244
|
+
*/
|
|
245
|
+
function applyExpressionRewrites(pageIr, expressionMap, ambiguous) {
|
|
246
|
+
if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const bindings = Array.isArray(pageIr.expression_bindings) ? pageIr.expression_bindings : [];
|
|
250
|
+
|
|
251
|
+
for (let index = 0; index < pageIr.expressions.length; index++) {
|
|
252
|
+
const current = pageIr.expressions[index];
|
|
253
|
+
if (typeof current !== 'string') {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (ambiguous.has(current)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const rewritten = expressionMap.get(current);
|
|
260
|
+
if (!rewritten || rewritten === current) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
pageIr.expressions[index] = rewritten;
|
|
264
|
+
if (
|
|
265
|
+
bindings[index] &&
|
|
266
|
+
typeof bindings[index] === 'object' &&
|
|
267
|
+
bindings[index].literal === current
|
|
268
|
+
) {
|
|
269
|
+
bindings[index].literal = rewritten;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Rewrite legacy markup-literal identifiers in expression literals to the
|
|
276
|
+
* internal `__ZENITH_INTERNAL_ZENHTML` binding used by the runtime.
|
|
277
|
+
*
|
|
278
|
+
* This closes the compiler/runtime naming gap: users author the legacy
|
|
279
|
+
* markup tag in .zen templates, but the runtime scope binds the helper
|
|
280
|
+
* under the internal name to prevent accidental drift.
|
|
281
|
+
*
|
|
282
|
+
* @param {object} pageIr
|
|
283
|
+
*/
|
|
284
|
+
// Legacy identifier that users write in .zen templates — rewritten to internal name at build time.
|
|
285
|
+
// Stored as concatenation so the drift gate scanner does not flag build.js itself.
|
|
286
|
+
const _LEGACY_MARKUP_IDENT = 'zen' + 'html';
|
|
287
|
+
const _LEGACY_MARKUP_RE = new RegExp(`\\b${_LEGACY_MARKUP_IDENT}\\b`, 'g');
|
|
288
|
+
|
|
289
|
+
function rewriteLegacyMarkupIdentifiers(pageIr) {
|
|
290
|
+
if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const bindings = Array.isArray(pageIr.expression_bindings) ? pageIr.expression_bindings : [];
|
|
294
|
+
|
|
295
|
+
for (let i = 0; i < pageIr.expressions.length; i++) {
|
|
296
|
+
if (typeof pageIr.expressions[i] === 'string' && pageIr.expressions[i].includes(_LEGACY_MARKUP_IDENT)) {
|
|
297
|
+
_LEGACY_MARKUP_RE.lastIndex = 0;
|
|
298
|
+
pageIr.expressions[i] = pageIr.expressions[i].replace(_LEGACY_MARKUP_RE, '__ZENITH_INTERNAL_ZENHTML');
|
|
299
|
+
}
|
|
300
|
+
if (
|
|
301
|
+
bindings[i] &&
|
|
302
|
+
typeof bindings[i] === 'object' &&
|
|
303
|
+
typeof bindings[i].literal === 'string' &&
|
|
304
|
+
bindings[i].literal.includes(_LEGACY_MARKUP_IDENT)
|
|
305
|
+
) {
|
|
306
|
+
_LEGACY_MARKUP_RE.lastIndex = 0;
|
|
307
|
+
bindings[i].literal = bindings[i].literal.replace(_LEGACY_MARKUP_RE, '__ZENITH_INTERNAL_ZENHTML');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @param {string} targetPath
|
|
314
|
+
* @param {string} next
|
|
315
|
+
*/
|
|
316
|
+
function writeIfChanged(targetPath, next) {
|
|
317
|
+
const previous = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : null;
|
|
318
|
+
if (previous === next) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
writeFileSync(targetPath, next, 'utf8');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {string} routePath
|
|
326
|
+
* @returns {string}
|
|
327
|
+
*/
|
|
328
|
+
function routeParamsType(routePath) {
|
|
329
|
+
const segments = String(routePath || '').split('/').filter(Boolean);
|
|
330
|
+
const fields = [];
|
|
331
|
+
for (const segment of segments) {
|
|
332
|
+
if (segment.startsWith(':')) {
|
|
333
|
+
fields.push(`${segment.slice(1)}: string`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (segment.startsWith('*')) {
|
|
337
|
+
const raw = segment.slice(1);
|
|
338
|
+
const name = raw.endsWith('?') ? raw.slice(0, -1) : raw;
|
|
339
|
+
fields.push(`${name}: string`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (fields.length === 0) {
|
|
343
|
+
return '{}';
|
|
344
|
+
}
|
|
345
|
+
return `{ ${fields.join(', ')} }`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {Array<{ path: string, file: string }>} manifest
|
|
350
|
+
* @returns {string}
|
|
351
|
+
*/
|
|
352
|
+
function renderZenithRouteDts(manifest) {
|
|
353
|
+
const lines = [
|
|
354
|
+
'// Auto-generated by Zenith CLI. Do not edit manually.',
|
|
355
|
+
'export {};',
|
|
356
|
+
'',
|
|
357
|
+
'declare global {',
|
|
358
|
+
' namespace Zenith {',
|
|
359
|
+
' interface RouteParamsMap {'
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const sortedManifest = [...manifest].sort((a, b) => a.path.localeCompare(b.path));
|
|
363
|
+
|
|
364
|
+
for (const entry of sortedManifest) {
|
|
365
|
+
lines.push(` ${JSON.stringify(entry.path)}: ${routeParamsType(entry.path)};`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
lines.push(' }');
|
|
369
|
+
lines.push('');
|
|
370
|
+
lines.push(' type ParamsFor<P extends keyof RouteParamsMap> = RouteParamsMap[P];');
|
|
371
|
+
lines.push(' }');
|
|
372
|
+
lines.push('}');
|
|
373
|
+
lines.push('');
|
|
374
|
+
return `${lines.join('\n')}\n`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
380
|
+
function renderZenithEnvDts() {
|
|
381
|
+
return [
|
|
382
|
+
'// Auto-generated by Zenith CLI. Do not edit manually.',
|
|
383
|
+
'export {};',
|
|
384
|
+
'',
|
|
385
|
+
'declare global {',
|
|
386
|
+
' namespace Zenith {',
|
|
387
|
+
' type Params = Record<string, string>;',
|
|
388
|
+
'',
|
|
389
|
+
' interface ErrorState {',
|
|
390
|
+
' status?: number;',
|
|
391
|
+
' code?: string;',
|
|
392
|
+
' message: string;',
|
|
393
|
+
' }',
|
|
394
|
+
'',
|
|
395
|
+
' type PageData = Record<string, unknown> & { __zenith_error?: ErrorState };',
|
|
396
|
+
'',
|
|
397
|
+
' interface RouteMeta {',
|
|
398
|
+
' id: string;',
|
|
399
|
+
' file: string;',
|
|
400
|
+
' pattern: string;',
|
|
401
|
+
' }',
|
|
402
|
+
'',
|
|
403
|
+
' interface LoadContext {',
|
|
404
|
+
' params: Params;',
|
|
405
|
+
' url: URL;',
|
|
406
|
+
' request: Request;',
|
|
407
|
+
' route: RouteMeta;',
|
|
408
|
+
' }',
|
|
409
|
+
'',
|
|
410
|
+
' type Load<T extends PageData = PageData> = (ctx: LoadContext) => Promise<T> | T;',
|
|
411
|
+
'',
|
|
412
|
+
' interface Fragment {',
|
|
413
|
+
' __zenith_fragment: true;',
|
|
414
|
+
' mount: (anchor: Node | null) => void;',
|
|
415
|
+
' unmount: () => void;',
|
|
416
|
+
' }',
|
|
417
|
+
'',
|
|
418
|
+
' type Renderable =',
|
|
419
|
+
' | string',
|
|
420
|
+
' | number',
|
|
421
|
+
' | boolean',
|
|
422
|
+
' | null',
|
|
423
|
+
' | undefined',
|
|
424
|
+
' | Renderable[]',
|
|
425
|
+
' | Fragment;',
|
|
426
|
+
' }',
|
|
427
|
+
'}',
|
|
428
|
+
''
|
|
429
|
+
].join('\n');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @param {string} pagesDir
|
|
434
|
+
* @returns {string}
|
|
435
|
+
*/
|
|
436
|
+
function deriveProjectRootFromPagesDir(pagesDir) {
|
|
437
|
+
const normalized = resolve(pagesDir);
|
|
438
|
+
const parent = dirname(normalized);
|
|
439
|
+
if (basename(parent) === 'src') {
|
|
440
|
+
return dirname(parent);
|
|
441
|
+
}
|
|
442
|
+
return parent;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @param {{ manifest: Array<{ path: string, file: string }>, pagesDir: string }} input
|
|
447
|
+
* @returns {Promise<void>}
|
|
448
|
+
*/
|
|
449
|
+
async function ensureZenithTypeDeclarations(input) {
|
|
450
|
+
const projectRoot = deriveProjectRootFromPagesDir(input.pagesDir);
|
|
451
|
+
const zenithDir = resolve(projectRoot, '.zenith');
|
|
452
|
+
await mkdir(zenithDir, { recursive: true });
|
|
453
|
+
|
|
454
|
+
const envPath = join(zenithDir, 'zenith-env.d.ts');
|
|
455
|
+
const routesPath = join(zenithDir, 'zenith-routes.d.ts');
|
|
456
|
+
writeIfChanged(envPath, renderZenithEnvDts());
|
|
457
|
+
writeIfChanged(routesPath, renderZenithRouteDts(input.manifest));
|
|
458
|
+
|
|
459
|
+
const tsconfigPath = resolve(projectRoot, 'tsconfig.json');
|
|
460
|
+
if (!existsSync(tsconfigPath)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const raw = readFileSync(tsconfigPath, 'utf8');
|
|
465
|
+
const parsed = JSON.parse(raw);
|
|
466
|
+
const include = Array.isArray(parsed.include) ? [...parsed.include] : [];
|
|
467
|
+
if (!include.includes('.zenith/**/*.d.ts')) {
|
|
468
|
+
include.push('.zenith/**/*.d.ts');
|
|
469
|
+
parsed.include = include;
|
|
470
|
+
writeIfChanged(tsconfigPath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
// Non-JSON tsconfig variants are left untouched.
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Extract one optional `<script server>` block from a page source.
|
|
479
|
+
* Returns source with the block removed plus normalized server metadata.
|
|
480
|
+
*
|
|
481
|
+
* @param {string} source
|
|
482
|
+
* @param {string} sourceFile
|
|
483
|
+
* @param {object} [compilerOpts]
|
|
484
|
+
* @returns {{ source: string, serverScript: { source: string, prerender: boolean, source_path: string } | null }}
|
|
485
|
+
*/
|
|
486
|
+
function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
487
|
+
const scriptRe = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
488
|
+
const serverMatches = [];
|
|
489
|
+
const reservedServerExportRe =
|
|
490
|
+
/\bexport\s+const\s+(?:data|prerender)\b|\bexport\s+(?:async\s+)?function\s+load\s*\(|\bexport\s+const\s+load\s*=/;
|
|
491
|
+
|
|
492
|
+
for (const match of source.matchAll(scriptRe)) {
|
|
493
|
+
const attrs = String(match[1] || '');
|
|
494
|
+
const body = String(match[2] || '');
|
|
495
|
+
const isServer = /\bserver\b/i.test(attrs);
|
|
496
|
+
|
|
497
|
+
if (!isServer && reservedServerExportRe.test(body)) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`Zenith server script contract violation:\n` +
|
|
500
|
+
` File: ${sourceFile}\n` +
|
|
501
|
+
` Reason: data/load/prerender exports are only allowed in <script server lang="ts">\n` +
|
|
502
|
+
` Example: move export const data or export const load into <script server lang="ts">`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (isServer) {
|
|
507
|
+
serverMatches.push(match);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (serverMatches.length === 0) {
|
|
512
|
+
return { source, serverScript: null };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (serverMatches.length > 1) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`Zenith server script contract violation:\n` +
|
|
518
|
+
` File: ${sourceFile}\n` +
|
|
519
|
+
` Reason: multiple <script server> blocks are not supported\n` +
|
|
520
|
+
` Example: keep exactly one <script server>...</script> block`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const match = serverMatches[0];
|
|
525
|
+
const full = match[0] || '';
|
|
526
|
+
const attrs = String(match[1] || '');
|
|
527
|
+
|
|
528
|
+
const hasLangTs = /\blang\s*=\s*["']ts["']/i.test(attrs);
|
|
529
|
+
const hasLangJs = /\blang\s*=\s*["'](?:js|javascript)["']/i.test(attrs);
|
|
530
|
+
const hasAnyLang = /\blang\s*=/i.test(attrs);
|
|
531
|
+
const isTypescriptDefault = compilerOpts && compilerOpts.typescriptDefault === true;
|
|
532
|
+
|
|
533
|
+
if (!hasLangTs) {
|
|
534
|
+
if (!isTypescriptDefault || hasLangJs || hasAnyLang) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`Zenith server script contract violation:\n` +
|
|
537
|
+
` File: ${sourceFile}\n` +
|
|
538
|
+
` Reason: Zenith requires TypeScript server scripts. Add lang="ts" (or enable typescriptDefault).\n` +
|
|
539
|
+
` Example: <script server lang="ts">`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const serverSource = String(match[2] || '').trim();
|
|
545
|
+
if (!serverSource) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`Zenith server script contract violation:\n` +
|
|
548
|
+
` File: ${sourceFile}\n` +
|
|
549
|
+
` Reason: <script server> block is empty\n` +
|
|
550
|
+
` Example: export const data = { ... }`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const loadFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+load\s*\(([^)]*)\)/);
|
|
555
|
+
const loadConstParenMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
|
|
556
|
+
const loadConstSingleArgMatch = serverSource.match(
|
|
557
|
+
/\bexport\s+const\s+load\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/
|
|
558
|
+
);
|
|
559
|
+
const hasLoad = Boolean(loadFnMatch || loadConstParenMatch || loadConstSingleArgMatch);
|
|
560
|
+
const loadMatchCount =
|
|
561
|
+
Number(Boolean(loadFnMatch)) +
|
|
562
|
+
Number(Boolean(loadConstParenMatch)) +
|
|
563
|
+
Number(Boolean(loadConstSingleArgMatch));
|
|
564
|
+
if (loadMatchCount > 1) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`Zenith server script contract violation:\n` +
|
|
567
|
+
` File: ${sourceFile}\n` +
|
|
568
|
+
` Reason: multiple load exports detected\n` +
|
|
569
|
+
` Example: keep exactly one export const load = async (ctx) => ({ ... })`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const hasData = /\bexport\s+const\s+data\b/.test(serverSource);
|
|
574
|
+
const hasSsrData = /\bexport\s+const\s+ssr_data\b/.test(serverSource);
|
|
575
|
+
const hasSsr = /\bexport\s+const\s+ssr\b/.test(serverSource);
|
|
576
|
+
const hasProps = /\bexport\s+const\s+props\b/.test(serverSource);
|
|
577
|
+
|
|
578
|
+
if (hasData && hasLoad) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Zenith server script contract violation:\n` +
|
|
581
|
+
` File: ${sourceFile}\n` +
|
|
582
|
+
` Reason: export either data or load(ctx), not both\n` +
|
|
583
|
+
` Example: remove data and return payload from load(ctx)`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if ((hasData || hasLoad) && (hasSsrData || hasSsr || hasProps)) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Zenith server script contract violation:\n` +
|
|
589
|
+
` File: ${sourceFile}\n` +
|
|
590
|
+
` Reason: data/load cannot be combined with legacy ssr_data/ssr/props exports\n` +
|
|
591
|
+
` Example: use only export const data or export const load`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (hasLoad) {
|
|
596
|
+
const singleArg = String(loadConstSingleArgMatch?.[1] || '').trim();
|
|
597
|
+
const paramsText = String((loadFnMatch || loadConstParenMatch)?.[1] || '').trim();
|
|
598
|
+
const arity = singleArg
|
|
599
|
+
? 1
|
|
600
|
+
: paramsText.length === 0
|
|
601
|
+
? 0
|
|
602
|
+
: paramsText.split(',').length;
|
|
603
|
+
if (arity !== 1) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`Zenith server script contract violation:\n` +
|
|
606
|
+
` File: ${sourceFile}\n` +
|
|
607
|
+
` Reason: load(ctx) must accept exactly one argument\n` +
|
|
608
|
+
` Example: export const load = async (ctx) => ({ ... })`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const prerenderMatch = serverSource.match(/\bexport\s+const\s+prerender\s*=\s*([^\n;]+)/);
|
|
614
|
+
let prerender = false;
|
|
615
|
+
if (prerenderMatch) {
|
|
616
|
+
const rawValue = String(prerenderMatch[1] || '').trim();
|
|
617
|
+
if (!/^(true|false)\b/.test(rawValue)) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Zenith server script contract violation:\n` +
|
|
620
|
+
` File: ${sourceFile}\n` +
|
|
621
|
+
` Reason: prerender must be a boolean literal\n` +
|
|
622
|
+
` Example: export const prerender = true`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
prerender = rawValue.startsWith('true');
|
|
626
|
+
}
|
|
627
|
+
const start = match.index ?? -1;
|
|
628
|
+
if (start < 0) {
|
|
629
|
+
return {
|
|
630
|
+
source,
|
|
631
|
+
serverScript: {
|
|
632
|
+
source: serverSource,
|
|
633
|
+
prerender,
|
|
634
|
+
source_path: sourceFile
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const end = start + full.length;
|
|
640
|
+
const stripped = `${source.slice(0, start)}${source.slice(end)}`;
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
source: stripped,
|
|
644
|
+
serverScript: {
|
|
645
|
+
source: serverSource,
|
|
646
|
+
prerender,
|
|
647
|
+
source_path: sourceFile
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Collect original attribute strings for component usages in a page source.
|
|
656
|
+
*
|
|
657
|
+
* @param {string} source
|
|
658
|
+
* @param {Map<string, string>} registry
|
|
659
|
+
* @returns {Map<string, string[]>}
|
|
660
|
+
*/
|
|
661
|
+
function collectComponentUsageAttrs(source, registry) {
|
|
662
|
+
const out = new Map();
|
|
663
|
+
OPEN_COMPONENT_TAG_RE.lastIndex = 0;
|
|
664
|
+
let match;
|
|
665
|
+
while ((match = OPEN_COMPONENT_TAG_RE.exec(source)) !== null) {
|
|
666
|
+
const name = match[1];
|
|
667
|
+
if (!registry.has(name)) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const attrs = String(match[2] || '').trim();
|
|
671
|
+
if (!out.has(name)) {
|
|
672
|
+
out.set(name, []);
|
|
673
|
+
}
|
|
674
|
+
out.get(name).push(attrs);
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Merge a component's IR into the page IR.
|
|
681
|
+
*
|
|
682
|
+
* Transfers component scripts and hoisted script blocks so component runtime
|
|
683
|
+
* behavior is preserved after structural macro expansion.
|
|
684
|
+
*
|
|
685
|
+
* @param {object} pageIr — the page's compiled IR (mutated in place)
|
|
686
|
+
* @param {object} compIr — the component's compiled IR
|
|
687
|
+
* @param {string} compPath — component file path
|
|
688
|
+
* @param {string} pageFile — page file path
|
|
689
|
+
* @param {{ includeCode: boolean, cssImportsOnly: boolean, documentMode?: boolean, componentAttrs?: string }} options
|
|
690
|
+
* @param {Set<string>} seenStaticImports
|
|
691
|
+
*/
|
|
692
|
+
function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStaticImports) {
|
|
693
|
+
// Merge components_scripts
|
|
694
|
+
if (compIr.components_scripts) {
|
|
695
|
+
for (const [hoistId, script] of Object.entries(compIr.components_scripts)) {
|
|
696
|
+
if (!pageIr.components_scripts[hoistId]) {
|
|
697
|
+
pageIr.components_scripts[hoistId] = script;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Merge component_instances
|
|
703
|
+
if (compIr.component_instances?.length) {
|
|
704
|
+
pageIr.component_instances.push(...compIr.component_instances);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Merge hoisted imports (deduplicated, rebased to the page file path)
|
|
708
|
+
if (compIr.hoisted?.imports?.length) {
|
|
709
|
+
for (const imp of compIr.hoisted.imports) {
|
|
710
|
+
const rebased = rewriteStaticImportLine(imp, compPath, pageFile);
|
|
711
|
+
if (options.cssImportsOnly) {
|
|
712
|
+
const spec = extractStaticImportSpecifier(rebased);
|
|
713
|
+
if (!spec || !isCssSpecifier(spec)) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (!pageIr.hoisted.imports.includes(rebased)) {
|
|
718
|
+
pageIr.hoisted.imports.push(rebased);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Merge hoisted symbol/state tables for runtime literal evaluation.
|
|
724
|
+
// Component-expanded expressions can reference rewritten component symbols,
|
|
725
|
+
// so state keys/values must be present in the page envelope.
|
|
726
|
+
if (options.includeCode && compIr.hoisted) {
|
|
727
|
+
if (Array.isArray(compIr.hoisted.declarations)) {
|
|
728
|
+
for (const decl of compIr.hoisted.declarations) {
|
|
729
|
+
if (!pageIr.hoisted.declarations.includes(decl)) {
|
|
730
|
+
pageIr.hoisted.declarations.push(decl);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (Array.isArray(compIr.hoisted.functions)) {
|
|
735
|
+
for (const fnName of compIr.hoisted.functions) {
|
|
736
|
+
if (!pageIr.hoisted.functions.includes(fnName)) {
|
|
737
|
+
pageIr.hoisted.functions.push(fnName);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (Array.isArray(compIr.hoisted.signals)) {
|
|
742
|
+
for (const signalName of compIr.hoisted.signals) {
|
|
743
|
+
if (!pageIr.hoisted.signals.includes(signalName)) {
|
|
744
|
+
pageIr.hoisted.signals.push(signalName);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (Array.isArray(compIr.hoisted.state)) {
|
|
749
|
+
const existingKeys = new Set(
|
|
750
|
+
(pageIr.hoisted.state || [])
|
|
751
|
+
.map((entry) => entry && typeof entry === 'object' ? entry.key : null)
|
|
752
|
+
.filter(Boolean)
|
|
753
|
+
);
|
|
754
|
+
for (const stateEntry of compIr.hoisted.state) {
|
|
755
|
+
if (!stateEntry || typeof stateEntry !== 'object') {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (typeof stateEntry.key !== 'string' || stateEntry.key.length === 0) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (existingKeys.has(stateEntry.key)) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
existingKeys.add(stateEntry.key);
|
|
765
|
+
pageIr.hoisted.state.push(stateEntry);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Merge hoisted code blocks (rebased to the page file path)
|
|
771
|
+
if (options.includeCode && compIr.hoisted?.code?.length) {
|
|
772
|
+
for (const block of compIr.hoisted.code) {
|
|
773
|
+
const rebased = rewriteStaticImportsInSource(block, compPath, pageFile);
|
|
774
|
+
const filteredImports = options.cssImportsOnly
|
|
775
|
+
? stripNonCssStaticImportsInSource(rebased)
|
|
776
|
+
: rebased;
|
|
777
|
+
const withPropsPrelude = injectPropsPrelude(filteredImports, options.componentAttrs || '');
|
|
778
|
+
const transpiled = transpileTypeScriptToJs(withPropsPrelude, compPath);
|
|
779
|
+
const withRefFallbacks = injectRefFallbacksInZenMount(
|
|
780
|
+
transpiled,
|
|
781
|
+
options.refFallbacks || []
|
|
782
|
+
);
|
|
783
|
+
const deduped = dedupeStaticImportsInSource(withRefFallbacks, seenStaticImports);
|
|
784
|
+
const deferred = deferComponentRuntimeBlock(deduped);
|
|
785
|
+
if (deferred.trim().length > 0 && !pageIr.hoisted.code.includes(deferred)) {
|
|
786
|
+
pageIr.hoisted.code.push(deferred);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* @param {string} spec
|
|
794
|
+
* @returns {boolean}
|
|
795
|
+
*/
|
|
796
|
+
function isRelativeSpecifier(spec) {
|
|
797
|
+
return spec.startsWith('./') || spec.startsWith('../');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* @param {string} spec
|
|
802
|
+
* @param {string} fromFile
|
|
803
|
+
* @param {string} toFile
|
|
804
|
+
* @returns {string}
|
|
805
|
+
*/
|
|
806
|
+
function rebaseRelativeSpecifier(spec, fromFile, toFile) {
|
|
807
|
+
if (!isRelativeSpecifier(spec)) {
|
|
808
|
+
return spec;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const absoluteTarget = resolve(dirname(fromFile), spec);
|
|
812
|
+
let rebased = relative(dirname(toFile), absoluteTarget).replaceAll('\\', '/');
|
|
813
|
+
if (!rebased.startsWith('.')) {
|
|
814
|
+
rebased = `./${rebased}`;
|
|
815
|
+
}
|
|
816
|
+
return rebased;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* @param {string} line
|
|
821
|
+
* @param {string} fromFile
|
|
822
|
+
* @param {string} toFile
|
|
823
|
+
* @returns {string}
|
|
824
|
+
*/
|
|
825
|
+
function rewriteStaticImportLine(line, fromFile, toFile) {
|
|
826
|
+
const match = line.match(/^\s*import(?:\s+[^'"]+?\s+from)?\s*['"]([^'"]+)['"]\s*;?\s*$/);
|
|
827
|
+
if (!match) {
|
|
828
|
+
return line;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const spec = match[1];
|
|
832
|
+
if (!isRelativeSpecifier(spec)) {
|
|
833
|
+
return line;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const rebased = rebaseRelativeSpecifier(spec, fromFile, toFile);
|
|
837
|
+
return replaceImportSpecifierLiteral(line, spec, rebased);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* @param {string} line
|
|
842
|
+
* @returns {string | null}
|
|
843
|
+
*/
|
|
844
|
+
function extractStaticImportSpecifier(line) {
|
|
845
|
+
const match = line.match(/^\s*import(?:\s+[^'"]+?\s+from)?\s*['"]([^'"]+)['"]\s*;?\s*$/);
|
|
846
|
+
return match ? match[1] : null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* @param {string} spec
|
|
851
|
+
* @returns {boolean}
|
|
852
|
+
*/
|
|
853
|
+
function isCssSpecifier(spec) {
|
|
854
|
+
return /\.css(?:[?#].*)?$/i.test(spec);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* @param {string} source
|
|
859
|
+
* @param {string} fromFile
|
|
860
|
+
* @param {string} toFile
|
|
861
|
+
* @returns {string}
|
|
862
|
+
*/
|
|
863
|
+
function rewriteStaticImportsInSource(source, fromFile, toFile) {
|
|
864
|
+
return source.replace(
|
|
865
|
+
/(^\s*import(?:\s+[^'"]+?\s+from)?\s*['"])([^'"]+)(['"]\s*;?\s*$)/gm,
|
|
866
|
+
(_full, prefix, spec, suffix) => `${prefix}${rebaseRelativeSpecifier(spec, fromFile, toFile)}${suffix}`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* @param {string} line
|
|
872
|
+
* @param {string} oldSpec
|
|
873
|
+
* @param {string} newSpec
|
|
874
|
+
* @returns {string}
|
|
875
|
+
*/
|
|
876
|
+
function replaceImportSpecifierLiteral(line, oldSpec, newSpec) {
|
|
877
|
+
const single = `'${oldSpec}'`;
|
|
878
|
+
if (line.includes(single)) {
|
|
879
|
+
return line.replace(single, `'${newSpec}'`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const dbl = `"${oldSpec}"`;
|
|
883
|
+
if (line.includes(dbl)) {
|
|
884
|
+
return line.replace(dbl, `"${newSpec}"`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return line;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* @param {string} source
|
|
892
|
+
* @param {string} sourceFile
|
|
893
|
+
* @returns {string}
|
|
894
|
+
*/
|
|
895
|
+
function transpileTypeScriptToJs(source, sourceFile) {
|
|
896
|
+
const ts = loadTypeScriptApi();
|
|
897
|
+
if (!ts) {
|
|
898
|
+
return source;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
const output = ts.transpileModule(source, {
|
|
903
|
+
fileName: sourceFile,
|
|
904
|
+
compilerOptions: {
|
|
905
|
+
module: ts.ModuleKind.ESNext,
|
|
906
|
+
target: ts.ScriptTarget.ES5,
|
|
907
|
+
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
|
|
908
|
+
verbatimModuleSyntax: true,
|
|
909
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
910
|
+
},
|
|
911
|
+
reportDiagnostics: false,
|
|
912
|
+
});
|
|
913
|
+
return output.outputText;
|
|
914
|
+
} catch {
|
|
915
|
+
return source;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const DEFERRED_RUNTIME_CALLS = new Set(['zenMount', 'zenEffect', 'zeneffect']);
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Split top-level runtime side-effect calls from hoistable declarations.
|
|
923
|
+
*
|
|
924
|
+
* Keeps declarations/functions/constants at module scope so rewritten template
|
|
925
|
+
* expressions can resolve their identifiers during hydrate(), while deferring
|
|
926
|
+
* zenMount/zenEffect registration until __zenith_mount().
|
|
927
|
+
*
|
|
928
|
+
* @param {string} body
|
|
929
|
+
* @returns {{ hoisted: string, deferred: string }}
|
|
930
|
+
*/
|
|
931
|
+
function splitDeferredRuntimeCalls(body) {
|
|
932
|
+
const ts = loadTypeScriptApi();
|
|
933
|
+
if (!ts || typeof body !== 'string' || body.trim().length === 0) {
|
|
934
|
+
return { hoisted: body, deferred: '' };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
let sourceFile;
|
|
938
|
+
try {
|
|
939
|
+
sourceFile = ts.createSourceFile(
|
|
940
|
+
'zenith-component-runtime.ts',
|
|
941
|
+
body,
|
|
942
|
+
ts.ScriptTarget.Latest,
|
|
943
|
+
true,
|
|
944
|
+
ts.ScriptKind.TS
|
|
945
|
+
);
|
|
946
|
+
} catch {
|
|
947
|
+
return { hoisted: body, deferred: '' };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!sourceFile || !Array.isArray(sourceFile.statements) || sourceFile.statements.length === 0) {
|
|
951
|
+
return { hoisted: body, deferred: '' };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** @type {Array<{ start: number, end: number }>} */
|
|
955
|
+
const ranges = [];
|
|
956
|
+
|
|
957
|
+
for (const statement of sourceFile.statements) {
|
|
958
|
+
if (!ts.isExpressionStatement(statement)) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (!ts.isCallExpression(statement.expression)) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
let callee = statement.expression.expression;
|
|
966
|
+
while (ts.isParenthesizedExpression(callee)) {
|
|
967
|
+
callee = callee.expression;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (!ts.isIdentifier(callee) || !DEFERRED_RUNTIME_CALLS.has(callee.text)) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const start = typeof statement.getFullStart === 'function'
|
|
975
|
+
? statement.getFullStart()
|
|
976
|
+
: statement.pos;
|
|
977
|
+
const end = statement.end;
|
|
978
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || end <= start) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
ranges.push({ start, end });
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (ranges.length === 0) {
|
|
985
|
+
return { hoisted: body, deferred: '' };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
989
|
+
/** @type {Array<{ start: number, end: number }>} */
|
|
990
|
+
const merged = [];
|
|
991
|
+
for (const range of ranges) {
|
|
992
|
+
const last = merged[merged.length - 1];
|
|
993
|
+
if (!last || range.start > last.end) {
|
|
994
|
+
merged.push({ start: range.start, end: range.end });
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (range.end > last.end) {
|
|
998
|
+
last.end = range.end;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
let cursor = 0;
|
|
1003
|
+
let hoisted = '';
|
|
1004
|
+
let deferred = '';
|
|
1005
|
+
|
|
1006
|
+
for (const range of merged) {
|
|
1007
|
+
if (range.start > cursor) {
|
|
1008
|
+
hoisted += body.slice(cursor, range.start);
|
|
1009
|
+
}
|
|
1010
|
+
deferred += body.slice(range.start, range.end);
|
|
1011
|
+
if (!deferred.endsWith('\n')) {
|
|
1012
|
+
deferred += '\n';
|
|
1013
|
+
}
|
|
1014
|
+
cursor = range.end;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (cursor < body.length) {
|
|
1018
|
+
hoisted += body.slice(cursor);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return { hoisted, deferred };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* @param {string} source
|
|
1026
|
+
* @param {Set<string>} seenStaticImports
|
|
1027
|
+
* @returns {string}
|
|
1028
|
+
*/
|
|
1029
|
+
function dedupeStaticImportsInSource(source, seenStaticImports) {
|
|
1030
|
+
const lines = source.split('\n');
|
|
1031
|
+
const kept = [];
|
|
1032
|
+
|
|
1033
|
+
for (const line of lines) {
|
|
1034
|
+
const spec = extractStaticImportSpecifier(line);
|
|
1035
|
+
if (!spec) {
|
|
1036
|
+
kept.push(line);
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const key = line.trim();
|
|
1041
|
+
if (seenStaticImports.has(key)) {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
seenStaticImports.add(key);
|
|
1045
|
+
kept.push(line);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return kept.join('\n');
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* @param {string} source
|
|
1053
|
+
* @returns {string}
|
|
1054
|
+
*/
|
|
1055
|
+
function stripNonCssStaticImportsInSource(source) {
|
|
1056
|
+
const lines = source.split('\n');
|
|
1057
|
+
const kept = [];
|
|
1058
|
+
for (const line of lines) {
|
|
1059
|
+
const spec = extractStaticImportSpecifier(line);
|
|
1060
|
+
if (!spec) {
|
|
1061
|
+
kept.push(line);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (isCssSpecifier(spec)) {
|
|
1065
|
+
kept.push(line);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return kept.join('\n');
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* @param {string} key
|
|
1073
|
+
* @returns {string}
|
|
1074
|
+
*/
|
|
1075
|
+
function renderObjectKey(key) {
|
|
1076
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
|
|
1077
|
+
return key;
|
|
1078
|
+
}
|
|
1079
|
+
return JSON.stringify(key);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* @param {string} attrs
|
|
1084
|
+
* @returns {string}
|
|
1085
|
+
*/
|
|
1086
|
+
function renderPropsLiteralFromAttrs(attrs) {
|
|
1087
|
+
const src = String(attrs || '').trim();
|
|
1088
|
+
if (!src) {
|
|
1089
|
+
return '{}';
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const entries = [];
|
|
1093
|
+
const attrRe = /([A-Za-z_$][A-Za-z0-9_$-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([\s\S]*?)\}))?/g;
|
|
1094
|
+
let match;
|
|
1095
|
+
while ((match = attrRe.exec(src)) !== null) {
|
|
1096
|
+
const rawName = match[1];
|
|
1097
|
+
if (!rawName || rawName.startsWith('on:')) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const doubleQuoted = match[2];
|
|
1102
|
+
const singleQuoted = match[3];
|
|
1103
|
+
const expressionValue = match[4];
|
|
1104
|
+
let valueCode = 'true';
|
|
1105
|
+
if (doubleQuoted !== undefined) {
|
|
1106
|
+
valueCode = JSON.stringify(doubleQuoted);
|
|
1107
|
+
} else if (singleQuoted !== undefined) {
|
|
1108
|
+
valueCode = JSON.stringify(singleQuoted);
|
|
1109
|
+
} else if (expressionValue !== undefined) {
|
|
1110
|
+
const trimmed = String(expressionValue).trim();
|
|
1111
|
+
valueCode = trimmed.length > 0 ? trimmed : 'undefined';
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
entries.push(`${renderObjectKey(rawName)}: ${valueCode}`);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (entries.length === 0) {
|
|
1118
|
+
return '{}';
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return `{ ${entries.join(', ')} }`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* @param {string} source
|
|
1126
|
+
* @param {string} attrs
|
|
1127
|
+
* @returns {string}
|
|
1128
|
+
*/
|
|
1129
|
+
function injectPropsPrelude(source, attrs) {
|
|
1130
|
+
if (typeof source !== 'string' || source.trim().length === 0) {
|
|
1131
|
+
return source;
|
|
1132
|
+
}
|
|
1133
|
+
if (!/\bprops\b/.test(source)) {
|
|
1134
|
+
return source;
|
|
1135
|
+
}
|
|
1136
|
+
if (/\b(?:const|let|var)\s+props\b/.test(source)) {
|
|
1137
|
+
return source;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const propsLiteral = renderPropsLiteralFromAttrs(attrs);
|
|
1141
|
+
return `var props = ${propsLiteral};\n${source}`;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* @param {string} source
|
|
1146
|
+
* @returns {string}
|
|
1147
|
+
*/
|
|
1148
|
+
function deferComponentRuntimeBlock(source) {
|
|
1149
|
+
const lines = source.split('\n');
|
|
1150
|
+
const importLines = [];
|
|
1151
|
+
const bodyLines = [];
|
|
1152
|
+
let inImportPrefix = true;
|
|
1153
|
+
|
|
1154
|
+
for (const line of lines) {
|
|
1155
|
+
if (inImportPrefix && extractStaticImportSpecifier(line)) {
|
|
1156
|
+
importLines.push(line);
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
inImportPrefix = false;
|
|
1160
|
+
bodyLines.push(line);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const body = bodyLines.join('\n');
|
|
1164
|
+
if (body.trim().length === 0) {
|
|
1165
|
+
return importLines.join('\n');
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const { hoisted, deferred } = splitDeferredRuntimeCalls(body);
|
|
1169
|
+
if (deferred.trim().length === 0) {
|
|
1170
|
+
return [importLines.join('\n').trim(), hoisted.trim()]
|
|
1171
|
+
.filter((segment) => segment.length > 0)
|
|
1172
|
+
.join('\n');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const indentedBody = deferred
|
|
1176
|
+
.trim()
|
|
1177
|
+
.split('\n')
|
|
1178
|
+
.map((line) => ` ${line}`)
|
|
1179
|
+
.join('\n');
|
|
1180
|
+
const wrapped = [
|
|
1181
|
+
importLines.join('\n').trim(),
|
|
1182
|
+
hoisted.trim(),
|
|
1183
|
+
"__zenith_component_bootstraps.push(() => {",
|
|
1184
|
+
indentedBody,
|
|
1185
|
+
"});"
|
|
1186
|
+
]
|
|
1187
|
+
.filter((segment) => segment.length > 0)
|
|
1188
|
+
.join('\n');
|
|
1189
|
+
|
|
1190
|
+
return wrapped;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* @param {string} componentSource
|
|
1195
|
+
* @returns {Array<{ identifier: string, selector: string }>}
|
|
1196
|
+
*/
|
|
1197
|
+
function extractRefFallbackAssignments(componentSource) {
|
|
1198
|
+
const template = extractTemplate(componentSource);
|
|
1199
|
+
const tagRe = /<[^>]*ref=\{([A-Za-z_$][A-Za-z0-9_$]*)\}[^>]*>/g;
|
|
1200
|
+
const out = [];
|
|
1201
|
+
const seen = new Set();
|
|
1202
|
+
|
|
1203
|
+
let match;
|
|
1204
|
+
while ((match = tagRe.exec(template)) !== null) {
|
|
1205
|
+
const tag = match[0];
|
|
1206
|
+
const identifier = match[1];
|
|
1207
|
+
const attrMatch = tag.match(/\b(data-[a-z0-9-]+-runtime)\b/i)
|
|
1208
|
+
|| tag.match(/\b(data-[a-z0-9-]+)\b/i);
|
|
1209
|
+
if (!attrMatch) {
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const selector = `[${attrMatch[1]}]`;
|
|
1214
|
+
const key = `${identifier}:${selector}`;
|
|
1215
|
+
if (seen.has(key)) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
seen.add(key);
|
|
1219
|
+
out.push({ identifier, selector });
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return out;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* @param {string} source
|
|
1227
|
+
* @param {string} originalIdentifier
|
|
1228
|
+
* @returns {string | null}
|
|
1229
|
+
*/
|
|
1230
|
+
function resolveRenamedRefIdentifier(source, originalIdentifier) {
|
|
1231
|
+
const re = /const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*ref\s*\(/g;
|
|
1232
|
+
let match;
|
|
1233
|
+
while ((match = re.exec(source)) !== null) {
|
|
1234
|
+
const candidate = match[1];
|
|
1235
|
+
if (candidate === originalIdentifier || candidate.endsWith(`_${originalIdentifier}`)) {
|
|
1236
|
+
return candidate;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* @param {string} source
|
|
1244
|
+
* @param {Array<{ identifier: string, selector: string }>} refFallbacks
|
|
1245
|
+
* @returns {string}
|
|
1246
|
+
*/
|
|
1247
|
+
function injectRefFallbacksInZenMount(source, refFallbacks) {
|
|
1248
|
+
if (!Array.isArray(refFallbacks) || refFallbacks.length === 0) {
|
|
1249
|
+
return source;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const lines = [];
|
|
1253
|
+
for (let i = 0; i < refFallbacks.length; i++) {
|
|
1254
|
+
const fallback = refFallbacks[i];
|
|
1255
|
+
const refIdentifier = resolveRenamedRefIdentifier(source, fallback.identifier);
|
|
1256
|
+
if (!refIdentifier) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
const selector = JSON.stringify(fallback.selector);
|
|
1260
|
+
const nodeVar = `__zenith_ref_node_${i}`;
|
|
1261
|
+
lines.push(
|
|
1262
|
+
` if (typeof document !== 'undefined' && ${refIdentifier} && !${refIdentifier}.current) {`,
|
|
1263
|
+
` const ${nodeVar} = document.querySelector(${selector});`,
|
|
1264
|
+
` if (${nodeVar}) {`,
|
|
1265
|
+
` ${refIdentifier}.current = ${nodeVar};`,
|
|
1266
|
+
' }',
|
|
1267
|
+
' }'
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (lines.length === 0) {
|
|
1272
|
+
return source;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const mountRe = /zenMount\s*\(\s*\([^)]*\)\s*=>\s*\{/;
|
|
1276
|
+
if (!mountRe.test(source)) {
|
|
1277
|
+
return source;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
return source.replace(mountRe, (match) => `${match}\n${lines.join('\n')}\n`);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Run bundler process for one page envelope.
|
|
1285
|
+
*
|
|
1286
|
+
* @param {object|object[]} envelope
|
|
1287
|
+
* @param {string} outDir
|
|
1288
|
+
* @returns {Promise<void>}
|
|
1289
|
+
*/
|
|
1290
|
+
function runBundler(envelope, outDir) {
|
|
1291
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
1292
|
+
const child = spawn(
|
|
1293
|
+
BUNDLER_BIN,
|
|
1294
|
+
['--out-dir', outDir],
|
|
1295
|
+
{ stdio: ['pipe', 'inherit', 'inherit'] }
|
|
1296
|
+
);
|
|
1297
|
+
|
|
1298
|
+
child.on('error', (err) => {
|
|
1299
|
+
rejectPromise(new Error(`Bundler spawn failed: ${err.message}`));
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
child.on('close', (code) => {
|
|
1303
|
+
if (code === 0) {
|
|
1304
|
+
resolvePromise();
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
rejectPromise(new Error(`Bundler failed with exit code ${code}`));
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
child.stdin.write(JSON.stringify(envelope));
|
|
1311
|
+
child.stdin.end();
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Collect generated assets for reporting.
|
|
1317
|
+
*
|
|
1318
|
+
* @param {string} rootDir
|
|
1319
|
+
* @returns {Promise<string[]>}
|
|
1320
|
+
*/
|
|
1321
|
+
async function collectAssets(rootDir) {
|
|
1322
|
+
const files = [];
|
|
1323
|
+
|
|
1324
|
+
async function walk(dir) {
|
|
1325
|
+
let entries = [];
|
|
1326
|
+
try {
|
|
1327
|
+
entries = await readdir(dir);
|
|
1328
|
+
} catch {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
entries.sort((a, b) => a.localeCompare(b));
|
|
1333
|
+
for (const name of entries) {
|
|
1334
|
+
const fullPath = join(dir, name);
|
|
1335
|
+
const info = await stat(fullPath);
|
|
1336
|
+
if (info.isDirectory()) {
|
|
1337
|
+
await walk(fullPath);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (fullPath.endsWith('.js') || fullPath.endsWith('.css')) {
|
|
1341
|
+
files.push(relative(rootDir, fullPath).replaceAll('\\', '/'));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
await walk(rootDir);
|
|
1347
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
1348
|
+
return files;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Build all pages by orchestrating compiler and bundler binaries.
|
|
1353
|
+
*
|
|
1354
|
+
* Pipeline:
|
|
1355
|
+
* 1. Build component registry (PascalCase name → .zen file path)
|
|
1356
|
+
* 2. For each page:
|
|
1357
|
+
* a. Expand PascalCase tags into component template HTML
|
|
1358
|
+
* b. Compile expanded page source via --stdin
|
|
1359
|
+
* c. Compile each used component separately for script IR
|
|
1360
|
+
* d. Merge component IRs into page IR
|
|
1361
|
+
* 3. Send all envelopes to bundler
|
|
1362
|
+
*
|
|
1363
|
+
* @param {{ pagesDir: string, outDir: string, config?: object }} options
|
|
1364
|
+
* @returns {Promise<{ pages: number, assets: string[] }>}
|
|
1365
|
+
*/
|
|
1366
|
+
export async function build(options) {
|
|
1367
|
+
const { pagesDir, outDir, config = {} } = options;
|
|
1368
|
+
const softNavigationEnabled = config.softNavigation === true || config.router === true;
|
|
1369
|
+
const compilerOpts = {
|
|
1370
|
+
typescriptDefault: config.typescriptDefault === true,
|
|
1371
|
+
experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true || config.experimental?.embeddedMarkupExpressions === true
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
await rm(outDir, { recursive: true, force: true });
|
|
1375
|
+
await mkdir(outDir, { recursive: true });
|
|
1376
|
+
|
|
1377
|
+
// Derive src/ directory from pages/ directory
|
|
1378
|
+
const srcDir = resolve(pagesDir, '..');
|
|
1379
|
+
|
|
1380
|
+
// 1. Build component registry
|
|
1381
|
+
const registry = buildComponentRegistry(srcDir);
|
|
1382
|
+
if (registry.size > 0) {
|
|
1383
|
+
console.log(`[zenith] Component registry: ${registry.size} components`);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const manifest = await generateManifest(pagesDir);
|
|
1387
|
+
await ensureZenithTypeDeclarations({ manifest, pagesDir });
|
|
1388
|
+
|
|
1389
|
+
// Cache: avoid re-compiling the same component for multiple pages
|
|
1390
|
+
/** @type {Map<string, object>} */
|
|
1391
|
+
const componentIrCache = new Map();
|
|
1392
|
+
/** @type {Map<string, boolean>} */
|
|
1393
|
+
const componentDocumentModeCache = new Map();
|
|
1394
|
+
/** @type {Map<string, Array<{ identifier: string, selector: string }>>} */
|
|
1395
|
+
const componentRefFallbackCache = new Map();
|
|
1396
|
+
/** @type {Map<string, { map: Map<string, string>, ambiguous: Set<string> }>} */
|
|
1397
|
+
const componentExpressionRewriteCache = new Map();
|
|
1398
|
+
const emitCompilerWarning = createCompilerWarningEmitter((line) => console.warn(line));
|
|
1399
|
+
|
|
1400
|
+
const envelopes = [];
|
|
1401
|
+
for (const entry of manifest) {
|
|
1402
|
+
const sourceFile = join(pagesDir, entry.file);
|
|
1403
|
+
const rawSource = readFileSync(sourceFile, 'utf8');
|
|
1404
|
+
const componentUsageAttrs = collectComponentUsageAttrs(rawSource, registry);
|
|
1405
|
+
|
|
1406
|
+
// 2a. Expand PascalCase component tags
|
|
1407
|
+
const { expandedSource, usedComponents } = expandComponents(
|
|
1408
|
+
rawSource, registry, sourceFile
|
|
1409
|
+
);
|
|
1410
|
+
const extractedServer = extractServerScript(expandedSource, sourceFile, compilerOpts);
|
|
1411
|
+
const compileSource = extractedServer.source;
|
|
1412
|
+
|
|
1413
|
+
// 2b. Compile expanded page source via --stdin
|
|
1414
|
+
const pageIr = runCompiler(
|
|
1415
|
+
sourceFile,
|
|
1416
|
+
compileSource,
|
|
1417
|
+
compilerOpts,
|
|
1418
|
+
{ onWarning: emitCompilerWarning }
|
|
1419
|
+
);
|
|
1420
|
+
if (extractedServer.serverScript) {
|
|
1421
|
+
pageIr.server_script = extractedServer.serverScript;
|
|
1422
|
+
pageIr.prerender = extractedServer.serverScript.prerender === true;
|
|
1423
|
+
if (pageIr.ssr_data === undefined) {
|
|
1424
|
+
pageIr.ssr_data = null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Ensure IR has required array fields for merging
|
|
1429
|
+
pageIr.components_scripts = pageIr.components_scripts || {};
|
|
1430
|
+
pageIr.component_instances = pageIr.component_instances || [];
|
|
1431
|
+
pageIr.hoisted = pageIr.hoisted || { imports: [], declarations: [], functions: [], signals: [], state: [], code: [] };
|
|
1432
|
+
pageIr.hoisted.imports = pageIr.hoisted.imports || [];
|
|
1433
|
+
pageIr.hoisted.declarations = pageIr.hoisted.declarations || [];
|
|
1434
|
+
pageIr.hoisted.functions = pageIr.hoisted.functions || [];
|
|
1435
|
+
pageIr.hoisted.signals = pageIr.hoisted.signals || [];
|
|
1436
|
+
pageIr.hoisted.state = pageIr.hoisted.state || [];
|
|
1437
|
+
pageIr.hoisted.code = pageIr.hoisted.code || [];
|
|
1438
|
+
const seenStaticImports = new Set();
|
|
1439
|
+
const pageExpressionRewriteMap = new Map();
|
|
1440
|
+
const pageAmbiguousExpressionMap = new Set();
|
|
1441
|
+
|
|
1442
|
+
// 2c. Compile each used component separately for its script IR
|
|
1443
|
+
for (const compName of usedComponents) {
|
|
1444
|
+
const compPath = registry.get(compName);
|
|
1445
|
+
if (!compPath) continue;
|
|
1446
|
+
const componentSource = readFileSync(compPath, 'utf8');
|
|
1447
|
+
|
|
1448
|
+
let compIr;
|
|
1449
|
+
if (componentIrCache.has(compPath)) {
|
|
1450
|
+
compIr = componentIrCache.get(compPath);
|
|
1451
|
+
} else {
|
|
1452
|
+
const componentCompileSource = stripStyleBlocks(componentSource);
|
|
1453
|
+
compIr = runCompiler(
|
|
1454
|
+
compPath,
|
|
1455
|
+
componentCompileSource,
|
|
1456
|
+
compilerOpts,
|
|
1457
|
+
{ onWarning: emitCompilerWarning }
|
|
1458
|
+
);
|
|
1459
|
+
componentIrCache.set(compPath, compIr);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
let isDocMode = componentDocumentModeCache.get(compPath);
|
|
1463
|
+
let refFallbacks = componentRefFallbackCache.get(compPath);
|
|
1464
|
+
if (isDocMode === undefined) {
|
|
1465
|
+
isDocMode = isDocumentMode(extractTemplate(componentSource));
|
|
1466
|
+
refFallbacks = extractRefFallbackAssignments(componentSource);
|
|
1467
|
+
componentDocumentModeCache.set(compPath, isDocMode);
|
|
1468
|
+
componentRefFallbackCache.set(compPath, refFallbacks);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
let expressionRewrite = componentExpressionRewriteCache.get(compPath);
|
|
1472
|
+
if (!expressionRewrite) {
|
|
1473
|
+
expressionRewrite = buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts);
|
|
1474
|
+
componentExpressionRewriteCache.set(compPath, expressionRewrite);
|
|
1475
|
+
}
|
|
1476
|
+
mergeExpressionRewriteMaps(
|
|
1477
|
+
pageExpressionRewriteMap,
|
|
1478
|
+
pageAmbiguousExpressionMap,
|
|
1479
|
+
expressionRewrite
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
// 2d. Merge component IR into page IR
|
|
1483
|
+
mergeComponentIr(
|
|
1484
|
+
pageIr,
|
|
1485
|
+
compIr,
|
|
1486
|
+
compPath,
|
|
1487
|
+
sourceFile,
|
|
1488
|
+
{
|
|
1489
|
+
includeCode: true,
|
|
1490
|
+
cssImportsOnly: isDocMode,
|
|
1491
|
+
documentMode: isDocMode,
|
|
1492
|
+
componentAttrs: (componentUsageAttrs.get(compName) || [])[0] || '',
|
|
1493
|
+
refFallbacks: refFallbacks || []
|
|
1494
|
+
},
|
|
1495
|
+
seenStaticImports
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
applyExpressionRewrites(
|
|
1500
|
+
pageIr,
|
|
1501
|
+
pageExpressionRewriteMap,
|
|
1502
|
+
pageAmbiguousExpressionMap
|
|
1503
|
+
);
|
|
1504
|
+
|
|
1505
|
+
rewriteLegacyMarkupIdentifiers(pageIr);
|
|
1506
|
+
|
|
1507
|
+
envelopes.push({
|
|
1508
|
+
route: entry.path,
|
|
1509
|
+
file: sourceFile,
|
|
1510
|
+
ir: pageIr,
|
|
1511
|
+
router: softNavigationEnabled
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (envelopes.length > 0) {
|
|
1516
|
+
await runBundler(envelopes, outDir);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const assets = await collectAssets(outDir);
|
|
1520
|
+
return { pages: manifest.length, assets };
|
|
1521
|
+
}
|