@zenithbuild/cli 0.6.3 → 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.
@@ -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/dist/index.js CHANGED
@@ -116,6 +116,15 @@ export async function cli(args, cwd) {
116
116
  const outDir = join(projectRoot, 'dist');
117
117
  const config = await loadConfig(projectRoot);
118
118
 
119
+ if (command === 'build' || command === 'dev') {
120
+ const { maybeWarnAboutZenithVersionMismatch } = await import('./version-check.js');
121
+ await maybeWarnAboutZenithVersionMismatch({
122
+ projectRoot,
123
+ logger,
124
+ command
125
+ });
126
+ }
127
+
119
128
  if (command === 'build') {
120
129
  const { build } = await import('./build.js');
121
130
  logger.build('Building…');
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const CLI_ROOT = resolve(__dirname, '..');
9
+ const localRequire = createRequire(import.meta.url);
10
+
11
+ function safeCreateRequire(projectRoot) {
12
+ if (!projectRoot) {
13
+ return localRequire;
14
+ }
15
+ try {
16
+ return createRequire(resolve(projectRoot, 'package.json'));
17
+ } catch {
18
+ return localRequire;
19
+ }
20
+ }
21
+
22
+ function safeResolve(requireFn, specifier) {
23
+ try {
24
+ return requireFn.resolve(specifier);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function resolveBinary(candidates) {
31
+ for (const candidate of candidates) {
32
+ if (existsSync(candidate)) {
33
+ return candidate;
34
+ }
35
+ }
36
+ return candidates[0] || '';
37
+ }
38
+
39
+ export function resolvePackageRoot(packageName, projectRoot = null) {
40
+ const projectRequire = safeCreateRequire(projectRoot);
41
+ const projectPath = safeResolve(projectRequire, `${packageName}/package.json`);
42
+ if (projectPath) {
43
+ return dirname(projectPath);
44
+ }
45
+
46
+ const localPath = safeResolve(localRequire, `${packageName}/package.json`);
47
+ return localPath ? dirname(localPath) : null;
48
+ }
49
+
50
+ export function readInstalledPackageVersion(packageName, projectRoot = null) {
51
+ const packageRoot = resolvePackageRoot(packageName, projectRoot);
52
+ if (!packageRoot) {
53
+ return null;
54
+ }
55
+ try {
56
+ const pkg = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
57
+ return typeof pkg.version === 'string' ? pkg.version : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export function readCliPackageVersion() {
64
+ try {
65
+ const pkg = JSON.parse(readFileSync(resolve(CLI_ROOT, 'package.json'), 'utf8'));
66
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
67
+ } catch {
68
+ return '0.0.0';
69
+ }
70
+ }
71
+
72
+ export function compilerBinCandidates(projectRoot = null) {
73
+ const candidates = [
74
+ resolve(CLI_ROOT, '../compiler/target/release/zenith-compiler'),
75
+ resolve(CLI_ROOT, '../zenith-compiler/target/release/zenith-compiler')
76
+ ];
77
+ const installedRoot = resolvePackageRoot('@zenithbuild/compiler', projectRoot);
78
+ if (installedRoot) {
79
+ candidates.unshift(resolve(installedRoot, 'target/release/zenith-compiler'));
80
+ }
81
+ return candidates;
82
+ }
83
+
84
+ export function resolveCompilerBin(projectRoot = null) {
85
+ return resolveBinary(compilerBinCandidates(projectRoot));
86
+ }
87
+
88
+ export function bundlerBinCandidates(projectRoot = null, env = process.env) {
89
+ const candidates = [];
90
+ const envBin = env?.ZENITH_BUNDLER_BIN;
91
+ if (typeof envBin === 'string' && envBin.length > 0) {
92
+ candidates.push(envBin);
93
+ }
94
+
95
+ const installedRoot = resolvePackageRoot('@zenithbuild/bundler', projectRoot);
96
+ if (installedRoot) {
97
+ candidates.push(resolve(installedRoot, 'target/release/zenith-bundler'));
98
+ }
99
+
100
+ candidates.push(
101
+ resolve(CLI_ROOT, '../bundler/target/release/zenith-bundler'),
102
+ resolve(CLI_ROOT, '../zenith-bundler/target/release/zenith-bundler')
103
+ );
104
+
105
+ return candidates;
106
+ }
107
+
108
+ export function resolveBundlerBin(projectRoot = null, env = process.env) {
109
+ return resolveBinary(bundlerBinCandidates(projectRoot, env));
110
+ }