@zenithbuild/cli 0.6.6 → 0.6.9

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.
Files changed (43) hide show
  1. package/dist/build.d.ts +32 -0
  2. package/dist/build.js +193 -548
  3. package/dist/compiler-bridge-runner.d.ts +5 -0
  4. package/dist/compiler-bridge-runner.js +70 -0
  5. package/dist/component-instance-ir.d.ts +6 -0
  6. package/dist/component-instance-ir.js +0 -20
  7. package/dist/component-occurrences.d.ts +6 -0
  8. package/dist/component-occurrences.js +6 -28
  9. package/dist/dev-server.d.ts +18 -0
  10. package/dist/dev-server.js +65 -114
  11. package/dist/dev-watch.d.ts +1 -0
  12. package/dist/dev-watch.js +2 -2
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.js +6 -28
  15. package/dist/manifest.d.ts +23 -0
  16. package/dist/manifest.js +22 -48
  17. package/dist/preview.d.ts +100 -0
  18. package/dist/preview.js +418 -488
  19. package/dist/resolve-components.d.ts +39 -0
  20. package/dist/resolve-components.js +30 -104
  21. package/dist/server/resolve-request-route.d.ts +39 -0
  22. package/dist/server/resolve-request-route.js +104 -113
  23. package/dist/server-contract.d.ts +39 -0
  24. package/dist/server-contract.js +15 -67
  25. package/dist/toolchain-paths.d.ts +23 -0
  26. package/dist/toolchain-paths.js +139 -39
  27. package/dist/toolchain-runner.d.ts +33 -0
  28. package/dist/toolchain-runner.js +194 -0
  29. package/dist/types/generate-env-dts.d.ts +5 -0
  30. package/dist/types/generate-env-dts.js +4 -2
  31. package/dist/types/generate-routes-dts.d.ts +8 -0
  32. package/dist/types/generate-routes-dts.js +7 -5
  33. package/dist/types/index.d.ts +14 -0
  34. package/dist/types/index.js +16 -7
  35. package/dist/ui/env.d.ts +18 -0
  36. package/dist/ui/env.js +0 -12
  37. package/dist/ui/format.d.ts +33 -0
  38. package/dist/ui/format.js +8 -46
  39. package/dist/ui/logger.d.ts +59 -0
  40. package/dist/ui/logger.js +3 -32
  41. package/dist/version-check.d.ts +54 -0
  42. package/dist/version-check.js +41 -98
  43. package/package.json +6 -4
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Walk `srcDir/components/` recursively. Return Map<PascalName, absPath>.
3
+ * Errors on duplicate component names within the registry.
4
+ *
5
+ * Also scans `srcDir/layouts/` for layout components (Document Mode).
6
+ *
7
+ * @param {string} srcDir — absolute path to the project's `src/` directory
8
+ * @returns {Map<string, string>}
9
+ */
10
+ export function buildComponentRegistry(srcDir: string): Map<string, string>;
11
+ /**
12
+ * Strip all <script ...>...</script> and <style ...>...</style> blocks
13
+ * from a .zen source. Return template-only markup.
14
+ *
15
+ * @param {string} zenSource
16
+ * @returns {string}
17
+ */
18
+ export function extractTemplate(zenSource: string): string;
19
+ /**
20
+ * Returns true if the template contains <!doctype or <html,
21
+ * indicating it's a Document Mode component (layout wrapper).
22
+ *
23
+ * @param {string} template
24
+ * @returns {boolean}
25
+ */
26
+ export function isDocumentMode(template: string): boolean;
27
+ /**
28
+ * Recursively expand PascalCase component tags in `source`.
29
+ *
30
+ * @param {string} source — page or component template source
31
+ * @param {Map<string, string>} registry — component name → .zen file path
32
+ * @param {string} sourceFile — source file path (for error messages)
33
+ * @param {Set<string>} [visited] — cycle detection set
34
+ * @returns {{ expandedSource: string, usedComponents: string[] }}
35
+ */
36
+ export function expandComponents(source: string, registry: Map<string, string>, sourceFile: string, visited?: Set<string>): {
37
+ expandedSource: string;
38
+ usedComponents: string[];
39
+ };
@@ -8,14 +8,11 @@
8
8
  // Pipeline:
