@zenithbuild/cli 0.6.17 → 0.7.0
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/compiler-runtime.d.ts +59 -0
- package/dist/build/compiler-runtime.js +277 -0
- package/dist/build/expression-rewrites.d.ts +88 -0
- package/dist/build/expression-rewrites.js +372 -0
- package/dist/build/hoisted-code-transforms.d.ts +44 -0
- package/dist/build/hoisted-code-transforms.js +316 -0
- package/dist/build/merge-component-ir.d.ts +16 -0
- package/dist/build/merge-component-ir.js +257 -0
- package/dist/build/page-component-loop.d.ts +92 -0
- package/dist/build/page-component-loop.js +257 -0
- package/dist/build/page-ir-normalization.d.ts +23 -0
- package/dist/build/page-ir-normalization.js +370 -0
- package/dist/build/page-loop-metrics.d.ts +100 -0
- package/dist/build/page-loop-metrics.js +131 -0
- package/dist/build/page-loop-state.d.ts +261 -0
- package/dist/build/page-loop-state.js +92 -0
- package/dist/build/page-loop.d.ts +33 -0
- package/dist/build/page-loop.js +217 -0
- package/dist/build/scoped-identifier-rewrite.d.ts +112 -0
- package/dist/build/scoped-identifier-rewrite.js +245 -0
- package/dist/build/server-script.d.ts +41 -0
- package/dist/build/server-script.js +210 -0
- package/dist/build/type-declarations.d.ts +16 -0
- package/dist/build/type-declarations.js +158 -0
- package/dist/build/typescript-expression-utils.d.ts +23 -0
- package/dist/build/typescript-expression-utils.js +272 -0
- package/dist/build.d.ts +10 -18
- package/dist/build.js +74 -2261
- package/dist/component-instance-ir.d.ts +2 -2
- package/dist/component-instance-ir.js +146 -39
- package/dist/component-occurrences.js +63 -15
- package/dist/config.d.ts +66 -0
- package/dist/config.js +86 -0
- package/dist/debug-script.d.ts +1 -0
- package/dist/debug-script.js +8 -0
- package/dist/dev-build-session.d.ts +23 -0
- package/dist/dev-build-session.js +421 -0
- package/dist/dev-server.js +256 -54
- package/dist/framework-components/Image.zen +316 -0
- package/dist/images/materialize.d.ts +17 -0
- package/dist/images/materialize.js +200 -0
- package/dist/images/payload.d.ts +18 -0
- package/dist/images/payload.js +65 -0
- package/dist/images/runtime.d.ts +4 -0
- package/dist/images/runtime.js +254 -0
- package/dist/images/service.d.ts +4 -0
- package/dist/images/service.js +302 -0
- package/dist/images/shared.d.ts +58 -0
- package/dist/images/shared.js +306 -0
- package/dist/index.js +2 -17
- package/dist/manifest.js +45 -0
- package/dist/preview.d.ts +4 -1
- package/dist/preview.js +59 -6
- package/dist/resolve-components.js +20 -3
- package/dist/server-contract.js +3 -2
- package/dist/server-script-composition.d.ts +39 -0
- package/dist/server-script-composition.js +133 -0
- package/dist/startup-profile.d.ts +10 -0
- package/dist/startup-profile.js +62 -0
- package/dist/toolchain-paths.d.ts +1 -0
- package/dist/toolchain-paths.js +31 -0
- package/dist/version-check.d.ts +2 -1
- package/dist/version-check.js +12 -5
- package/package.json +5 -4
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const DEFAULT_DEVICE_SIZES = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
2
|
+
const DEFAULT_IMAGE_SIZES = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
3
|
+
const DEFAULT_FORMATS = ['webp', 'avif'];
|
|
4
|
+
const DEFAULT_REMOTE_PATTERNS = [];
|
|
5
|
+
const DEFAULT_MAX_REMOTE_BYTES = 10 * 1024 * 1024;
|
|
6
|
+
const DEFAULT_MAX_PIXELS = 40_000_000;
|
|
7
|
+
const DEFAULT_MINIMUM_CACHE_TTL = 60;
|
|
8
|
+
const DEFAULT_QUALITY = 75;
|
|
9
|
+
const IMAGE_RUNTIME_GLOBAL = '__zenith_image_runtime';
|
|
10
|
+
export const DEFAULT_IMAGE_CONFIG = {
|
|
11
|
+
formats: DEFAULT_FORMATS,
|
|
12
|
+
quality: DEFAULT_QUALITY,
|
|
13
|
+
deviceSizes: DEFAULT_DEVICE_SIZES,
|
|
14
|
+
imageSizes: DEFAULT_IMAGE_SIZES,
|
|
15
|
+
remotePatterns: DEFAULT_REMOTE_PATTERNS,
|
|
16
|
+
allowSvg: false,
|
|
17
|
+
maxRemoteBytes: DEFAULT_MAX_REMOTE_BYTES,
|
|
18
|
+
maxPixels: DEFAULT_MAX_PIXELS,
|
|
19
|
+
minimumCacheTTL: DEFAULT_MINIMUM_CACHE_TTL,
|
|
20
|
+
dangerouslyAllowLocalNetwork: false
|
|
21
|
+
};
|
|
22
|
+
const TOP_LEVEL_KEYS = new Set([
|
|
23
|
+
'formats',
|
|
24
|
+
'quality',
|
|
25
|
+
'deviceSizes',
|
|
26
|
+
'imageSizes',
|
|
27
|
+
'remotePatterns',
|
|
28
|
+
'allowSvg',
|
|
29
|
+
'maxRemoteBytes',
|
|
30
|
+
'maxPixels',
|
|
31
|
+
'minimumCacheTTL',
|
|
32
|
+
'dangerouslyAllowLocalNetwork'
|
|
33
|
+
]);
|
|
34
|
+
function isPlainObject(value) {
|
|
35
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function asPositiveInt(value, key) {
|
|
38
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
39
|
+
throw new Error(`[Zenith:Config] images.${key} must be a positive integer`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function normalizeStringArray(value, key) {
|
|
44
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
45
|
+
throw new Error(`[Zenith:Config] images.${key} must be a non-empty array`);
|
|
46
|
+
}
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const entry of value) {
|
|
49
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
50
|
+
throw new Error(`[Zenith:Config] images.${key} must contain non-empty strings`);
|
|
51
|
+
}
|
|
52
|
+
out.push(entry.trim().toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
return [...new Set(out)];
|
|
55
|
+
}
|
|
56
|
+
function normalizePositiveIntArray(value, key) {
|
|
57
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
58
|
+
throw new Error(`[Zenith:Config] images.${key} must be a non-empty array`);
|
|
59
|
+
}
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const entry of value) {
|
|
62
|
+
out.push(asPositiveInt(entry, key));
|
|
63
|
+
}
|
|
64
|
+
return [...new Set(out)].sort((left, right) => left - right);
|
|
65
|
+
}
|
|
66
|
+
function normalizeRemotePattern(pattern) {
|
|
67
|
+
if (!isPlainObject(pattern)) {
|
|
68
|
+
throw new Error('[Zenith:Config] images.remotePatterns must contain plain objects');
|
|
69
|
+
}
|
|
70
|
+
const protocol = typeof pattern.protocol === 'string' && pattern.protocol.trim().length > 0
|
|
71
|
+
? pattern.protocol.trim().replace(/:$/, '').toLowerCase()
|
|
72
|
+
: 'https';
|
|
73
|
+
const hostname = typeof pattern.hostname === 'string' ? pattern.hostname.trim().toLowerCase() : '';
|
|
74
|
+
if (!hostname) {
|
|
75
|
+
throw new Error('[Zenith:Config] images.remotePatterns[].hostname is required');
|
|
76
|
+
}
|
|
77
|
+
const port = typeof pattern.port === 'string' ? pattern.port.trim() : '';
|
|
78
|
+
const pathname = typeof pattern.pathname === 'string' && pattern.pathname.trim().length > 0
|
|
79
|
+
? pattern.pathname.trim()
|
|
80
|
+
: '/**';
|
|
81
|
+
const search = typeof pattern.search === 'string' && pattern.search.length > 0
|
|
82
|
+
? pattern.search
|
|
83
|
+
: '';
|
|
84
|
+
return {
|
|
85
|
+
protocol,
|
|
86
|
+
hostname,
|
|
87
|
+
port,
|
|
88
|
+
pathname,
|
|
89
|
+
search
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function cloneImageConfig(config = DEFAULT_IMAGE_CONFIG) {
|
|
93
|
+
return {
|
|
94
|
+
...config,
|
|
95
|
+
formats: [...(config.formats || [])],
|
|
96
|
+
deviceSizes: [...(config.deviceSizes || [])],
|
|
97
|
+
imageSizes: [...(config.imageSizes || [])],
|
|
98
|
+
remotePatterns: Array.isArray(config.remotePatterns)
|
|
99
|
+
? config.remotePatterns.map((pattern) => ({ ...pattern }))
|
|
100
|
+
: []
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function normalizeImageConfig(input) {
|
|
104
|
+
if (input === undefined || input === null) {
|
|
105
|
+
return cloneImageConfig(DEFAULT_IMAGE_CONFIG);
|
|
106
|
+
}
|
|
107
|
+
if (!isPlainObject(input)) {
|
|
108
|
+
throw new Error('[Zenith:Config] images must be a plain object');
|
|
109
|
+
}
|
|
110
|
+
for (const key of Object.keys(input)) {
|
|
111
|
+
if (!TOP_LEVEL_KEYS.has(key)) {
|
|
112
|
+
throw new Error(`[Zenith:Config] Unknown key: "images.${key}"`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const config = cloneImageConfig(DEFAULT_IMAGE_CONFIG);
|
|
116
|
+
if ('formats' in input) {
|
|
117
|
+
config.formats = normalizeStringArray(input.formats, 'formats');
|
|
118
|
+
}
|
|
119
|
+
if ('quality' in input) {
|
|
120
|
+
config.quality = asPositiveInt(input.quality, 'quality');
|
|
121
|
+
}
|
|
122
|
+
if ('deviceSizes' in input) {
|
|
123
|
+
config.deviceSizes = normalizePositiveIntArray(input.deviceSizes, 'deviceSizes');
|
|
124
|
+
}
|
|
125
|
+
if ('imageSizes' in input) {
|
|
126
|
+
config.imageSizes = normalizePositiveIntArray(input.imageSizes, 'imageSizes');
|
|
127
|
+
}
|
|
128
|
+
if ('remotePatterns' in input) {
|
|
129
|
+
if (!Array.isArray(input.remotePatterns)) {
|
|
130
|
+
throw new Error('[Zenith:Config] images.remotePatterns must be an array');
|
|
131
|
+
}
|
|
132
|
+
config.remotePatterns = input.remotePatterns.map(normalizeRemotePattern);
|
|
133
|
+
}
|
|
134
|
+
if ('allowSvg' in input) {
|
|
135
|
+
if (typeof input.allowSvg !== 'boolean') {
|
|
136
|
+
throw new Error('[Zenith:Config] images.allowSvg must be boolean');
|
|
137
|
+
}
|
|
138
|
+
config.allowSvg = input.allowSvg;
|
|
139
|
+
}
|
|
140
|
+
if ('maxRemoteBytes' in input) {
|
|
141
|
+
config.maxRemoteBytes = asPositiveInt(input.maxRemoteBytes, 'maxRemoteBytes');
|
|
142
|
+
}
|
|
143
|
+
if ('maxPixels' in input) {
|
|
144
|
+
config.maxPixels = asPositiveInt(input.maxPixels, 'maxPixels');
|
|
145
|
+
}
|
|
146
|
+
if ('minimumCacheTTL' in input) {
|
|
147
|
+
config.minimumCacheTTL = asPositiveInt(input.minimumCacheTTL, 'minimumCacheTTL');
|
|
148
|
+
}
|
|
149
|
+
if ('dangerouslyAllowLocalNetwork' in input) {
|
|
150
|
+
if (typeof input.dangerouslyAllowLocalNetwork !== 'boolean') {
|
|
151
|
+
throw new Error('[Zenith:Config] images.dangerouslyAllowLocalNetwork must be boolean');
|
|
152
|
+
}
|
|
153
|
+
config.dangerouslyAllowLocalNetwork = input.dangerouslyAllowLocalNetwork;
|
|
154
|
+
}
|
|
155
|
+
return config;
|
|
156
|
+
}
|
|
157
|
+
function escapeRegex(value) {
|
|
158
|
+
return String(value).replace(/[|\\{}()[\]^$+?.*]/g, '\\$&');
|
|
159
|
+
}
|
|
160
|
+
function globToRegExp(glob, isHostname = false) {
|
|
161
|
+
let pattern = escapeRegex(glob);
|
|
162
|
+
pattern = pattern.replaceAll('\\*\\*', '__DOUBLE_STAR__');
|
|
163
|
+
pattern = pattern.replaceAll('\\*', isHostname ? '[^.]*' : '[^/]*');
|
|
164
|
+
pattern = pattern.replaceAll('__DOUBLE_STAR__', '.*');
|
|
165
|
+
return new RegExp(`^${pattern}$`, 'i');
|
|
166
|
+
}
|
|
167
|
+
function hostnameMatches(hostname, pattern) {
|
|
168
|
+
if (pattern.startsWith('*.')) {
|
|
169
|
+
const suffix = pattern.slice(1);
|
|
170
|
+
return hostname.endsWith(suffix) && hostname.length > suffix.length;
|
|
171
|
+
}
|
|
172
|
+
return globToRegExp(pattern, true).test(hostname);
|
|
173
|
+
}
|
|
174
|
+
export function matchRemotePattern(inputUrl, patterns) {
|
|
175
|
+
if (!inputUrl || !Array.isArray(patterns) || patterns.length === 0) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const url = inputUrl instanceof URL ? inputUrl : new URL(String(inputUrl));
|
|
179
|
+
const protocol = url.protocol.replace(/:$/, '').toLowerCase();
|
|
180
|
+
const hostname = url.hostname.toLowerCase();
|
|
181
|
+
const port = url.port || '';
|
|
182
|
+
const pathname = url.pathname || '/';
|
|
183
|
+
const search = url.search || '';
|
|
184
|
+
return patterns.some((pattern) => {
|
|
185
|
+
if (pattern.protocol && pattern.protocol !== protocol) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (!hostnameMatches(hostname, pattern.hostname || '')) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (pattern.port && pattern.port !== port) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (pattern.search && pattern.search !== search) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return globToRegExp(pattern.pathname || '/**').test(pathname);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
export function isRemoteImageUrl(value) {
|
|
201
|
+
if (typeof value !== 'string') {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return /^https?:\/\//i.test(value.trim());
|
|
205
|
+
}
|
|
206
|
+
export function normalizeImageSource(input) {
|
|
207
|
+
if (typeof input === 'string') {
|
|
208
|
+
const trimmed = input.trim();
|
|
209
|
+
if (!trimmed) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (isRemoteImageUrl(trimmed)) {
|
|
213
|
+
return { kind: 'remote', url: trimmed, width: null, height: null, alt: '' };
|
|
214
|
+
}
|
|
215
|
+
if (trimmed.startsWith('/')) {
|
|
216
|
+
return { kind: 'local', path: trimmed, width: null, height: null, alt: '' };
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (!isPlainObject(input)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const rawUrl = typeof input.url === 'string'
|
|
224
|
+
? input.url
|
|
225
|
+
: typeof input.src === 'string'
|
|
226
|
+
? input.src
|
|
227
|
+
: typeof input.path === 'string'
|
|
228
|
+
? input.path
|
|
229
|
+
: '';
|
|
230
|
+
const normalized = normalizeImageSource(rawUrl);
|
|
231
|
+
if (!normalized) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const width = Number.isInteger(input.width) && input.width > 0 ? input.width : null;
|
|
235
|
+
const height = Number.isInteger(input.height) && input.height > 0 ? input.height : null;
|
|
236
|
+
const alt = typeof input.alt === 'string' ? input.alt : '';
|
|
237
|
+
return {
|
|
238
|
+
...normalized,
|
|
239
|
+
width,
|
|
240
|
+
height,
|
|
241
|
+
alt
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
export function normalizeImageFormat(value) {
|
|
245
|
+
return String(value || '').trim().toLowerCase().replace(/^\./, '');
|
|
246
|
+
}
|
|
247
|
+
export function buildLocalImageKey(publicPath) {
|
|
248
|
+
const input = String(publicPath || '');
|
|
249
|
+
let hash = 2166136261;
|
|
250
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
251
|
+
hash ^= input.charCodeAt(index);
|
|
252
|
+
hash = Math.imul(hash, 16777619);
|
|
253
|
+
}
|
|
254
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
255
|
+
}
|
|
256
|
+
export function buildLocalVariantPath(publicPath, width, quality, format) {
|
|
257
|
+
const key = buildLocalImageKey(publicPath);
|
|
258
|
+
return `/_zenith/image/local/${key}/w${width}-q${quality}.${normalizeImageFormat(format)}`;
|
|
259
|
+
}
|
|
260
|
+
export function buildRemoteVariantPath(remoteUrl, width, quality, format) {
|
|
261
|
+
const query = new URLSearchParams();
|
|
262
|
+
query.set('url', String(remoteUrl || ''));
|
|
263
|
+
query.set('w', String(width));
|
|
264
|
+
query.set('q', String(quality));
|
|
265
|
+
if (format) {
|
|
266
|
+
query.set('f', normalizeImageFormat(format));
|
|
267
|
+
}
|
|
268
|
+
return `/_zenith/image?${query.toString()}`;
|
|
269
|
+
}
|
|
270
|
+
export function resolveWidthCandidates(width, sizes, config, manifestEntry) {
|
|
271
|
+
const base = new Set([
|
|
272
|
+
...(config?.deviceSizes || DEFAULT_DEVICE_SIZES),
|
|
273
|
+
...(config?.imageSizes || DEFAULT_IMAGE_SIZES)
|
|
274
|
+
]);
|
|
275
|
+
if (Number.isInteger(width) && width > 0) {
|
|
276
|
+
base.add(width);
|
|
277
|
+
base.add(width * 2);
|
|
278
|
+
}
|
|
279
|
+
if (!width && typeof sizes === 'string' && sizes.trim().length > 0) {
|
|
280
|
+
for (const candidate of config?.deviceSizes || DEFAULT_DEVICE_SIZES) {
|
|
281
|
+
base.add(candidate);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let widths = [...base].filter((entry) => Number.isInteger(entry) && entry > 0).sort((left, right) => left - right);
|
|
285
|
+
const available = Array.isArray(manifestEntry?.availableWidths) ? manifestEntry.availableWidths : null;
|
|
286
|
+
if (available && available.length > 0) {
|
|
287
|
+
widths = widths.filter((entry) => available.includes(entry));
|
|
288
|
+
if (widths.length === 0) {
|
|
289
|
+
widths = [...available];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return widths;
|
|
293
|
+
}
|
|
294
|
+
export function imageRuntimeGlobalName() {
|
|
295
|
+
return IMAGE_RUNTIME_GLOBAL;
|
|
296
|
+
}
|
|
297
|
+
export function normalizeImageRuntimePayload(payload) {
|
|
298
|
+
if (!isPlainObject(payload)) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
mode: payload.mode === 'endpoint' ? 'endpoint' : 'passthrough',
|
|
303
|
+
config: normalizeImageConfig(payload.config || {}),
|
|
304
|
+
localImages: isPlainObject(payload.localImages) ? payload.localImages : {}
|
|
305
|
+
};
|
|
306
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { resolve, join, dirname } from 'node:path';
|
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { createZenithLogger } from './ui/logger.js';
|
|
16
|
+
import { loadConfig } from './config.js';
|
|
16
17
|
const COMMANDS = ['dev', 'build', 'preview'];
|
|
17
18
|
const DEFAULT_VERSION = '0.0.0';
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -59,22 +60,6 @@ function resolvePort(args, fallback) {
|
|
|
59
60
|
}
|
|
60
61
|
return fallback;
|
|
61
62
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Load zenith.config.js from project root.
|
|
64
|
-
*
|
|
65
|
-
* @param {string} projectRoot
|
|
66
|
-
* @returns {Promise<object>}
|
|
67
|
-
*/
|
|
68
|
-
async function loadConfig(projectRoot) {
|
|
69
|
-
const configPath = join(projectRoot, 'zenith.config.js');
|
|
70
|
-
try {
|
|
71
|
-
const mod = await import(configPath);
|
|
72
|
-
return mod.default || {};
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return {};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
63
|
/**
|
|
79
64
|
* CLI entry point.
|
|
80
65
|
*
|
|
@@ -142,7 +127,7 @@ export async function cli(args, cwd) {
|
|
|
142
127
|
const port = resolvePort(args.slice(1), 4000);
|
|
143
128
|
const host = process.env.ZENITH_PREVIEW_HOST || '127.0.0.1';
|
|
144
129
|
logger.dev('Starting preview server…');
|
|
145
|
-
const preview = await createPreviewServer({ distDir: outDir, port, host, logger });
|
|
130
|
+
const preview = await createPreviewServer({ distDir: outDir, port, host, logger, config, projectRoot });
|
|
146
131
|
logger.ok(`http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${preview.port}`);
|
|
147
132
|
process.on('SIGINT', () => {
|
|
148
133
|
preview.close();
|
package/dist/manifest.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function generateManifest(pagesDir, extension = '.zen') {
|
|
|
32
32
|
for (const entry of entries) {
|
|
33
33
|
_validateParams(entry.path);
|
|
34
34
|
}
|
|
35
|
+
_validateManifestConflicts(entries);
|
|
35
36
|
// Sort: static first, dynamic after, alpha within each category
|
|
36
37
|
return _sortEntries(entries);
|
|
37
38
|
}
|
|
@@ -142,6 +143,32 @@ function _validateParams(routePath) {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Reject duplicate or structurally ambiguous routes across different files.
|
|
148
|
+
*
|
|
149
|
+
* @param {ManifestEntry[]} entries
|
|
150
|
+
*/
|
|
151
|
+
function _validateManifestConflicts(entries) {
|
|
152
|
+
/** @type {Map<string, ManifestEntry>} */
|
|
153
|
+
const exactPaths = new Map();
|
|
154
|
+
/** @type {Map<string, ManifestEntry>} */
|
|
155
|
+
const structural = new Map();
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const existingExact = exactPaths.get(entry.path);
|
|
158
|
+
if (existingExact) {
|
|
159
|
+
throw new Error(`[Zenith CLI] Duplicate route path '${entry.path}' generated by '${existingExact.file}' and '${entry.file}'`);
|
|
160
|
+
}
|
|
161
|
+
exactPaths.set(entry.path, entry);
|
|
162
|
+
const signature = _routeConflictSignature(entry.path);
|
|
163
|
+
const existingStructural = structural.get(signature);
|
|
164
|
+
if (existingStructural && existingStructural.path !== entry.path) {
|
|
165
|
+
throw new Error(`[Zenith CLI] Ambiguous route patterns '${existingStructural.path}' (${existingStructural.file}) and '${entry.path}' (${entry.file}) match the same URL shape`);
|
|
166
|
+
}
|
|
167
|
+
if (!existingStructural) {
|
|
168
|
+
structural.set(signature, entry);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
145
172
|
/**
|
|
146
173
|
* Check if a route contains any dynamic segments.
|
|
147
174
|
*
|
|
@@ -160,6 +187,24 @@ function _isDynamic(routePath) {
|
|
|
160
187
|
function _sortEntries(entries) {
|
|
161
188
|
return [...entries].sort((a, b) => compareRouteSpecificity(a.path, b.path));
|
|
162
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Normalize a route path so structurally equivalent param names conflict.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} routePath
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
function _routeConflictSignature(routePath) {
|
|
197
|
+
const segments = routePath.split('/').filter(Boolean).map((segment) => {
|
|
198
|
+
if (segment.startsWith('*')) {
|
|
199
|
+
return segment.endsWith('?') ? '*?' : '*';
|
|
200
|
+
}
|
|
201
|
+
if (segment.startsWith(':')) {
|
|
202
|
+
return ':';
|
|
203
|
+
}
|
|
204
|
+
return segment;
|
|
205
|
+
});
|
|
206
|
+
return '/' + segments.join('/');
|
|
207
|
+
}
|
|
163
208
|
/**
|
|
164
209
|
* Deterministic route precedence:
|
|
165
210
|
* static segment > param segment > catch-all segment.
|
package/dist/preview.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create and start a preview server.
|
|
3
3
|
*
|
|
4
|
-
* @param {{ distDir: string, port?: number, host?: string, logger?: object | null }} options
|
|
4
|
+
* @param {{ distDir: string, port?: number, host?: string, logger?: object | null, config?: object, projectRoot?: string }} options
|
|
5
5
|
* @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
|
|
6
6
|
*/
|
|
7
7
|
export function createPreviewServer(options: {
|
|
@@ -9,6 +9,8 @@ export function createPreviewServer(options: {
|
|
|
9
9
|
port?: number;
|
|
10
10
|
host?: string;
|
|
11
11
|
logger?: object | null;
|
|
12
|
+
config?: object;
|
|
13
|
+
projectRoot?: string;
|
|
12
14
|
}): Promise<{
|
|
13
15
|
server: import("http").Server;
|
|
14
16
|
port: number;
|
|
@@ -59,6 +61,7 @@ export function executeServerRoute({ source, sourcePath, params, requestUrl, req
|
|
|
59
61
|
load: string;
|
|
60
62
|
};
|
|
61
63
|
}>;
|
|
64
|
+
export function defaultRouteDenyMessage(status: any): "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
|
|
62
65
|
/**
|
|
63
66
|
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
64
67
|
* @returns {Promise<Record<string, unknown> | null>}
|
package/dist/preview.js
CHANGED
|
@@ -13,6 +13,10 @@ import { createServer } from 'node:http';
|
|
|
13
13
|
import { access, readFile } from 'node:fs/promises';
|
|
14
14
|
import { extname, join, normalize, resolve, sep, dirname } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { loadConfig } from './config.js';
|
|
17
|
+
import { materializeImageMarkup } from './images/materialize.js';
|
|
18
|
+
import { createImageRuntimePayload, injectImageRuntimePayload } from './images/payload.js';
|
|
19
|
+
import { handleImageRequest } from './images/service.js';
|
|
16
20
|
import { createSilentLogger } from './ui/logger.js';
|
|
17
21
|
import { compareRouteSpecificity, matchRoute as matchManifestRoute, resolveRequestRoute } from './server/resolve-request-route.js';
|
|
18
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -22,8 +26,12 @@ const MIME_TYPES = {
|
|
|
22
26
|
'.css': 'text/css',
|
|
23
27
|
'.json': 'application/json',
|
|
24
28
|
'.png': 'image/png',
|
|
29
|
+
'.jpeg': 'image/jpeg',
|
|
25
30
|
'.jpg': 'image/jpeg',
|
|
26
|
-
'.svg': 'image/svg+xml'
|
|
31
|
+
'.svg': 'image/svg+xml',
|
|
32
|
+
'.webp': 'image/webp',
|
|
33
|
+
'.avif': 'image/avif',
|
|
34
|
+
'.gif': 'image/gif'
|
|
27
35
|
};
|
|
28
36
|
const SERVER_SCRIPT_RUNNER = String.raw `
|
|
29
37
|
import vm from 'node:vm';
|
|
@@ -378,14 +386,30 @@ try {
|
|
|
378
386
|
/**
|
|
379
387
|
* Create and start a preview server.
|
|
380
388
|
*
|
|
381
|
-
* @param {{ distDir: string, port?: number, host?: string, logger?: object | null }} options
|
|
389
|
+
* @param {{ distDir: string, port?: number, host?: string, logger?: object | null, config?: object, projectRoot?: string }} options
|
|
382
390
|
* @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
|
|
383
391
|
*/
|
|
384
392
|
export async function createPreviewServer(options) {
|
|
393
|
+
const resolvedProjectRoot = options?.projectRoot ? resolve(options.projectRoot) : resolve(options.distDir, '..');
|
|
394
|
+
const resolvedConfig = options?.config && typeof options.config === 'object'
|
|
395
|
+
? options.config
|
|
396
|
+
: await loadConfig(resolvedProjectRoot);
|
|
385
397
|
const { distDir, port = 4000, host = '127.0.0.1', logger: providedLogger = null } = options;
|
|
398
|
+
const projectRoot = resolvedProjectRoot;
|
|
399
|
+
const config = resolvedConfig;
|
|
386
400
|
const logger = providedLogger || createSilentLogger();
|
|
387
401
|
const verboseLogging = logger.mode?.logLevel === 'verbose';
|
|
388
402
|
let actualPort = port;
|
|
403
|
+
async function loadImageManifest() {
|
|
404
|
+
try {
|
|
405
|
+
const manifestRaw = await readFile(join(distDir, '_zenith', 'image', 'manifest.json'), 'utf8');
|
|
406
|
+
const parsed = JSON.parse(manifestRaw);
|
|
407
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return {};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
389
413
|
function publicHost() {
|
|
390
414
|
if (host === '0.0.0.0' || host === '::') {
|
|
391
415
|
return '127.0.0.1';
|
|
@@ -469,6 +493,14 @@ export async function createPreviewServer(options) {
|
|
|
469
493
|
}));
|
|
470
494
|
return;
|
|
471
495
|
}
|
|
496
|
+
if (url.pathname === '/_zenith/image') {
|
|
497
|
+
await handleImageRequest(req, res, {
|
|
498
|
+
requestUrl: url,
|
|
499
|
+
projectRoot,
|
|
500
|
+
config: config.images
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
472
504
|
if (extname(url.pathname) && extname(url.pathname) !== '.html') {
|
|
473
505
|
const staticPath = resolveWithinDist(distDir, url.pathname);
|
|
474
506
|
if (!staticPath || !(await fileExists(staticPath))) {
|
|
@@ -540,7 +572,7 @@ export async function createPreviewServer(options) {
|
|
|
540
572
|
if (result && result.kind === 'deny') {
|
|
541
573
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
542
574
|
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
543
|
-
res.end(result.message || (status
|
|
575
|
+
res.end(result.message || defaultRouteDenyMessage(status));
|
|
544
576
|
return;
|
|
545
577
|
}
|
|
546
578
|
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
@@ -548,9 +580,20 @@ export async function createPreviewServer(options) {
|
|
|
548
580
|
}
|
|
549
581
|
}
|
|
550
582
|
let html = await readFile(htmlPath, 'utf8');
|
|
583
|
+
if (resolved.matched && resolved.route?.page_asset) {
|
|
584
|
+
const pageAssetPath = resolveWithinDist(distDir, resolved.route.page_asset);
|
|
585
|
+
html = await materializeImageMarkup({
|
|
586
|
+
html,
|
|
587
|
+
pageAssetPath,
|
|
588
|
+
payload: createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint'),
|
|
589
|
+
ssrData: ssrPayload,
|
|
590
|
+
routePathname: resolved.route.path || url.pathname
|
|
591
|
+
});
|
|
592
|
+
}
|
|
551
593
|
if (ssrPayload) {
|
|
552
594
|
html = injectSsrPayload(html, ssrPayload);
|
|
553
595
|
}
|
|
596
|
+
html = injectImageRuntimePayload(html, createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint'));
|
|
554
597
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
555
598
|
res.end(html);
|
|
556
599
|
}
|
|
@@ -674,6 +717,15 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
674
717
|
trace: { guard: 'none', load: 'data' }
|
|
675
718
|
};
|
|
676
719
|
}
|
|
720
|
+
export function defaultRouteDenyMessage(status) {
|
|
721
|
+
if (status === 401)
|
|
722
|
+
return 'Unauthorized';
|
|
723
|
+
if (status === 403)
|
|
724
|
+
return 'Forbidden';
|
|
725
|
+
if (status === 404)
|
|
726
|
+
return 'Not Found';
|
|
727
|
+
return 'Internal Server Error';
|
|
728
|
+
}
|
|
677
729
|
/**
|
|
678
730
|
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
679
731
|
* @returns {Promise<Record<string, unknown> | null>}
|
|
@@ -697,11 +749,12 @@ export async function executeServerScript(input) {
|
|
|
697
749
|
};
|
|
698
750
|
}
|
|
699
751
|
if (result.kind === 'deny') {
|
|
752
|
+
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
700
753
|
return {
|
|
701
754
|
__zenith_error: {
|
|
702
|
-
status
|
|
703
|
-
code: 'ACCESS_DENIED',
|
|
704
|
-
message: String(result.message ||
|
|
755
|
+
status,
|
|
756
|
+
code: status >= 500 ? 'LOAD_FAILED' : (status === 404 ? 'NOT_FOUND' : 'ACCESS_DENIED'),
|
|
757
|
+
message: String(result.message || defaultRouteDenyMessage(status))
|
|
705
758
|
}
|
|
706
759
|
};
|
|
707
760
|
}
|
|
@@ -8,9 +8,15 @@
|
|
|
8
8
|
// Pipeline:
|
|
9
9
|
// buildComponentRegistry() → expandComponents() → expanded source string
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
-
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
-
import { basename, extname, join } from 'node:path';
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import { findMatchingComponentClose, findNextKnownComponentTag } from './component-tag-parser.js';
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const FRAMEWORK_COMPONENTS = [
|
|
18
|
+
['Image', join(__dirname, 'framework-components', 'Image.zen')]
|
|
19
|
+
];
|
|
14
20
|
// ---------------------------------------------------------------------------
|
|
15
21
|
// Registry: Map<PascalCaseName, absolutePath>
|
|
16
22
|
// ---------------------------------------------------------------------------
|
|
@@ -37,6 +43,18 @@ export function buildComponentRegistry(srcDir) {
|
|
|
37
43
|
}
|
|
38
44
|
walkDir(dir, registry);
|
|
39
45
|
}
|
|
46
|
+
for (const [componentName, componentPath] of FRAMEWORK_COMPONENTS) {
|
|
47
|
+
if (!existsSync(componentPath)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (registry.has(componentName)) {
|
|
51
|
+
throw new Error(`Duplicate component name "${componentName}":\n` +
|
|
52
|
+
` 1) ${registry.get(componentName)}\n` +
|
|
53
|
+
` 2) ${componentPath}\n` +
|
|
54
|
+
'Rename the local component to avoid shadowing the Zenith framework component.');
|
|
55
|
+
}
|
|
56
|
+
registry.set(componentName, componentPath);
|
|
57
|
+
}
|
|
40
58
|
return registry;
|
|
41
59
|
}
|
|
42
60
|
/**
|
|
@@ -207,7 +225,6 @@ function expandTag(name, children, registry, sourceFile, chain, usedComponents)
|
|
|
207
225
|
}
|
|
208
226
|
const compSource = readFileSync(compPath, 'utf8');
|
|
209
227
|
let template = extractTemplate(compSource);
|
|
210
|
-
// Check Document Mode
|
|
211
228
|
const docMode = isDocumentMode(template);
|
|
212
229
|
if (docMode) {
|
|
213
230
|
// Document Mode: must contain exactly one <slot />
|
package/dist/server-contract.js
CHANGED
|
@@ -49,8 +49,9 @@ function assertValidRouteResultShape(value, where, allowedKinds) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
if (kind === 'deny') {
|
|
52
|
-
if (!Number.isInteger(value.status) ||
|
|
53
|
-
|
|
52
|
+
if (!Number.isInteger(value.status) ||
|
|
53
|
+
(value.status !== 401 && value.status !== 403 && value.status !== 404)) {
|
|
54
|
+
throw new Error(`[Zenith] ${where}: deny status must be 401, 403, or 404.`);
|
|
54
55
|
}
|
|
55
56
|
if (value.message !== undefined && typeof value.message !== 'string') {
|
|
56
57
|
throw new Error(`[Zenith] ${where}: deny message must be a string when provided.`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{
|
|
3
|
+
* sourceFile: string,
|
|
4
|
+
* inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null,
|
|
5
|
+
* adjacentGuardPath?: string | null,
|
|
6
|
+
* adjacentLoadPath?: string | null
|
|
7
|
+
* }} input
|
|
8
|
+
* @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null }}
|
|
9
|
+
*/
|
|
10
|
+
export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath }: {
|
|
11
|
+
sourceFile: string;
|
|
12
|
+
inlineServerScript?: {
|
|
13
|
+
source: string;
|
|
14
|
+
prerender: boolean;
|
|
15
|
+
has_guard: boolean;
|
|
16
|
+
has_load: boolean;
|
|
17
|
+
source_path: string;
|
|
18
|
+
} | null;
|
|
19
|
+
adjacentGuardPath?: string | null;
|
|
20
|
+
adjacentLoadPath?: string | null;
|
|
21
|
+
}): {
|
|
22
|
+
serverScript: {
|
|
23
|
+
source: string;
|
|
24
|
+
prerender: boolean;
|
|
25
|
+
has_guard: boolean;
|
|
26
|
+
has_load: boolean;
|
|
27
|
+
source_path: string;
|
|
28
|
+
} | null;
|
|
29
|
+
guardPath: string | null;
|
|
30
|
+
loadPath: string | null;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} sourceFile
|
|
34
|
+
* @returns {{ guardPath: string | null, loadPath: string | null }}
|
|
35
|
+
*/
|
|
36
|
+
export function resolveAdjacentServerModules(sourceFile: string): {
|
|
37
|
+
guardPath: string | null;
|
|
38
|
+
loadPath: string | null;
|
|
39
|
+
};
|