@zenithbuild/cli 0.4.11 → 0.5.0-beta.2.15
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/README.md +8 -0
- package/dist/build.js +1521 -0
- package/dist/dev-server.js +278 -0
- package/dist/index.js +175 -0
- package/dist/manifest.js +273 -0
- package/dist/preview.js +866 -0
- package/dist/resolve-components.js +490 -0
- package/dist/server/resolve-request-route.js +169 -0
- package/dist/server-contract.js +278 -0
- package/dist/types/generate-env-dts.js +52 -0
- package/dist/types/generate-routes-dts.js +22 -0
- package/dist/types/index.js +34 -0
- package/dist/ui/env.js +41 -0
- package/dist/ui/format.js +172 -0
- package/dist/ui/logger.js +105 -0
- package/package.json +21 -49
- package/bin/zen-build.ts +0 -2
- package/bin/zen-dev.ts +0 -2
- package/bin/zen-preview.ts +0 -2
- package/bin/zenith.ts +0 -2
- package/dist/zen-build.js +0 -9622
- package/dist/zen-dev.js +0 -9622
- package/dist/zen-preview.js +0 -9622
- package/dist/zenith.js +0 -9622
- package/src/commands/add.ts +0 -37
- package/src/commands/build.ts +0 -36
- package/src/commands/create.ts +0 -702
- package/src/commands/dev.ts +0 -472
- package/src/commands/index.ts +0 -112
- package/src/commands/preview.ts +0 -62
- package/src/commands/remove.ts +0 -33
- package/src/index.ts +0 -10
- package/src/main.ts +0 -101
- package/src/utils/branding.ts +0 -178
- package/src/utils/logger.ts +0 -52
- package/src/utils/plugin-manager.ts +0 -114
- package/src/utils/project.ts +0 -77
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// manifest.js — Zenith CLI V0
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// File-based manifest engine.
|
|
5
|
+
//
|
|
6
|
+
// Scans a /pages directory and produces a deterministic RouteManifest.
|
|
7
|
+
//
|
|
8
|
+
// Rules:
|
|
9
|
+
// - index.zen → parent directory path
|
|
10
|
+
// - [param].zen → :param dynamic segment
|
|
11
|
+
// - [...slug].zen → *slug catch-all segment (must be terminal, 1+ segments;
|
|
12
|
+
// root '/*slug' may match '/' in router matcher)
|
|
13
|
+
// - [[...slug]].zen → *slug? optional catch-all segment (must be terminal, 0+ segments)
|
|
14
|
+
// - Deterministic precedence: static > :param > *catchall
|
|
15
|
+
// - Tie-breaker: lexicographic route path
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
19
|
+
import { join, relative, sep, basename, extname, dirname } from 'node:path';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{ path: string, file: string }} ManifestEntry
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Scan a pages directory and produce a deterministic RouteManifest.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} pagesDir - Absolute path to /pages directory
|
|
29
|
+
* @param {string} [extension='.zen'] - File extension to scan for
|
|
30
|
+
* @returns {Promise<ManifestEntry[]>}
|
|
31
|
+
*/
|
|
32
|
+
export async function generateManifest(pagesDir, extension = '.zen') {
|
|
33
|
+
const entries = await _scanDir(pagesDir, pagesDir, extension);
|
|
34
|
+
|
|
35
|
+
// Validate: no repeated param names in any single route
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
_validateParams(entry.path);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Sort: static first, dynamic after, alpha within each category
|
|
41
|
+
return _sortEntries(entries);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Recursively scan a directory for page files.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} dir - Current directory
|
|
48
|
+
* @param {string} root - Root pages directory
|
|
49
|
+
* @param {string} ext - Extension to match
|
|
50
|
+
* @returns {Promise<ManifestEntry[]>}
|
|
51
|
+
*/
|
|
52
|
+
async function _scanDir(dir, root, ext) {
|
|
53
|
+
/** @type {ManifestEntry[]} */
|
|
54
|
+
const entries = [];
|
|
55
|
+
|
|
56
|
+
let items;
|
|
57
|
+
try {
|
|
58
|
+
items = await readdir(dir);
|
|
59
|
+
} catch {
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Sort items for deterministic traversal
|
|
64
|
+
items.sort();
|
|
65
|
+
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
const fullPath = join(dir, item);
|
|
68
|
+
const info = await stat(fullPath);
|
|
69
|
+
|
|
70
|
+
if (info.isDirectory()) {
|
|
71
|
+
const nested = await _scanDir(fullPath, root, ext);
|
|
72
|
+
entries.push(...nested);
|
|
73
|
+
} else if (item.endsWith(ext)) {
|
|
74
|
+
const routePath = _fileToRoute(fullPath, root, ext);
|
|
75
|
+
entries.push({ path: routePath, file: relative(root, fullPath) });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert a file path to a route path.
|
|
84
|
+
*
|
|
85
|
+
* pages/index.zen → /
|
|
86
|
+
* pages/about.zen → /about
|
|
87
|
+
* pages/users/[id].zen → /users/:id
|
|
88
|
+
* pages/docs/[...slug].zen → /docs/*slug
|
|
89
|
+
* pages/[[...slug]].zen → /*slug?
|
|
90
|
+
* pages/docs/api/index.zen → /docs/api
|
|
91
|
+
*
|
|
92
|
+
* @param {string} filePath - Absolute file path
|
|
93
|
+
* @param {string} root - Root pages directory
|
|
94
|
+
* @param {string} ext - Extension
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function _fileToRoute(filePath, root, ext) {
|
|
98
|
+
const rel = relative(root, filePath);
|
|
99
|
+
const withoutExt = rel.slice(0, -ext.length);
|
|
100
|
+
|
|
101
|
+
// Normalize path separators
|
|
102
|
+
const segments = withoutExt.split(sep).filter(Boolean);
|
|
103
|
+
|
|
104
|
+
// Convert segments
|
|
105
|
+
const routeSegments = segments.map((seg) => {
|
|
106
|
+
// [[...param]] → *param? (optional catch-all)
|
|
107
|
+
const optionalCatchAllMatch = seg.match(/^\[\[\.\.\.([a-zA-Z_][a-zA-Z0-9_]*)\]\]$/);
|
|
108
|
+
if (optionalCatchAllMatch) {
|
|
109
|
+
return '*' + optionalCatchAllMatch[1] + '?';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// [...param] → *param (required catch-all)
|
|
113
|
+
const catchAllMatch = seg.match(/^\[\.\.\.([a-zA-Z_][a-zA-Z0-9_]*)\]$/);
|
|
114
|
+
if (catchAllMatch) {
|
|
115
|
+
return '*' + catchAllMatch[1];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// [param] → :param
|
|
119
|
+
const paramMatch = seg.match(/^\[([a-zA-Z_][a-zA-Z0-9_]*)\]$/);
|
|
120
|
+
if (paramMatch) {
|
|
121
|
+
return ':' + paramMatch[1];
|
|
122
|
+
}
|
|
123
|
+
return seg;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Remove trailing 'index'
|
|
127
|
+
if (routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === 'index') {
|
|
128
|
+
routeSegments.pop();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const route = '/' + routeSegments.join('/');
|
|
132
|
+
return route;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate that a route path has no repeated param names.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} routePath
|
|
139
|
+
* @throws {Error} If repeated params found
|
|
140
|
+
*/
|
|
141
|
+
function _validateParams(routePath) {
|
|
142
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
143
|
+
const paramNames = new Set();
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < segments.length; i++) {
|
|
146
|
+
const seg = segments[i];
|
|
147
|
+
if (seg.startsWith(':') || seg.startsWith('*')) {
|
|
148
|
+
const rawName = seg.slice(1);
|
|
149
|
+
const isCatchAll = seg.startsWith('*');
|
|
150
|
+
const optionalCatchAll = isCatchAll && rawName.endsWith('?');
|
|
151
|
+
const name = optionalCatchAll ? rawName.slice(0, -1) : rawName;
|
|
152
|
+
const label = isCatchAll ? `*${rawName}` : `:${name}`;
|
|
153
|
+
if (paramNames.has(name)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`[Zenith CLI] Repeated param name '${label}' in route '${routePath}'`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (isCatchAll && i !== segments.length - 1) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`[Zenith CLI] Catch-all segment '${label}' must be the last segment in route '${routePath}'`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
paramNames.add(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a route contains any dynamic segments.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} routePath
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
function _isDynamic(routePath) {
|
|
175
|
+
return routePath.split('/').some((seg) => seg.startsWith(':') || seg.startsWith('*'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Sort manifest entries by deterministic route precedence.
|
|
180
|
+
*
|
|
181
|
+
* @param {ManifestEntry[]} entries
|
|
182
|
+
* @returns {ManifestEntry[]}
|
|
183
|
+
*/
|
|
184
|
+
function _sortEntries(entries) {
|
|
185
|
+
return [...entries].sort((a, b) => compareRouteSpecificity(a.path, b.path));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Deterministic route precedence:
|
|
190
|
+
* static segment > param segment > catch-all segment.
|
|
191
|
+
* Tie-breakers: segment count (more specific first), then lexicographic path.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} a
|
|
194
|
+
* @param {string} b
|
|
195
|
+
* @returns {number}
|
|
196
|
+
*/
|
|
197
|
+
function compareRouteSpecificity(a, b) {
|
|
198
|
+
if (a === '/' && b !== '/') return -1;
|
|
199
|
+
if (b === '/' && a !== '/') return 1;
|
|
200
|
+
|
|
201
|
+
const aSegs = a.split('/').filter(Boolean);
|
|
202
|
+
const bSegs = b.split('/').filter(Boolean);
|
|
203
|
+
const aClass = routeClass(aSegs);
|
|
204
|
+
const bClass = routeClass(bSegs);
|
|
205
|
+
if (aClass !== bClass) {
|
|
206
|
+
return bClass - aClass;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const max = Math.min(aSegs.length, bSegs.length);
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < max; i++) {
|
|
212
|
+
const aWeight = segmentWeight(aSegs[i]);
|
|
213
|
+
const bWeight = segmentWeight(bSegs[i]);
|
|
214
|
+
if (aWeight !== bWeight) {
|
|
215
|
+
return bWeight - aWeight;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (aSegs.length !== bSegs.length) {
|
|
220
|
+
return bSegs.length - aSegs.length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return a.localeCompare(b);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {string[]} segments
|
|
228
|
+
* @returns {number}
|
|
229
|
+
*/
|
|
230
|
+
function routeClass(segments) {
|
|
231
|
+
let hasParam = false;
|
|
232
|
+
let hasCatchAll = false;
|
|
233
|
+
for (const segment of segments) {
|
|
234
|
+
if (segment.startsWith('*')) {
|
|
235
|
+
hasCatchAll = true;
|
|
236
|
+
} else if (segment.startsWith(':')) {
|
|
237
|
+
hasParam = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!hasParam && !hasCatchAll) return 3;
|
|
241
|
+
if (hasCatchAll) return 1;
|
|
242
|
+
return 2;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {string | undefined} segment
|
|
247
|
+
* @returns {number}
|
|
248
|
+
*/
|
|
249
|
+
function segmentWeight(segment) {
|
|
250
|
+
if (!segment) return 0;
|
|
251
|
+
if (segment.startsWith('*')) return 1;
|
|
252
|
+
if (segment.startsWith(':')) return 2;
|
|
253
|
+
return 3;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Generate a JavaScript module string from manifest entries.
|
|
258
|
+
* Used for writing the manifest file to disk.
|
|
259
|
+
*
|
|
260
|
+
* @param {ManifestEntry[]} entries
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
export function serializeManifest(entries) {
|
|
264
|
+
const lines = entries.map((e) => {
|
|
265
|
+
const hasParams = _isDynamic(e.path);
|
|
266
|
+
const loader = hasParams
|
|
267
|
+
? `(params) => import('./pages/${e.file}')`
|
|
268
|
+
: `() => import('./pages/${e.file}')`;
|
|
269
|
+
return ` { path: '${e.path}', load: ${loader} }`;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return `export default [\n${lines.join(',\n')}\n];\n`;
|
|
273
|
+
}
|