@zenithbuild/cli 0.6.10 → 0.6.12
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 +101 -12
- package/dist/component-occurrences.js +3 -63
- package/dist/component-tag-parser.d.ts +13 -0
- package/dist/component-tag-parser.js +240 -0
- package/dist/resolve-components.js +3 -165
- package/package.json +2 -2
package/dist/build.js
CHANGED
|
@@ -18,6 +18,7 @@ import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
|
18
18
|
import { generateManifest } from './manifest.js';
|
|
19
19
|
import { buildComponentRegistry, expandComponents, extractTemplate, isDocumentMode } from './resolve-components.js';
|
|
20
20
|
import { collectExpandedComponentOccurrences } from './component-occurrences.js';
|
|
21
|
+
import { findNextKnownComponentTag } from './component-tag-parser.js';
|
|
21
22
|
import { applyOccurrenceRewritePlans, cloneComponentIrForInstance } from './component-instance-ir.js';
|
|
22
23
|
import { resolveBundlerBin } from './toolchain-paths.js';
|
|
23
24
|
import { createBundlerToolchain, createCompilerToolchain, ensureToolchainCompatibility, getActiveToolchainCandidate, runToolchainSync } from './toolchain-runner.js';
|
|
@@ -1056,7 +1057,6 @@ function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
|
1056
1057
|
}
|
|
1057
1058
|
};
|
|
1058
1059
|
}
|
|
1059
|
-
const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
|
|
1060
1060
|
/**
|
|
1061
1061
|
* Collect original attribute strings for component usages in a page source.
|
|
1062
1062
|
*
|
|
@@ -1067,18 +1067,19 @@ const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
|
|
|
1067
1067
|
*/
|
|
1068
1068
|
function collectComponentUsageAttrs(source, registry, ownerPath = null) {
|
|
1069
1069
|
const out = new Map();
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
continue;
|
|
1070
|
+
let cursor = 0;
|
|
1071
|
+
while (cursor < source.length) {
|
|
1072
|
+
const tag = findNextKnownComponentTag(source, registry, cursor);
|
|
1073
|
+
if (!tag) {
|
|
1074
|
+
break;
|
|
1076
1075
|
}
|
|
1077
|
-
const
|
|
1076
|
+
const name = tag.name;
|
|
1077
|
+
const attrs = String(tag.attrs || '').trim();
|
|
1078
1078
|
if (!out.has(name)) {
|
|
1079
1079
|
out.set(name, []);
|
|
1080
1080
|
}
|
|
1081
1081
|
out.get(name).push({ attrs, ownerPath });
|
|
1082
|
+
cursor = tag.end;
|
|
1082
1083
|
}
|
|
1083
1084
|
return out;
|
|
1084
1085
|
}
|
|
@@ -1774,6 +1775,82 @@ function rewritePropsExpression(expr, rewriteContext = null) {
|
|
|
1774
1775
|
}
|
|
1775
1776
|
return `${rewrittenRoot}${rootMatch[2]}`;
|
|
1776
1777
|
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Translate compiler `compiled_expr` output into module-scope JS for props
|
|
1780
|
+
* preludes. Expression bindings use `signalMap.get(i)` at hydrate-time, but
|
|
1781
|
+
* component props need direct access to the already-hoisted scoped symbols.
|
|
1782
|
+
*
|
|
1783
|
+
* @param {string | null | undefined} compiledExpr
|
|
1784
|
+
* @param {{
|
|
1785
|
+
* signals?: Array<{ state_index?: number }>,
|
|
1786
|
+
* stateBindings?: Array<{ key?: string }>
|
|
1787
|
+
* } | null | undefined} expressionRewrite
|
|
1788
|
+
* @returns {string | null}
|
|
1789
|
+
*/
|
|
1790
|
+
function resolveCompiledPropsExpression(compiledExpr, expressionRewrite = null) {
|
|
1791
|
+
const source = typeof compiledExpr === 'string' ? compiledExpr.trim() : '';
|
|
1792
|
+
if (!source) {
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
const signals = Array.isArray(expressionRewrite?.signals) ? expressionRewrite.signals : [];
|
|
1796
|
+
const stateBindings = Array.isArray(expressionRewrite?.stateBindings) ? expressionRewrite.stateBindings : [];
|
|
1797
|
+
return source.replace(/signalMap\.get\((\d+)\)(?:\.get\(\))?/g, (full, rawIndex) => {
|
|
1798
|
+
const signalIndex = Number.parseInt(rawIndex, 10);
|
|
1799
|
+
if (!Number.isInteger(signalIndex)) {
|
|
1800
|
+
return full;
|
|
1801
|
+
}
|
|
1802
|
+
const signal = signals[signalIndex];
|
|
1803
|
+
const stateIndex = signal?.state_index;
|
|
1804
|
+
const stateKey = Number.isInteger(stateIndex) ? stateBindings[stateIndex]?.key : null;
|
|
1805
|
+
if (typeof stateKey !== 'string' || stateKey.length === 0) {
|
|
1806
|
+
return full;
|
|
1807
|
+
}
|
|
1808
|
+
return full.endsWith('.get()') ? `${stateKey}.get()` : stateKey;
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Resolve a raw component prop expression to the same scoped symbol/expression
|
|
1813
|
+
* contract used by the compiler rename pass.
|
|
1814
|
+
*
|
|
1815
|
+
* @param {string} expr
|
|
1816
|
+
* @param {{
|
|
1817
|
+
* expressionRewrite?: {
|
|
1818
|
+
* map?: Map<string, string>,
|
|
1819
|
+
* bindings?: Map<string, {
|
|
1820
|
+
* compiled_expr?: string | null
|
|
1821
|
+
* }>,
|
|
1822
|
+
* ambiguous?: Set<string>,
|
|
1823
|
+
* signals?: Array<{ state_index?: number }>,
|
|
1824
|
+
* stateBindings?: Array<{ key?: string }>
|
|
1825
|
+
* } | null,
|
|
1826
|
+
* scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
|
|
1827
|
+
* } | null} rewriteContext
|
|
1828
|
+
* @returns {string}
|
|
1829
|
+
*/
|
|
1830
|
+
function resolvePropsValueCode(expr, rewriteContext = null) {
|
|
1831
|
+
const trimmed = String(expr || '').trim();
|
|
1832
|
+
if (!trimmed) {
|
|
1833
|
+
return trimmed;
|
|
1834
|
+
}
|
|
1835
|
+
const expressionRewrite = rewriteContext?.expressionRewrite;
|
|
1836
|
+
const expressionAmbiguous = expressionRewrite?.ambiguous;
|
|
1837
|
+
if (!(expressionAmbiguous instanceof Set && expressionAmbiguous.has(trimmed))) {
|
|
1838
|
+
const binding = expressionRewrite?.bindings instanceof Map
|
|
1839
|
+
? expressionRewrite.bindings.get(trimmed)
|
|
1840
|
+
: null;
|
|
1841
|
+
const compiled = resolveCompiledPropsExpression(binding?.compiled_expr, expressionRewrite);
|
|
1842
|
+
if (typeof compiled === 'string' && compiled.length > 0) {
|
|
1843
|
+
return compiled;
|
|
1844
|
+
}
|
|
1845
|
+
const exact = expressionRewrite?.map instanceof Map
|
|
1846
|
+
? expressionRewrite.map.get(trimmed)
|
|
1847
|
+
: null;
|
|
1848
|
+
if (typeof exact === 'string' && exact.length > 0) {
|
|
1849
|
+
return exact;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return rewritePropsExpression(trimmed, rewriteContext);
|
|
1853
|
+
}
|
|
1777
1854
|
/**
|
|
1778
1855
|
* @param {string} attrs
|
|
1779
1856
|
* @param {{
|
|
@@ -1807,7 +1884,7 @@ function renderPropsLiteralFromAttrs(attrs, rewriteContext = null) {
|
|
|
1807
1884
|
}
|
|
1808
1885
|
else if (expressionValue !== undefined) {
|
|
1809
1886
|
const trimmed = String(expressionValue).trim();
|
|
1810
|
-
valueCode = trimmed.length > 0 ?
|
|
1887
|
+
valueCode = trimmed.length > 0 ? resolvePropsValueCode(trimmed, rewriteContext) : 'undefined';
|
|
1811
1888
|
}
|
|
1812
1889
|
entries.push(`${renderObjectKey(rawName)}: ${valueCode}`);
|
|
1813
1890
|
}
|
|
@@ -2039,6 +2116,7 @@ export async function build(options) {
|
|
|
2039
2116
|
const sourceFile = join(pagesDir, entry.file);
|
|
2040
2117
|
const rawSource = readFileSync(sourceFile, 'utf8');
|
|
2041
2118
|
const componentOccurrences = collectExpandedComponentOccurrences(rawSource, registry, sourceFile);
|
|
2119
|
+
const pageOwnerSource = extractServerScript(rawSource, sourceFile, compilerOpts).source;
|
|
2042
2120
|
const baseName = sourceFile.slice(0, -extname(sourceFile).length);
|
|
2043
2121
|
let adjacentGuard = null;
|
|
2044
2122
|
let adjacentLoad = null;
|
|
@@ -2098,7 +2176,18 @@ export async function build(options) {
|
|
|
2098
2176
|
const pageAmbiguousExpressionMap = new Set();
|
|
2099
2177
|
const knownRefKeys = new Set();
|
|
2100
2178
|
const componentOccurrencePlans = [];
|
|
2101
|
-
const
|
|
2179
|
+
const pageOwnerIr = componentOccurrences.length > 0
|
|
2180
|
+
? runCompiler(sourceFile, pageOwnerSource, compilerOpts, {
|
|
2181
|
+
suppressWarnings: true,
|
|
2182
|
+
compilerToolchain: compilerBin
|
|
2183
|
+
})
|
|
2184
|
+
: null;
|
|
2185
|
+
const pageOwnerExpressionRewrite = pageOwnerIr
|
|
2186
|
+
? buildComponentExpressionRewrite(sourceFile, pageOwnerSource, pageOwnerIr, compilerOpts, compilerBin)
|
|
2187
|
+
: { map: new Map(), bindings: new Map(), signals: [], stateBindings: [], ambiguous: new Set(), sequence: [] };
|
|
2188
|
+
const pageOwnerScopeRewrite = pageOwnerIr
|
|
2189
|
+
? buildScopedIdentifierRewrite(pageOwnerIr)
|
|
2190
|
+
: { map: new Map(), ambiguous: new Set() };
|
|
2102
2191
|
const pageSelfExpressionRewrite = buildComponentExpressionRewrite(sourceFile, compileSource, pageIr, compilerOpts, compilerBin);
|
|
2103
2192
|
mergeExpressionRewriteMaps(pageExpressionRewriteMap, pageExpressionBindingMap, pageAmbiguousExpressionMap, pageSelfExpressionRewrite, pageIr);
|
|
2104
2193
|
const componentScopeRewriteCache = new Map();
|
|
@@ -2133,8 +2222,8 @@ export async function build(options) {
|
|
|
2133
2222
|
expressionRewrite = buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts, compilerBin);
|
|
2134
2223
|
componentExpressionRewriteCache.set(compPath, expressionRewrite);
|
|
2135
2224
|
}
|
|
2136
|
-
let attrExpressionRewrite =
|
|
2137
|
-
let attrScopeRewrite =
|
|
2225
|
+
let attrExpressionRewrite = pageOwnerExpressionRewrite;
|
|
2226
|
+
let attrScopeRewrite = pageOwnerScopeRewrite;
|
|
2138
2227
|
const ownerPath = typeof occurrence.ownerPath === 'string' && occurrence.ownerPath.length > 0
|
|
2139
2228
|
? occurrence.ownerPath
|
|
2140
2229
|
: sourceFile;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { findMatchingComponentClose, findNextKnownComponentTag } from './component-tag-parser.js';
|
|
2
3
|
import { extractTemplate, isDocumentMode } from './resolve-components.js';
|
|
3
|
-
const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
|
|
4
4
|
export function collectExpandedComponentOccurrences(source, registry, sourceFile) {
|
|
5
5
|
/** @type {Array<{ name: string, attrs: string, ownerPath: string, componentPath: string }>} */
|
|
6
6
|
const occurrences = [];
|
|
@@ -10,14 +10,14 @@ export function collectExpandedComponentOccurrences(source, registry, sourceFile
|
|
|
10
10
|
function walkSource(source, registry, sourceFile, chain, occurrences) {
|
|
11
11
|
let cursor = 0;
|
|
12
12
|
while (cursor < source.length) {
|
|
13
|
-
const tag =
|
|
13
|
+
const tag = findNextKnownComponentTag(source, registry, cursor);
|
|
14
14
|
if (!tag) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
let children = '';
|
|
18
18
|
let replaceEnd = tag.end;
|
|
19
19
|
if (!tag.selfClosing) {
|
|
20
|
-
const close =
|
|
20
|
+
const close = findMatchingComponentClose(source, tag.name, tag.end);
|
|
21
21
|
if (!close) {
|
|
22
22
|
throw new Error(`Unclosed component tag <${tag.name}> in ${sourceFile} at offset ${tag.start}`);
|
|
23
23
|
}
|
|
@@ -61,66 +61,6 @@ function materializeTemplate(componentSource, name, children, componentPath) {
|
|
|
61
61
|
}
|
|
62
62
|
return template;
|
|
63
63
|
}
|
|
64
|
-
function findNextKnownTag(source, registry, startIndex) {
|
|
65
|
-
OPEN_COMPONENT_TAG_RE.lastIndex = startIndex;
|
|
66
|
-
let match;
|
|
67
|
-
while ((match = OPEN_COMPONENT_TAG_RE.exec(source)) !== null) {
|
|
68
|
-
const name = match[1];
|
|
69
|
-
if (!registry.has(name)) {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (isInsideExpressionScope(source, match.index)) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
name,
|
|
77
|
-
attrs: String(match[2] || ''),
|
|
78
|
-
start: match.index,
|
|
79
|
-
end: OPEN_COMPONENT_TAG_RE.lastIndex,
|
|
80
|
-
selfClosing: match[3] === '/'
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
function isInsideExpressionScope(source, index) {
|
|
86
|
-
let depth = 0;
|
|
87
|
-
for (let i = 0; i < index; i++) {
|
|
88
|
-
if (source[i] === '{') {
|
|
89
|
-
depth += 1;
|
|
90
|
-
}
|
|
91
|
-
else if (source[i] === '}') {
|
|
92
|
-
depth = Math.max(0, depth - 1);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return depth > 0;
|
|
96
|
-
}
|
|
97
|
-
function findMatchingClose(source, tagName, startAfterOpen) {
|
|
98
|
-
let depth = 1;
|
|
99
|
-
const escapedName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
100
|
-
const tagRe = new RegExp(`<(/?)${escapedName}(?:\\s[^<>]*?)?\\s*(/?)>`, 'g');
|
|
101
|
-
tagRe.lastIndex = startAfterOpen;
|
|
102
|
-
let match;
|
|
103
|
-
while ((match = tagRe.exec(source)) !== null) {
|
|
104
|
-
const isClose = match[1] === '/';
|
|
105
|
-
const isSelfClose = match[2] === '/';
|
|
106
|
-
if (isSelfClose && !isClose) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (isClose) {
|
|
110
|
-
depth -= 1;
|
|
111
|
-
if (depth === 0) {
|
|
112
|
-
return {
|
|
113
|
-
contentEnd: match.index,
|
|
114
|
-
tagEnd: match.index + match[0].length
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
depth += 1;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
64
|
function countSlots(template) {
|
|
125
65
|
const matches = template.match(/<slot\s*>\s*<\/slot>|<slot\s*\/>|<slot\s*>/gi);
|
|
126
66
|
return matches ? matches.length : 0;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function isInsideExpressionScope(source: any, index: any): boolean;
|
|
2
|
+
export function findNextKnownComponentTag(source: any, registry: any, startIndex?: number): {
|
|
3
|
+
name: any;
|
|
4
|
+
attrs: any;
|
|
5
|
+
start: any;
|
|
6
|
+
end: any;
|
|
7
|
+
isClose: boolean;
|
|
8
|
+
selfClosing: any;
|
|
9
|
+
} | null;
|
|
10
|
+
export function findMatchingComponentClose(source: any, tagName: any, startAfterOpen: any): {
|
|
11
|
+
contentEnd: any;
|
|
12
|
+
tagEnd: any;
|
|
13
|
+
} | null;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
function isNameStart(ch) {
|
|
2
|
+
return /[A-Z]/.test(ch);
|
|
3
|
+
}
|
|
4
|
+
function isNameChar(ch) {
|
|
5
|
+
return /[a-zA-Z0-9]/.test(ch);
|
|
6
|
+
}
|
|
7
|
+
export function isInsideExpressionScope(source, index) {
|
|
8
|
+
let depth = 0;
|
|
9
|
+
let mode = 'code';
|
|
10
|
+
let escaped = false;
|
|
11
|
+
const lower = source.toLowerCase();
|
|
12
|
+
for (let i = 0; i < index; i++) {
|
|
13
|
+
if (mode === 'code') {
|
|
14
|
+
if (lower.startsWith('<script', i)) {
|
|
15
|
+
const close = lower.indexOf('</script>', i + 7);
|
|
16
|
+
if (close < 0 || close >= index) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
i = close + '</script>'.length - 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (lower.startsWith('<style', i)) {
|
|
23
|
+
const close = lower.indexOf('</style>', i + 6);
|
|
24
|
+
if (close < 0 || close >= index) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
i = close + '</style>'.length - 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const ch = source[i];
|
|
32
|
+
const next = i + 1 < index ? source[i + 1] : '';
|
|
33
|
+
if (mode === 'line-comment') {
|
|
34
|
+
if (ch === '\n') {
|
|
35
|
+
mode = 'code';
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (mode === 'block-comment') {
|
|
40
|
+
if (ch === '*' && next === '/') {
|
|
41
|
+
mode = 'code';
|
|
42
|
+
i += 1;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (mode === 'single-quote' || mode === 'double-quote' || mode === 'template') {
|
|
47
|
+
if (escaped) {
|
|
48
|
+
escaped = false;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === '\\') {
|
|
52
|
+
escaped = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if ((mode === 'single-quote' && ch === "'") ||
|
|
56
|
+
(mode === 'double-quote' && ch === '"') ||
|
|
57
|
+
(mode === 'template' && ch === '`')) {
|
|
58
|
+
mode = 'code';
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === '/' && next === '/') {
|
|
63
|
+
mode = 'line-comment';
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ch === '/' && next === '*') {
|
|
68
|
+
mode = 'block-comment';
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch === "'") {
|
|
73
|
+
mode = 'single-quote';
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === '"') {
|
|
77
|
+
mode = 'double-quote';
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === '`') {
|
|
81
|
+
mode = 'template';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '{') {
|
|
85
|
+
depth += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === '}') {
|
|
89
|
+
depth = Math.max(0, depth - 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return depth > 0;
|
|
93
|
+
}
|
|
94
|
+
function parseComponentTagAt(source, index) {
|
|
95
|
+
if (source[index] !== '<') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
let cursor = index + 1;
|
|
99
|
+
let isClose = false;
|
|
100
|
+
if (source[cursor] === '/') {
|
|
101
|
+
isClose = true;
|
|
102
|
+
cursor += 1;
|
|
103
|
+
}
|
|
104
|
+
const nameStart = cursor;
|
|
105
|
+
if (!isNameStart(source[cursor] || '')) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
cursor += 1;
|
|
109
|
+
while (isNameChar(source[cursor] || '')) {
|
|
110
|
+
cursor += 1;
|
|
111
|
+
}
|
|
112
|
+
const name = source.slice(nameStart, cursor);
|
|
113
|
+
const attrStart = cursor;
|
|
114
|
+
let mode = 'code';
|
|
115
|
+
let escaped = false;
|
|
116
|
+
let exprDepth = 0;
|
|
117
|
+
for (; cursor < source.length; cursor += 1) {
|
|
118
|
+
const ch = source[cursor];
|
|
119
|
+
const next = source[cursor + 1] || '';
|
|
120
|
+
if (mode === 'line-comment') {
|
|
121
|
+
if (ch === '\n') {
|
|
122
|
+
mode = 'code';
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (mode === 'block-comment') {
|
|
127
|
+
if (ch === '*' && next === '/') {
|
|
128
|
+
mode = 'code';
|
|
129
|
+
cursor += 1;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (mode === 'single-quote' || mode === 'double-quote' || mode === 'template') {
|
|
134
|
+
if (escaped) {
|
|
135
|
+
escaped = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (ch === '\\') {
|
|
139
|
+
escaped = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if ((mode === 'single-quote' && ch === "'") ||
|
|
143
|
+
(mode === 'double-quote' && ch === '"') ||
|
|
144
|
+
(mode === 'template' && ch === '`')) {
|
|
145
|
+
mode = 'code';
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (exprDepth > 0 && ch === '/' && next === '/') {
|
|
150
|
+
mode = 'line-comment';
|
|
151
|
+
cursor += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (exprDepth > 0 && ch === '/' && next === '*') {
|
|
155
|
+
mode = 'block-comment';
|
|
156
|
+
cursor += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === "'") {
|
|
160
|
+
mode = 'single-quote';
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === '"') {
|
|
164
|
+
mode = 'double-quote';
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (ch === '`') {
|
|
168
|
+
mode = 'template';
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === '{') {
|
|
172
|
+
exprDepth += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (ch === '}') {
|
|
176
|
+
exprDepth = Math.max(0, exprDepth - 1);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (exprDepth === 0 && ch === '>') {
|
|
180
|
+
const rawAttrs = source.slice(attrStart, cursor);
|
|
181
|
+
const trimmed = rawAttrs.trimEnd();
|
|
182
|
+
const selfClosing = !isClose && trimmed.endsWith('/');
|
|
183
|
+
const attrs = selfClosing ? trimmed.slice(0, -1) : rawAttrs;
|
|
184
|
+
return {
|
|
185
|
+
name,
|
|
186
|
+
attrs,
|
|
187
|
+
start: index,
|
|
188
|
+
end: cursor + 1,
|
|
189
|
+
isClose,
|
|
190
|
+
selfClosing
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
export function findNextKnownComponentTag(source, registry, startIndex = 0) {
|
|
197
|
+
for (let index = startIndex; index < source.length; index += 1) {
|
|
198
|
+
if (source[index] !== '<') {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const tag = parseComponentTagAt(source, index);
|
|
202
|
+
if (!tag || tag.isClose || !registry.has(tag.name)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (isInsideExpressionScope(source, index)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
return tag;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
export function findMatchingComponentClose(source, tagName, startAfterOpen) {
|
|
213
|
+
let depth = 1;
|
|
214
|
+
for (let index = startAfterOpen; index < source.length; index += 1) {
|
|
215
|
+
if (source[index] !== '<') {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const tag = parseComponentTagAt(source, index);
|
|
219
|
+
if (!tag || tag.name !== tagName) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (isInsideExpressionScope(source, index)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (tag.isClose) {
|
|
226
|
+
depth -= 1;
|
|
227
|
+
if (depth === 0) {
|
|
228
|
+
return {
|
|
229
|
+
contentEnd: index,
|
|
230
|
+
tagEnd: tag.end
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!tag.selfClosing) {
|
|
236
|
+
depth += 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
12
|
import { basename, extname, join } from 'node:path';
|
|
13
|
+
import { findMatchingComponentClose, findNextKnownComponentTag } from './component-tag-parser.js';
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Registry: Map<PascalCaseName, absolutePath>
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -127,7 +128,6 @@ export function isDocumentMode(template) {
|
|
|
127
128
|
// ---------------------------------------------------------------------------
|
|
128
129
|
// Component expansion
|
|
129
130
|
// ---------------------------------------------------------------------------
|
|
130
|
-
const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
|
|
131
131
|
/**
|
|
132
132
|
* Recursively expand PascalCase component tags in `source`.
|
|
133
133
|
*
|
|
@@ -164,14 +164,14 @@ function expandSource(source, registry, sourceFile, chain, usedComponents) {
|
|
|
164
164
|
const MAX_ITERATIONS = 10_000;
|
|
165
165
|
while (iterations < MAX_ITERATIONS) {
|
|
166
166
|
iterations += 1;
|
|
167
|
-
const tag =
|
|
167
|
+
const tag = findNextKnownComponentTag(output, registry, 0);
|
|
168
168
|
if (!tag) {
|
|
169
169
|
return output;
|
|
170
170
|
}
|
|
171
171
|
let children = '';
|
|
172
172
|
let replaceEnd = tag.end;
|
|
173
173
|
if (!tag.selfClosing) {
|
|
174
|
-
const close =
|
|
174
|
+
const close = findMatchingComponentClose(output, tag.name, tag.end);
|
|
175
175
|
if (!close) {
|
|
176
176
|
throw new Error(`Unclosed component tag <${tag.name}> in ${sourceFile} at offset ${tag.start}`);
|
|
177
177
|
}
|
|
@@ -183,168 +183,6 @@ function expandSource(source, registry, sourceFile, chain, usedComponents) {
|
|
|
183
183
|
}
|
|
184
184
|
throw new Error(`Component expansion exceeded ${MAX_ITERATIONS} replacements in ${sourceFile}.`);
|
|
185
185
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Find the next component opening tag that exists in the registry.
|
|
188
|
-
*
|
|
189
|
-
* @param {string} source
|
|
190
|
-
* @param {Map<string, string>} registry
|
|
191
|
-
* @param {number} startIndex
|
|
192
|
-
* @returns {{ name: string, start: number, end: number, selfClosing: boolean } | null}
|
|
193
|
-
*/
|
|
194
|
-
function findNextKnownTag(source, registry, startIndex) {
|
|
195
|
-
OPEN_COMPONENT_TAG_RE.lastIndex = startIndex;
|
|
196
|
-
let match;
|
|
197
|
-
while ((match = OPEN_COMPONENT_TAG_RE.exec(source)) !== null) {
|
|
198
|
-
const name = match[1];
|
|
199
|
-
if (!registry.has(name)) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (isInsideExpressionScope(source, match.index)) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
name,
|
|
207
|
-
start: match.index,
|
|
208
|
-
end: OPEN_COMPONENT_TAG_RE.lastIndex,
|
|
209
|
-
selfClosing: match[3] === '/',
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Detect whether `index` is inside a `{ ... }` expression scope.
|
|
216
|
-
*
|
|
217
|
-
* This prevents component macro expansion inside embedded markup expressions,
|
|
218
|
-
* which must remain expression-local so the compiler can lower them safely.
|
|
219
|
-
*
|
|
220
|
-
* @param {string} source
|
|
221
|
-
* @param {number} index
|
|
222
|
-
* @returns {boolean}
|
|
223
|
-
*/
|
|
224
|
-
function isInsideExpressionScope(source, index) {
|
|
225
|
-
let depth = 0;
|
|
226
|
-
let mode = 'code';
|
|
227
|
-
let escaped = false;
|
|
228
|
-
const lower = source.toLowerCase();
|
|
229
|
-
for (let i = 0; i < index; i++) {
|
|
230
|
-
if (mode === 'code') {
|
|
231
|
-
if (lower.startsWith('<script', i)) {
|
|
232
|
-
const close = lower.indexOf('</script>', i + 7);
|
|
233
|
-
if (close < 0 || close >= index) {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
i = close + '</script>'.length - 1;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (lower.startsWith('<style', i)) {
|
|
240
|
-
const close = lower.indexOf('</style>', i + 6);
|
|
241
|
-
if (close < 0 || close >= index) {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
i = close + '</style>'.length - 1;
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const ch = source[i];
|
|
249
|
-
const next = i + 1 < index ? source[i + 1] : '';
|
|
250
|
-
if (mode === 'line-comment') {
|
|
251
|
-
if (ch === '\n') {
|
|
252
|
-
mode = 'code';
|
|
253
|
-
}
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (mode === 'block-comment') {
|
|
257
|
-
if (ch === '*' && next === '/') {
|
|
258
|
-
mode = 'code';
|
|
259
|
-
i += 1;
|
|
260
|
-
}
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
if (mode === 'single-quote' || mode === 'double-quote' || mode === 'template') {
|
|
264
|
-
if (escaped) {
|
|
265
|
-
escaped = false;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (ch === '\\') {
|
|
269
|
-
escaped = true;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if ((mode === 'single-quote' && ch === "'") ||
|
|
273
|
-
(mode === 'double-quote' && ch === '"') ||
|
|
274
|
-
(mode === 'template' && ch === '`')) {
|
|
275
|
-
mode = 'code';
|
|
276
|
-
}
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (ch === '/' && next === '/') {
|
|
280
|
-
mode = 'line-comment';
|
|
281
|
-
i += 1;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (ch === '/' && next === '*') {
|
|
285
|
-
mode = 'block-comment';
|
|
286
|
-
i += 1;
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
if (ch === "'") {
|
|
290
|
-
mode = 'single-quote';
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
if (ch === '"') {
|
|
294
|
-
mode = 'double-quote';
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (ch === '`') {
|
|
298
|
-
mode = 'template';
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (ch === '{') {
|
|
302
|
-
depth += 1;
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (ch === '}') {
|
|
306
|
-
depth = Math.max(0, depth - 1);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return depth > 0;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Find the matching </Name> for an opening tag, accounting for nested
|
|
313
|
-
* tags with the same name.
|
|
314
|
-
*
|
|
315
|
-
* @param {string} source — full source
|
|
316
|
-
* @param {string} tagName — tag name to match
|
|
317
|
-
* @param {number} startAfterOpen — position after the opening tag's `>`
|
|
318
|
-
* @returns {{ contentEnd: number, tagEnd: number } | null}
|
|
319
|
-
*/
|
|
320
|
-
function findMatchingClose(source, tagName, startAfterOpen) {
|
|
321
|
-
let depth = 1;
|
|
322
|
-
const escapedName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
323
|
-
const tagRe = new RegExp(`<(/?)${escapedName}(?:\\s[^<>]*?)?\\s*(/?)>`, 'g');
|
|
324
|
-
tagRe.lastIndex = startAfterOpen;
|
|
325
|
-
let match;
|
|
326
|
-
while ((match = tagRe.exec(source)) !== null) {
|
|
327
|
-
const isClose = match[1] === '/';
|
|
328
|
-
const isSelfClose = match[2] === '/';
|
|
329
|
-
if (isSelfClose && !isClose) {
|
|
330
|
-
// Self-closing <Name />, doesn't affect depth.
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (isClose) {
|
|
334
|
-
depth--;
|
|
335
|
-
if (depth === 0) {
|
|
336
|
-
return {
|
|
337
|
-
contentEnd: match.index,
|
|
338
|
-
tagEnd: match.index + match[0].length,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
depth++;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
186
|
/**
|
|
349
187
|
* Expand a single component tag into its template HTML.
|
|
350
188
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "Deterministic project orchestrator for Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepublishOnly": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@zenithbuild/compiler": "0.6.
|
|
37
|
+
"@zenithbuild/compiler": "0.6.12",
|
|
38
38
|
"picocolors": "^1.1.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|