9
9
  // buildComponentRegistry() → expandComponents() → expanded source string
10
10
  // ---------------------------------------------------------------------------
11
-
12
11
  import { readdirSync, readFileSync, statSync } from 'node:fs';
13
12
  import { basename, extname, join } from 'node:path';
14
-
15
13
  // ---------------------------------------------------------------------------
16
14
  // Registry: Map<PascalCaseName, absolutePath>
17
15
  // ---------------------------------------------------------------------------
18
-
19
16
  /**
20
17
  * Walk `srcDir/components/` recursively. Return Map<PascalName, absPath>.
21
18
  * Errors on duplicate component names within the registry.
@@ -28,21 +25,19 @@ import { basename, extname, join } from 'node:path';
28
25
  export function buildComponentRegistry(srcDir) {
29
26
  /** @type {Map<string, string>} */
30
27
  const registry = new Map();
31
-
32
28
  const scanDirs = ['components', 'layouts', 'globals'];
33
29
  for (const sub of scanDirs) {
34
30
  const dir = join(srcDir, sub);
35
31
  try {
36
32
  statSync(dir);
37
- } catch {
33
+ }
34
+ catch {
38
35
  continue; // Directory doesn't exist, skip
39
36
  }
40
37
  walkDir(dir, registry);
41
38
  }
42
-
43
39
  return registry;
44
40
  }
