@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 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
- OPEN_COMPONENT_TAG_RE.lastIndex = 0;
1071
- let match;
1072
- while ((match = OPEN_COMPONENT_TAG_RE.exec(source)) !== null) {
1073
- const name = match[1];
1074
- if (!registry.has(name)) {
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 attrs = String(match[2] || '').trim();
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 ? rewritePropsExpression(trimmed, rewriteContext) : 'undefined';
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 pageScopeRewrite = buildScopedIdentifierRewrite(pageIr);
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 = pageSelfExpressionRewrite;
2137
- let attrScopeRewrite = pageScopeRewrite;
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 = findNextKnownTag(source, registry, cursor);
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 = findMatchingClose(source, tag.name, tag.end);
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 = findNextKnownTag(output, registry, 0);
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 = findMatchingClose(output, tag.name, tag.end);
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.10",
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.10",
37
+ "@zenithbuild/compiler": "0.6.12",
38
38
  "picocolors": "^1.1.1"
39
39
  },
40
40
  "devDependencies": {