@zenithbuild/cli 0.6.4 → 0.6.5
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/dist/build.js +67 -21
- package/dist/component-instance-ir.js +217 -0
- package/dist/component-occurrences.js +152 -0
- package/package.json +2 -2
package/dist/build.js
CHANGED
|
@@ -18,6 +18,8 @@ import { createRequire } from 'node:module';
|
|
|
18
18
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
19
19
|
import { generateManifest } from './manifest.js';
|
|
20
20
|
import { buildComponentRegistry, expandComponents, extractTemplate, isDocumentMode } from './resolve-components.js';
|
|
21
|
+
import { collectExpandedComponentOccurrences } from './component-occurrences.js';
|
|
22
|
+
import { applyOccurrenceRewritePlans, cloneComponentIrForInstance } from './component-instance-ir.js';
|
|
21
23
|
import { resolveBundlerBin, resolveCompilerBin } from './toolchain-paths.js';
|
|
22
24
|
import { maybeWarnAboutZenithVersionMismatch } from './version-check.js';
|
|
23
25
|
|
|
@@ -189,7 +191,8 @@ function buildComponentExpressionRewrite(compPath, componentSource, compIr, comp
|
|
|
189
191
|
bindings: new Map(),
|
|
190
192
|
signals: Array.isArray(compIr?.signals) ? compIr.signals : [],
|
|
191
193
|
stateBindings: Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [],
|
|
192
|
-
ambiguous: new Set()
|
|
194
|
+
ambiguous: new Set(),
|
|
195
|
+
sequence: []
|
|
193
196
|
};
|
|
194
197
|
const rewrittenExpressions = Array.isArray(compIr?.expressions) ? compIr.expressions : [];
|
|
195
198
|
const rewrittenBindings = Array.isArray(compIr?.expression_bindings) ? compIr.expression_bindings : [];
|
|
@@ -235,6 +238,12 @@ function buildComponentExpressionRewrite(compPath, componentSource, compIr, comp
|
|
|
235
238
|
}
|
|
236
239
|
: null;
|
|
237
240
|
|
|
241
|
+
out.sequence.push({
|
|
242
|
+
raw,
|
|
243
|
+
rewritten,
|
|
244
|
+
binding: normalizedBinding
|
|
245
|
+
});
|
|
246
|
+
|
|
238
247
|
if (!out.ambiguous.has(raw) && normalizedBinding) {
|
|
239
248
|
const existingBinding = out.bindings.get(raw);
|
|
240
249
|
if (existingBinding) {
|
|
@@ -2287,7 +2296,7 @@ export async function build(options) {
|
|
|
2287
2296
|
for (const entry of manifest) {
|
|
2288
2297
|
const sourceFile = join(pagesDir, entry.file);
|
|
2289
2298
|
const rawSource = readFileSync(sourceFile, 'utf8');
|
|
2290
|
-
const
|
|
2299
|
+
const componentOccurrences = collectExpandedComponentOccurrences(rawSource, registry, sourceFile);
|
|
2291
2300
|
|
|
2292
2301
|
const baseName = sourceFile.slice(0, -extname(sourceFile).length);
|
|
2293
2302
|
let adjacentGuard = null;
|
|
@@ -2298,7 +2307,7 @@ export async function build(options) {
|
|
|
2298
2307
|
}
|
|
2299
2308
|
|
|
2300
2309
|
// 2a. Expand PascalCase component tags
|
|
2301
|
-
const { expandedSource
|
|
2310
|
+
const { expandedSource } = expandComponents(
|
|
2302
2311
|
rawSource, registry, sourceFile
|
|
2303
2312
|
);
|
|
2304
2313
|
const extractedServer = extractServerScript(expandedSource, sourceFile, compilerOpts);
|
|
@@ -2352,10 +2361,17 @@ export async function build(options) {
|
|
|
2352
2361
|
pageIr.hoisted.state = pageIr.hoisted.state || [];
|
|
2353
2362
|
pageIr.hoisted.code = pageIr.hoisted.code || [];
|
|
2354
2363
|
const seenStaticImports = new Set();
|
|
2364
|
+
const occurrenceCountByPath = new Map();
|
|
2365
|
+
for (const occurrence of componentOccurrences) {
|
|
2366
|
+
const key = occurrence.componentPath || occurrence.name;
|
|
2367
|
+
occurrenceCountByPath.set(key, (occurrenceCountByPath.get(key) || 0) + 1);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2355
2370
|
const pageExpressionRewriteMap = new Map();
|
|
2356
2371
|
const pageExpressionBindingMap = new Map();
|
|
2357
2372
|
const pageAmbiguousExpressionMap = new Set();
|
|
2358
2373
|
const knownRefKeys = new Set();
|
|
2374
|
+
const componentOccurrencePlans = [];
|
|
2359
2375
|
const pageScopeRewrite = buildScopedIdentifierRewrite(pageIr);
|
|
2360
2376
|
const pageSelfExpressionRewrite = buildComponentExpressionRewrite(
|
|
2361
2377
|
sourceFile,
|
|
@@ -2372,12 +2388,15 @@ export async function build(options) {
|
|
|
2372
2388
|
pageIr
|
|
2373
2389
|
);
|
|
2374
2390
|
const componentScopeRewriteCache = new Map();
|
|
2391
|
+
let componentInstanceCounter = 0;
|
|
2375
2392
|
|
|
2376
2393
|
// 2c. Compile each used component separately for its script IR
|
|
2377
|
-
for (const
|
|
2378
|
-
const
|
|
2394
|
+
for (const occurrence of componentOccurrences) {
|
|
2395
|
+
const compName = occurrence.name;
|
|
2396
|
+
const compPath = occurrence.componentPath || registry.get(compName);
|
|
2379
2397
|
if (!compPath) continue;
|
|
2380
2398
|
const componentSource = readFileSync(compPath, 'utf8');
|
|
2399
|
+
const occurrenceCount = occurrenceCountByPath.get(compPath) || 0;
|
|
2381
2400
|
|
|
2382
2401
|
let compIr;
|
|
2383
2402
|
if (componentIrCache.has(compPath)) {
|
|
@@ -2414,15 +2433,10 @@ export async function build(options) {
|
|
|
2414
2433
|
componentExpressionRewriteCache.set(compPath, expressionRewrite);
|
|
2415
2434
|
}
|
|
2416
2435
|
|
|
2417
|
-
let usageEntry = (componentUsageAttrs.get(compName) || [])[0] || { attrs: '', ownerPath: sourceFile };
|
|
2418
|
-
if (!usageEntry || typeof usageEntry !== 'object') {
|
|
2419
|
-
usageEntry = { attrs: '', ownerPath: sourceFile };
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
2436
|
let attrExpressionRewrite = pageSelfExpressionRewrite;
|
|
2423
2437
|
let attrScopeRewrite = pageScopeRewrite;
|
|
2424
|
-
const ownerPath = typeof
|
|
2425
|
-
?
|
|
2438
|
+
const ownerPath = typeof occurrence.ownerPath === 'string' && occurrence.ownerPath.length > 0
|
|
2439
|
+
? occurrence.ownerPath
|
|
2426
2440
|
: sourceFile;
|
|
2427
2441
|
|
|
2428
2442
|
if (ownerPath !== sourceFile) {
|
|
@@ -2461,17 +2475,36 @@ export async function build(options) {
|
|
|
2461
2475
|
}
|
|
2462
2476
|
}
|
|
2463
2477
|
|
|
2478
|
+
const useIsolatedInstance = occurrenceCount > 1;
|
|
2479
|
+
const { ir: instanceIr, refIdentifierPairs } = useIsolatedInstance
|
|
2480
|
+
? cloneComponentIrForInstance(
|
|
2481
|
+
compIr,
|
|
2482
|
+
componentInstanceCounter++,
|
|
2483
|
+
extractDeclaredIdentifiers,
|
|
2484
|
+
resolveStateKeyFromBindings
|
|
2485
|
+
)
|
|
2486
|
+
: { ir: compIr, refIdentifierPairs: [] };
|
|
2487
|
+
const instanceRewrite = useIsolatedInstance
|
|
2488
|
+
? buildComponentExpressionRewrite(
|
|
2489
|
+
compPath,
|
|
2490
|
+
componentSource,
|
|
2491
|
+
instanceIr,
|
|
2492
|
+
compilerOpts,
|
|
2493
|
+
compilerBin
|
|
2494
|
+
)
|
|
2495
|
+
: expressionRewrite;
|
|
2496
|
+
|
|
2464
2497
|
// 2d. Merge component IR into page IR
|
|
2465
2498
|
mergeComponentIr(
|
|
2466
2499
|
pageIr,
|
|
2467
|
-
|
|
2500
|
+
instanceIr,
|
|
2468
2501
|
compPath,
|
|
2469
2502
|
sourceFile,
|
|
2470
2503
|
{
|
|
2471
2504
|
includeCode: true,
|
|
2472
2505
|
cssImportsOnly: isDocMode,
|
|
2473
2506
|
documentMode: isDocMode,
|
|
2474
|
-
componentAttrs: typeof
|
|
2507
|
+
componentAttrs: typeof occurrence.attrs === 'string' ? occurrence.attrs : '',
|
|
2475
2508
|
componentAttrsRewrite: {
|
|
2476
2509
|
expressionRewrite: attrExpressionRewrite,
|
|
2477
2510
|
scopeRewrite: attrScopeRewrite
|
|
@@ -2481,15 +2514,28 @@ export async function build(options) {
|
|
|
2481
2514
|
knownRefKeys
|
|
2482
2515
|
);
|
|
2483
2516
|
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2517
|
+
if (useIsolatedInstance) {
|
|
2518
|
+
componentOccurrencePlans.push({
|
|
2519
|
+
rewrite: instanceRewrite,
|
|
2520
|
+
expressionSequence: instanceRewrite.sequence,
|
|
2521
|
+
refSequence: refIdentifierPairs
|
|
2522
|
+
});
|
|
2523
|
+
} else {
|
|
2524
|
+
mergeExpressionRewriteMaps(
|
|
2525
|
+
pageExpressionRewriteMap,
|
|
2526
|
+
pageExpressionBindingMap,
|
|
2527
|
+
pageAmbiguousExpressionMap,
|
|
2528
|
+
expressionRewrite,
|
|
2529
|
+
pageIr
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2491
2532
|
}
|
|
2492
2533
|
|
|
2534
|
+
applyOccurrenceRewritePlans(
|
|
2535
|
+
pageIr,
|
|
2536
|
+
componentOccurrencePlans,
|
|
2537
|
+
(rewrite, binding) => resolveRewrittenBindingMetadata(pageIr, rewrite, binding)
|
|
2538
|
+
);
|
|
2493
2539
|
applyExpressionRewrites(
|
|
2494
2540
|
pageIr,
|
|
2495
2541
|
pageExpressionRewriteMap,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
function deepClone(value) {
|
|
2
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function escapeIdentifier(identifier) {
|
|
6
|
+
return identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function replaceIdentifierRefs(input, renameMap) {
|
|
10
|
+
let output = String(input || '');
|
|
11
|
+
const entries = [...renameMap.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
12
|
+
for (const [from, to] of entries) {
|
|
13
|
+
const pattern = new RegExp(`\\b${escapeIdentifier(from)}\\b`, 'g');
|
|
14
|
+
output = output.replace(pattern, to);
|
|
15
|
+
}
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectRenameTargets(compIr, extractDeclaredIdentifiers) {
|
|
20
|
+
const targets = new Set();
|
|
21
|
+
|
|
22
|
+
const stateBindings = Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [];
|
|
23
|
+
for (const entry of stateBindings) {
|
|
24
|
+
if (typeof entry?.key === 'string' && entry.key.length > 0) {
|
|
25
|
+
targets.add(entry.key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const functions = Array.isArray(compIr?.hoisted?.functions) ? compIr.hoisted.functions : [];
|
|
30
|
+
for (const fnName of functions) {
|
|
31
|
+
if (typeof fnName === 'string' && fnName.length > 0) {
|
|
32
|
+
targets.add(fnName);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const signals = Array.isArray(compIr?.hoisted?.signals) ? compIr.hoisted.signals : [];
|
|
37
|
+
for (const signalName of signals) {
|
|
38
|
+
if (typeof signalName === 'string' && signalName.length > 0) {
|
|
39
|
+
targets.add(signalName);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const declarations = Array.isArray(compIr?.hoisted?.declarations) ? compIr.hoisted.declarations : [];
|
|
44
|
+
for (const declaration of declarations) {
|
|
45
|
+
if (typeof declaration !== 'string') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const identifier of extractDeclaredIdentifiers(declaration)) {
|
|
49
|
+
targets.add(identifier);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [...targets];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildRefIdentifierMap(baseIr, renameMap, resolveStateKeyFromBindings) {
|
|
57
|
+
const baseState = Array.isArray(baseIr?.hoisted?.state) ? baseIr.hoisted.state : [];
|
|
58
|
+
const baseRefs = Array.isArray(baseIr?.ref_bindings) ? baseIr.ref_bindings : [];
|
|
59
|
+
|
|
60
|
+
return baseRefs.map((binding) => {
|
|
61
|
+
const raw = typeof binding?.identifier === 'string' ? binding.identifier : null;
|
|
62
|
+
const resolvedBase = raw ? resolveStateKeyFromBindings(raw, baseState) : null;
|
|
63
|
+
const rewritten = resolvedBase ? (renameMap.get(resolvedBase) || null) : null;
|
|
64
|
+
return {
|
|
65
|
+
raw,
|
|
66
|
+
rewritten: rewritten || null
|
|
67
|
+
};
|
|
68
|
+
}).filter((entry) => typeof entry.raw === 'string' && typeof entry.rewritten === 'string');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function cloneComponentIrForInstance(compIr, instanceId, extractDeclaredIdentifiers, resolveStateKeyFromBindings) {
|
|
72
|
+
const suffix = `__inst${instanceId}`;
|
|
73
|
+
const cloned = deepClone(compIr);
|
|
74
|
+
const renameTargets = collectRenameTargets(compIr, extractDeclaredIdentifiers);
|
|
75
|
+
const renameMap = new Map(renameTargets.map((name) => [name, `${name}${suffix}`]));
|
|
76
|
+
|
|
77
|
+
if (Array.isArray(cloned?.expressions)) {
|
|
78
|
+
cloned.expressions = cloned.expressions.map((expr) => replaceIdentifierRefs(expr, renameMap));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(cloned?.expression_bindings)) {
|
|
82
|
+
cloned.expression_bindings = cloned.expression_bindings.map((binding) => {
|
|
83
|
+
if (!binding || typeof binding !== 'object') {
|
|
84
|
+
return binding;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...binding,
|
|
88
|
+
literal: typeof binding.literal === 'string' ? replaceIdentifierRefs(binding.literal, renameMap) : binding.literal,
|
|
89
|
+
compiled_expr: typeof binding.compiled_expr === 'string'
|
|
90
|
+
? replaceIdentifierRefs(binding.compiled_expr, renameMap)
|
|
91
|
+
: binding.compiled_expr,
|
|
92
|
+
component_instance: typeof binding.component_instance === 'string'
|
|
93
|
+
? replaceIdentifierRefs(binding.component_instance, renameMap)
|
|
94
|
+
: binding.component_instance,
|
|
95
|
+
component_binding: typeof binding.component_binding === 'string'
|
|
96
|
+
? replaceIdentifierRefs(binding.component_binding, renameMap)
|
|
97
|
+
: binding.component_binding
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (cloned?.hoisted) {
|
|
103
|
+
if (Array.isArray(cloned.hoisted.declarations)) {
|
|
104
|
+
cloned.hoisted.declarations = cloned.hoisted.declarations.map((line) => replaceIdentifierRefs(line, renameMap));
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(cloned.hoisted.functions)) {
|
|
107
|
+
cloned.hoisted.functions = cloned.hoisted.functions.map((name) => renameMap.get(name) || name);
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(cloned.hoisted.signals)) {
|
|
110
|
+
cloned.hoisted.signals = cloned.hoisted.signals.map((name) => renameMap.get(name) || name);
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(cloned.hoisted.state)) {
|
|
113
|
+
cloned.hoisted.state = cloned.hoisted.state.map((entry) => {
|
|
114
|
+
if (!entry || typeof entry !== 'object') {
|
|
115
|
+
return entry;
|
|
116
|
+
}
|
|
117
|
+
const key = typeof entry.key === 'string' ? (renameMap.get(entry.key) || entry.key) : entry.key;
|
|
118
|
+
const value = typeof entry.value === 'string' ? replaceIdentifierRefs(entry.value, renameMap) : entry.value;
|
|
119
|
+
return { ...entry, key, value };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(cloned.hoisted.code)) {
|
|
123
|
+
cloned.hoisted.code = cloned.hoisted.code.map((line) => replaceIdentifierRefs(line, renameMap));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(cloned?.ref_bindings)) {
|
|
128
|
+
const clonedState = Array.isArray(cloned?.hoisted?.state) ? cloned.hoisted.state : [];
|
|
129
|
+
cloned.ref_bindings = cloned.ref_bindings.map((binding) => {
|
|
130
|
+
if (!binding || typeof binding !== 'object' || typeof binding.identifier !== 'string') {
|
|
131
|
+
return binding;
|
|
132
|
+
}
|
|
133
|
+
const resolved = resolveStateKeyFromBindings(binding.identifier, clonedState);
|
|
134
|
+
return {
|
|
135
|
+
...binding,
|
|
136
|
+
identifier: resolved || binding.identifier
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const refIdentifierPairs = buildRefIdentifierMap(compIr, renameMap, resolveStateKeyFromBindings);
|
|
142
|
+
return {
|
|
143
|
+
ir: cloned,
|
|
144
|
+
renameMap,
|
|
145
|
+
refIdentifierPairs
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function applyOccurrenceRewritePlans(pageIr, occurrencePlans, resolveBindingMetadata) {
|
|
150
|
+
const expressions = Array.isArray(pageIr?.expressions) ? pageIr.expressions : [];
|
|
151
|
+
const bindings = Array.isArray(pageIr?.expression_bindings) ? pageIr.expression_bindings : [];
|
|
152
|
+
const refBindings = Array.isArray(pageIr?.ref_bindings) ? pageIr.ref_bindings : [];
|
|
153
|
+
|
|
154
|
+
let exprCursor = 0;
|
|
155
|
+
let refCursor = 0;
|
|
156
|
+
|
|
157
|
+
for (const plan of occurrencePlans) {
|
|
158
|
+
const sequence = Array.isArray(plan?.expressionSequence) ? plan.expressionSequence : [];
|
|
159
|
+
for (const item of sequence) {
|
|
160
|
+
if (typeof item?.raw !== 'string') {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
let found = -1;
|
|
164
|
+
for (let index = exprCursor; index < expressions.length; index++) {
|
|
165
|
+
if (expressions[index] === item.raw) {
|
|
166
|
+
found = index;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (found === -1) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const rewritten = typeof item.rewritten === 'string' && item.rewritten.length > 0
|
|
174
|
+
? item.rewritten
|
|
175
|
+
: item.raw;
|
|
176
|
+
expressions[found] = rewritten;
|
|
177
|
+
const binding = bindings[found];
|
|
178
|
+
if (binding && typeof binding === 'object') {
|
|
179
|
+
if (binding.literal === item.raw) {
|
|
180
|
+
binding.literal = rewritten;
|
|
181
|
+
}
|
|
182
|
+
if (binding.compiled_expr === item.raw) {
|
|
183
|
+
binding.compiled_expr = rewritten;
|
|
184
|
+
}
|
|
185
|
+
const resolved = resolveBindingMetadata(plan.rewrite, item.binding);
|
|
186
|
+
if (resolved) {
|
|
187
|
+
binding.compiled_expr = resolved.compiled_expr;
|
|
188
|
+
binding.signal_index = resolved.signal_index;
|
|
189
|
+
binding.signal_indices = resolved.signal_indices;
|
|
190
|
+
binding.state_index = resolved.state_index;
|
|
191
|
+
binding.component_instance = resolved.component_instance;
|
|
192
|
+
binding.component_binding = resolved.component_binding;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exprCursor = found + 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const refSequence = Array.isArray(plan?.refSequence) ? plan.refSequence : [];
|
|
199
|
+
for (const refItem of refSequence) {
|
|
200
|
+
if (typeof refItem?.raw !== 'string' || typeof refItem?.rewritten !== 'string') {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
let found = -1;
|
|
204
|
+
for (let index = refCursor; index < refBindings.length; index++) {
|
|
205
|
+
if (refBindings[index]?.identifier === refItem.raw) {
|
|
206
|
+
found = index;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (found === -1) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
refBindings[found].identifier = refItem.rewritten;
|
|
214
|
+
refCursor = found + 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { extractTemplate, isDocumentMode } from './resolve-components.js';
|
|
3
|
+
|
|
4
|
+
const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
|
|
5
|
+
|
|
6
|
+
export function collectExpandedComponentOccurrences(source, registry, sourceFile) {
|
|
7
|
+
/** @type {Array<{ name: string, attrs: string, ownerPath: string, componentPath: string }>} */
|
|
8
|
+
const occurrences = [];
|
|
9
|
+
walkSource(String(source || ''), registry, sourceFile, [], occurrences);
|
|
10
|
+
return occurrences;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function walkSource(source, registry, sourceFile, chain, occurrences) {
|
|
14
|
+
let cursor = 0;
|
|
15
|
+
|
|
16
|
+
while (cursor < source.length) {
|
|
17
|
+
const tag = findNextKnownTag(source, registry, cursor);
|
|
18
|
+
if (!tag) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let children = '';
|
|
23
|
+
let replaceEnd = tag.end;
|
|
24
|
+
if (!tag.selfClosing) {
|
|
25
|
+
const close = findMatchingClose(source, tag.name, tag.end);
|
|
26
|
+
if (!close) {
|
|
27
|
+
throw new Error(`Unclosed component tag <${tag.name}> in ${sourceFile} at offset ${tag.start}`);
|
|
28
|
+
}
|
|
29
|
+
children = source.slice(tag.end, close.contentEnd);
|
|
30
|
+
replaceEnd = close.tagEnd;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const compPath = registry.get(tag.name);
|
|
34
|
+
if (!compPath) {
|
|
35
|
+
throw new Error(`Unknown component "${tag.name}" referenced in ${sourceFile}`);
|
|
36
|
+
}
|
|
37
|
+
if (chain.includes(tag.name)) {
|
|
38
|
+
const cycle = [...chain, tag.name].join(' -> ');
|
|
39
|
+
throw new Error(`Circular component dependency detected: ${cycle}\nFile: ${sourceFile}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
occurrences.push({
|
|
43
|
+
name: tag.name,
|
|
44
|
+
attrs: String(tag.attrs || '').trim(),
|
|
45
|
+
ownerPath: sourceFile,
|
|
46
|
+
componentPath: compPath
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const compSource = readFileSync(compPath, 'utf8');
|
|
50
|
+
const nextTemplate = materializeTemplate(compSource, tag.name, children, compPath);
|
|
51
|
+
walkSource(nextTemplate, registry, compPath, [...chain, tag.name], occurrences);
|
|
52
|
+
cursor = replaceEnd;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function materializeTemplate(componentSource, name, children, componentPath) {
|
|
57
|
+
let template = extractTemplate(componentSource);
|
|
58
|
+
const slotCount = countSlots(template);
|
|
59
|
+
|
|
60
|
+
if (isDocumentMode(template)) {
|
|
61
|
+
if (slotCount !== 1) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Document Mode component "${name}" must contain exactly one <slot />, found ${slotCount}.\nFile: ${componentPath}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return replaceSlot(template, children);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (children.trim().length > 0 && slotCount === 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Component "${name}" has children but its template has no <slot />.\nEither add <slot /> to ${componentPath} or make the tag self-closing.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (slotCount > 0) {
|
|
76
|
+
template = replaceSlot(template, children || '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return template;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findNextKnownTag(source, registry, startIndex) {
|
|
83
|
+
OPEN_COMPONENT_TAG_RE.lastIndex = startIndex;
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = OPEN_COMPONENT_TAG_RE.exec(source)) !== null) {
|
|
86
|
+
const name = match[1];
|
|
87
|
+
if (!registry.has(name)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (isInsideExpressionScope(source, match.index)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
attrs: String(match[2] || ''),
|
|
96
|
+
start: match.index,
|
|
97
|
+
end: OPEN_COMPONENT_TAG_RE.lastIndex,
|
|
98
|
+
selfClosing: match[3] === '/'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isInsideExpressionScope(source, index) {
|
|
105
|
+
let depth = 0;
|
|
106
|
+
for (let i = 0; i < index; i++) {
|
|
107
|
+
if (source[i] === '{') {
|
|
108
|
+
depth += 1;
|
|
109
|
+
} else if (source[i] === '}') {
|
|
110
|
+
depth = Math.max(0, depth - 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return depth > 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findMatchingClose(source, tagName, startAfterOpen) {
|
|
117
|
+
let depth = 1;
|
|
118
|
+
const escapedName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
119
|
+
const tagRe = new RegExp(`<(/?)${escapedName}(?:\\s[^<>]*?)?\\s*(/?)>`, 'g');
|
|
120
|
+
tagRe.lastIndex = startAfterOpen;
|
|
121
|
+
|
|
122
|
+
let match;
|
|
123
|
+
while ((match = tagRe.exec(source)) !== null) {
|
|
124
|
+
const isClose = match[1] === '/';
|
|
125
|
+
const isSelfClose = match[2] === '/';
|
|
126
|
+
if (isSelfClose && !isClose) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (isClose) {
|
|
130
|
+
depth -= 1;
|
|
131
|
+
if (depth === 0) {
|
|
132
|
+
return {
|
|
133
|
+
contentEnd: match.index,
|
|
134
|
+
tagEnd: match.index + match[0].length
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
depth += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function countSlots(template) {
|
|
146
|
+
const matches = template.match(/<slot\s*>\s*<\/slot>|<slot\s*\/>|<slot\s*>/gi);
|
|
147
|
+
return matches ? matches.length : 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function replaceSlot(template, content) {
|
|
151
|
+
return template.replace(/<slot\s*>\s*<\/slot>|<slot\s*\/>|<slot\s*>/i, content);
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Deterministic project orchestrator for Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"prepublishOnly": "npm run build"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@zenithbuild/compiler": "0.6.
|
|
27
|
+
"@zenithbuild/compiler": "^0.6.5",
|
|
28
28
|
"picocolors": "^1.1.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|