45
-
46
41
  /**
47
42
  * @param {string} dir
48
43
  * @param {Map<string, string>} registry
@@ -51,11 +46,11 @@ function walkDir(dir, registry) {
51
46
  let entries;
52
47
  try {
53
48
  entries = readdirSync(dir);
54
- } catch {
49
+ }
50
+ catch {
55
51
  return;
56
52
  }
57
53
  entries.sort();
58
-
59
54
  for (const name of entries) {
60
55
  const fullPath = join(dir, name);
61
56
  const info = statSync(fullPath);
@@ -63,28 +58,24 @@ function walkDir(dir, registry) {
63
58
  walkDir(fullPath, registry);
64
59
  continue;
65
60
  }
66
- if (extname(name) !== '.zen') continue;
67
-
61
+ if (extname(name) !== '.zen')
62
+ continue;
68
63
  const componentName = basename(name, '.zen');
69
64
  // Only register PascalCase names (first char uppercase)
70
- if (!/^[A-Z]/.test(componentName)) continue;
71
-
65
+ if (!/^[A-Z]/.test(componentName))
66
+ continue;
72
67
  if (registry.has(componentName)) {
73
- throw new Error(
74
- `Duplicate component name "${componentName}":\n` +
68
+ throw new Error(`Duplicate component name "${componentName}":\n` +
75
69
  ` 1) ${registry.get(componentName)}\n` +
76
70
  ` 2) ${fullPath}\n` +
77
- `Rename one to resolve the conflict.`
78
- );
71
+ `Rename one to resolve the conflict.`);
79
72
  }
80
73
  registry.set(componentName, fullPath);
81
74
  }
82
75
  }
83
-
84
76
  // ---------------------------------------------------------------------------
85
77
  // Template extraction
86
78
  // ---------------------------------------------------------------------------
87
-
88
79
  /**
89
80
  * Strip all <script ...>...</script> and <style ...>...</style> blocks
90
81
  * from a .zen source. Return template-only markup.
@@ -95,15 +86,12 @@ function walkDir(dir, registry) {
95
86
  export function extractTemplate(zenSource) {
96
87
  // Remove <script ...>...</script> blocks (greedy matching for nested content)
97
88
  let template = zenSource;
98
-
99
89
  // Strip script blocks (handles <script>, <script lang="ts">, etc.)
100
90
  template = stripBlock(template, 'script');
101
91
  // Strip style blocks
102
92
  template = stripBlock(template, 'style');
103
-
104
93
  return template.trim();
105
94
  }
106
-
107
95
  /**
108
96
  * Strip a matched pair of <tag ...>...</tag> from source.
109
97
  * Handles multiple occurrences and attributes on the opening tag.
@@ -116,19 +104,15 @@ function stripBlock(source, tag) {
116
104
  // Use a regex that matches <tag ...>...</tag> including multiline content
117
105
  // We need a non-greedy approach for nested scenarios, but script/style
118
106
  // blocks cannot be nested in HTML, so we can match the first closing tag.
119
- const re = new RegExp(
120
- `<${tag}(?:\\s[^>]*)?>` + // opening tag with optional attributes
121
- `[\\s\\S]*?` + // content (non-greedy)
122
- `</${tag}>`, // closing tag
123
- 'gi'
124
- );
107
+ const re = new RegExp(`<${tag}(?:\\s[^>]*)?>` + // opening tag with optional attributes
108
+ `[\\s\\S]*?` + // content (non-greedy)
109
+ `</${tag}>`, // closing tag
110
+ 'gi');
125
111
  return source.replace(re, '');
126
112
  }
127
-
128
113
  // ---------------------------------------------------------------------------
129
114
  // Document Mode detection
130
115
  // ---------------------------------------------------------------------------
131
-
132
116
  /**
133
117
  * Returns true if the template contains <!doctype or <html,
134
118
  * indicating it's a Document Mode component (layout wrapper).
@@ -140,13 +124,10 @@ export function isDocumentMode(template) {
140
124
  const lower = template.toLowerCase();
141
125
  return lower.includes('<!doctype') || lower.includes('<html');
142
126
  }
143
-
144
127
  // ---------------------------------------------------------------------------
145
128
  // Component expansion
146
129
  // ---------------------------------------------------------------------------
147
-
148
130
  const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
149
-
150
131
  /**
151
132
  * Recursively expand PascalCase component tags in `source`.
152
133
  *
@@ -160,7 +141,6 @@ export function expandComponents(source, registry, sourceFile, visited) {
160
141
  if (visited && visited.size > 0) {
161
142
  throw new Error('expandComponents() does not accept a pre-populated visited set');
162
143
  }
163
-
164
144
  const usedComponents = [];
165
145
  const expandedSource = expandSource(source, registry, sourceFile, [], usedComponents);
166
146
  return {
@@ -168,7 +148,6 @@ export function expandComponents(source, registry, sourceFile, visited) {
168
148
  usedComponents: [...new Set(usedComponents)],
169
149
  };
170
150
  }
171
-
172
151
  /**
173
152
  * Expand component tags recursively.
174
153
  *
@@ -183,51 +162,27 @@ function expandSource(source, registry, sourceFile, chain, usedComponents) {
183
162
  let output = source;
184
163
  let iterations = 0;
185
164
  const MAX_ITERATIONS = 10_000;
186
-
187
165
  while (iterations < MAX_ITERATIONS) {
188
166
  iterations += 1;
189
167
  const tag = findNextKnownTag(output, registry, 0);
190
168
  if (!tag) {
191
169
  return output;
192
170
  }
193
-
194
171
  let children = '';
195
172
  let replaceEnd = tag.end;
196
-
197
173
  if (!tag.selfClosing) {
198
174
  const close = findMatchingClose(output, tag.name, tag.end);
199
175
  if (!close) {
200
- throw new Error(
201
- `Unclosed component tag <${tag.name}> in ${sourceFile} at offset ${tag.start}`
202
- );
176
+ throw new Error(`Unclosed component tag <${tag.name}> in ${sourceFile} at offset ${tag.start}`);
203
177
  }
204
- children = expandSource(
205
- output.slice(tag.end, close.contentEnd),
206
- registry,
207
- sourceFile,
208
- chain,
209
- usedComponents
210
- );
178
+ children = expandSource(output.slice(tag.end, close.contentEnd), registry, sourceFile, chain, usedComponents);
211
179
  replaceEnd = close.tagEnd;
212
180
  }
213
-
214
- const replacement = expandTag(
215
- tag.name,
216
- children,
217
- registry,
218
- sourceFile,
219
- chain,
220
- usedComponents
221
- );
222
-
181
+ const replacement = expandTag(tag.name, children, registry, sourceFile, chain, usedComponents);
223
182
  output = output.slice(0, tag.start) + replacement + output.slice(replaceEnd);
224
183
  }
225
-
226
- throw new Error(
227
- `Component expansion exceeded ${MAX_ITERATIONS} replacements in ${sourceFile}.`
228
- );
184
+ throw new Error(`Component expansion exceeded ${MAX_ITERATIONS} replacements in ${sourceFile}.`);
229
185
  }
230
-
231
186
  /**
232
187
  * Find the next component opening tag that exists in the registry.
233
188
  *
@@ -238,7 +193,6 @@ function expandSource(source, registry, sourceFile, chain, usedComponents) {
238
193
  */
239
194
  function findNextKnownTag(source, registry, startIndex) {
240
195
  OPEN_COMPONENT_TAG_RE.lastIndex = startIndex;
241
-
242
196
  let match;
243
197
  while ((match = OPEN_COMPONENT_TAG_RE.exec(source)) !== null) {
244
198
  const name = match[1];
@@ -255,10 +209,8 @@ function findNextKnownTag(source, registry, startIndex) {
255
209
  selfClosing: match[3] === '/',
256
210
  };
257
211
  }
258
-
259
212
  return null;
260
213
  }
261
-
262
214
  /**
263
215
  * Detect whether `index` is inside a `{ ... }` expression scope.
264
216
  *
@@ -274,7 +226,6 @@ function isInsideExpressionScope(source, index) {
274
226
  let mode = 'code';
275
227
  let escaped = false;
276
228
  const lower = source.toLowerCase();
277
-
278
229
  for (let i = 0; i < index; i++) {
279
230
  if (mode === 'code') {
280
231
  if (lower.startsWith('<script', i)) {
@@ -294,10 +245,8 @@ function isInsideExpressionScope(source, index) {
294
245
  continue;
295
246
  }
296
247
  }
297
-
298
248
  const ch = source[i];
299
249
  const next = i + 1 < index ? source[i + 1] : '';
300
-
301
250
  if (mode === 'line-comment') {
302
251
  if (ch === '\n') {
303
252
  mode = 'code';
@@ -320,16 +269,13 @@ function isInsideExpressionScope(source, index) {
320
269
  escaped = true;
321
270
  continue;
322
271
  }
323
- if (
324
- (mode === 'single-quote' && ch === "'") ||
272
+ if ((mode === 'single-quote' && ch === "'") ||
325
273
  (mode === 'double-quote' && ch === '"') ||
326
- (mode === 'template' && ch === '`')
327
- ) {
274
+ (mode === 'template' && ch === '`')) {
328
275
  mode = 'code';
329
276
  }
330
277
  continue;
331
278
  }
332
-
333
279
  if (ch === '/' && next === '/') {
334
280
  mode = 'line-comment';
335
281
  i += 1;
@@ -360,10 +306,8 @@ function isInsideExpressionScope(source, index) {
360
306
  depth = Math.max(0, depth - 1);
361
307
  }
362
308
  }
363
-
364
309
  return depth > 0;
365
310
  }
366
-
367
311
  /**
368
312
  * Find the matching </Name> for an opening tag, accounting for nested
369
313
  * tags with the same name.
@@ -378,17 +322,14 @@ function findMatchingClose(source, tagName, startAfterOpen) {
378
322
  const escapedName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
379
323
  const tagRe = new RegExp(`<(/?)${escapedName}(?:\\s[^<>]*?)?\\s*(/?)>`, 'g');
380
324
  tagRe.lastIndex = startAfterOpen;
381
-
382
325
  let match;
383
326
  while ((match = tagRe.exec(source)) !== null) {
384
327
  const isClose = match[1] === '/';
385
328
  const isSelfClose = match[2] === '/';
386
-
387
329
  if (isSelfClose && !isClose) {
388
330
  // Self-closing <Name />, doesn't affect depth.
389
331
  continue;
390
332
  }
391
-
392
333
  if (isClose) {
393
334
  depth--;
394
335
  if (depth === 0) {
@@ -397,14 +338,13 @@ function findMatchingClose(source, tagName, startAfterOpen) {
397
338
  tagEnd: match.index + match[0].length,
398
339
  };
399
340
  }
400
- } else {
341
+ }
342
+ else {
401
343
  depth++;
402
344
  }
403
345
  }
404
-
405
346
  return null;
406
347
  }
407
-
408
348
  /**
409
349
  * Expand a single component tag into its template HTML.
410
350
  *
@@ -417,57 +357,44 @@ function findMatchingClose(source, tagName, startAfterOpen) {
417
357
  * @returns {string}
418
358
  */
419
359
  function expandTag(name, children, registry, sourceFile, chain, usedComponents) {
420
-
421
360
  const compPath = registry.get(name);
422
361
  if (!compPath) {
423
362
  throw new Error(`Unknown component "${name}" referenced in ${sourceFile}`);
424
363
  }
425
-
426
364
  // Cycle detection
427
365
  if (chain.includes(name)) {
428
366
  const cycle = [...chain, name].join(' -> ');
429
- throw new Error(
430
- `Circular component dependency detected: ${cycle}\n` +
431
- `File: ${sourceFile}`
432
- );
367
+ throw new Error(`Circular component dependency detected: ${cycle}\n` +
368
+ `File: ${sourceFile}`);
433
369
  }
434
-
435
370
  const compSource = readFileSync(compPath, 'utf8');
436
371
  let template = extractTemplate(compSource);
437
-
438
372
  // Check Document Mode
439
373
  const docMode = isDocumentMode(template);
440
-
441
374
  if (docMode) {
442
375
  // Document Mode: must contain exactly one <slot />
443
376
  const slotCount = countSlots(template);
444
377
  if (slotCount !== 1) {
445
- throw new Error(
446
- `Document Mode component "${name}" must contain exactly one <slot />, found ${slotCount}.\n` +
447
- `File: ${compPath}`
448
- );
378
+ throw new Error(`Document Mode component "${name}" must contain exactly one <slot />, found ${slotCount}.\n` +
379
+ `File: ${compPath}`);
449
380
  }
450
381
  // Replace <slot /> with children
451
382
  template = replaceSlot(template, children);
452
- } else {
383
+ }
384
+ else {
453
385
  // Standard component
454
386
  const slotCount = countSlots(template);
455
387
  if (children.trim().length > 0 && slotCount === 0) {
456
- throw new Error(
457
- `Component "${name}" has children but its template has no <slot />.\n` +
458
- `Either add <slot /> to ${compPath} or make the tag self-closing.`
459
- );
388
+ throw new Error(`Component "${name}" has children but its template has no <slot />.\n` +
389
+ `Either add <slot /> to ${compPath} or make the tag self-closing.`);
460
390
  }
461
391
  if (slotCount > 0) {
462
392
  template = replaceSlot(template, children || '');
463
393
  }
464
394
  }
465
-
466
395
  usedComponents.push(name);
467
-
468
396
  return expandSource(template, registry, compPath, [...chain, name], usedComponents);
469
397
  }
470
-
471
398
  /**
472
399
  * Count occurrences of <slot /> or <slot></slot> in template.
473
400
  * @param {string} template
@@ -477,7 +404,6 @@ function countSlots(template) {
477
404
  const matches = template.match(/<slot\s*>\s*<\/slot>|<slot\s*\/>|<slot\s*>/gi);
478
405
  return matches ? matches.length : 0;
479
406
  }
480
-
481
407
  /**
482
408
  * Replace <slot />, <slot/>, or <slot></slot> with replacement content.
483
409
  * @param {string} template
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Deterministic route precedence:
3
+ * static segment > param segment > catch-all segment.
4
+ * Tie-breakers: segment count (more specific first), then lexicographic path.
5
+ *
6
+ * @param {string} a
7
+ * @param {string} b
8
+ * @returns {number}
9
+ */
10
+ export function compareRouteSpecificity(a: string, b: string): number;
11
+ /**
12
+ * @param {string} pathname
13
+ * @param {Array<{ path: string }>} routes
14
+ * @returns {{ entry: { path: string }, params: Record<string, string> } | null}
15
+ */
16
+ export function matchRoute(pathname: string, routes: Array<{
17
+ path: string;
18
+ }>): {
19
+ entry: {
20
+ path: string;
21
+ };
22
+ params: Record<string, string>;
23
+ } | null;
24
+ /**
25
+ * Resolve an incoming request URL against a manifest route list.
26
+ *
27
+ * @param {string | URL} reqUrl
28
+ * @param {Array<{ path: string }>} manifest
29
+ * @returns {{ matched: boolean, route: { path: string } | null, params: Record<string, string> }}
30
+ */
31
+ export function resolveRequestRoute(reqUrl: string | URL, manifest: Array<{
32
+ path: string;
33
+ }>): {
34
+ matched: boolean;
35
+ route: {
36
+ path: string;
37
+ } | null;
38
+ params: Record<string, string>;
39
+ };