@zenithbuild/cli 0.7.11 → 0.7.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/README.md +10 -1
- package/dist/adapters/adapter-netlify-static.d.ts +2 -5
- package/dist/adapters/adapter-netlify.d.ts +2 -5
- package/dist/adapters/adapter-netlify.js +22 -5
- package/dist/adapters/adapter-types.d.ts +32 -13
- package/dist/adapters/adapter-types.js +0 -59
- package/dist/adapters/adapter-vercel-static.d.ts +2 -5
- package/dist/adapters/adapter-vercel.d.ts +2 -5
- package/dist/adapters/adapter-vercel.js +21 -6
- package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
- package/dist/adapters/copy-hosted-page-runtime.js +68 -3
- package/dist/adapters/resolve-adapter.d.ts +6 -4
- package/dist/build/expression-rewrites.d.ts +3 -1
- package/dist/build/expression-rewrites.js +14 -2
- package/dist/build/page-component-loop.d.ts +1 -0
- package/dist/build/page-component-loop.js +66 -6
- package/dist/build/page-ir-normalization.js +7 -0
- package/dist/build/page-loop-state.d.ts +2 -1
- package/dist/build/page-loop-state.js +9 -2
- package/dist/build/page-loop.js +10 -1
- package/dist/build/scoped-expression-context.d.ts +5 -0
- package/dist/build/scoped-expression-context.js +133 -0
- package/dist/build/type-declarations.d.ts +2 -1
- package/dist/build/type-declarations.js +31 -1
- package/dist/build-output-manifest.d.ts +10 -6
- package/dist/build-output-manifest.js +4 -1
- package/dist/build.js +11 -2
- package/dist/component-instance-ir.js +1 -0
- package/dist/component-occurrences.d.ts +9 -0
- package/dist/component-occurrences.js +18 -0
- package/dist/config-plugins.d.ts +12 -0
- package/dist/config-plugins.js +100 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +56 -5
- package/dist/dev-server/request-handler.js +46 -4
- package/dist/dev-server.js +92 -4
- package/dist/global-middleware-runtime-source.d.ts +15 -0
- package/dist/global-middleware-runtime-source.js +62 -0
- package/dist/global-middleware.d.ts +13 -0
- package/dist/global-middleware.js +252 -0
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +66 -26
- package/dist/preview/request-handler.js +78 -5
- package/dist/preview/server-runner.d.ts +7 -2
- package/dist/preview/server-runner.js +19 -6
- package/dist/preview/server-script-runner-template.js +97 -29
- package/dist/route-classification.d.ts +2 -1
- package/dist/route-classification.js +6 -2
- package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
- package/dist/scoped-server-data/analyze-owner-file.js +149 -0
- package/dist/scoped-server-data/diagnostics.d.ts +18 -0
- package/dist/scoped-server-data/diagnostics.js +32 -0
- package/dist/scoped-server-data/lowering.d.ts +27 -0
- package/dist/scoped-server-data/lowering.js +242 -0
- package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
- package/dist/scoped-server-data/manifest-integration.js +125 -0
- package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
- package/dist/scoped-server-data/owner-scanner.js +55 -0
- package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
- package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
- package/dist/scoped-server-data/runtime.d.ts +24 -0
- package/dist/scoped-server-data/runtime.js +121 -0
- package/dist/scoped-server-data/serialization-set.d.ts +2 -0
- package/dist/scoped-server-data/serialization-set.js +52 -0
- package/dist/scoped-server-data/static-props.d.ts +12 -0
- package/dist/scoped-server-data/static-props.js +307 -0
- package/dist/scoped-server-data/type-declarations.d.ts +10 -0
- package/dist/scoped-server-data/type-declarations.js +368 -0
- package/dist/scoped-server-data/types.d.ts +74 -0
- package/dist/scoped-server-data/types.js +1 -0
- package/dist/server-contract/auth-control-flow.d.ts +1 -0
- package/dist/server-contract/auth-control-flow.js +10 -0
- package/dist/server-contract/resolve.d.ts +19 -0
- package/dist/server-contract/resolve.js +85 -13
- package/dist/server-contract/resolved-envelope.d.ts +9 -0
- package/dist/server-contract/resolved-envelope.js +14 -0
- package/dist/server-contract/stage.js +1 -10
- package/dist/server-module-output.d.ts +9 -0
- package/dist/server-module-output.js +250 -0
- package/dist/server-output.d.ts +7 -1
- package/dist/server-output.js +138 -179
- package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
- package/dist/server-runtime/matched-route-pipeline.js +1 -0
- package/dist/server-runtime/node-server.js +21 -1
- package/dist/server-runtime/route-render.d.ts +12 -3
- package/dist/server-runtime/route-render.js +67 -13
- package/package.json +3 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const PLUGIN_OBJECT_KEYS = new Set(['name', 'config']);
|
|
2
|
+
const freezeObject = Object.freeze;
|
|
3
|
+
export const PLUGIN_CONFIG_PATCH_KEYS = new Set([
|
|
4
|
+
'router',
|
|
5
|
+
'embeddedMarkupExpressions',
|
|
6
|
+
'typescriptDefault',
|
|
7
|
+
'strictDomLints',
|
|
8
|
+
'images',
|
|
9
|
+
'basePath',
|
|
10
|
+
'outDir'
|
|
11
|
+
]);
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const proto = Object.getPrototypeOf(value);
|
|
17
|
+
return proto === Object.prototype || proto === null;
|
|
18
|
+
}
|
|
19
|
+
function describePlugin(index, plugin) {
|
|
20
|
+
if (plugin && typeof plugin === 'object' && typeof plugin.name === 'string' && plugin.name.trim()) {
|
|
21
|
+
return `"${plugin.name.trim()}"`;
|
|
22
|
+
}
|
|
23
|
+
return `at index ${index}`;
|
|
24
|
+
}
|
|
25
|
+
export function normalizePlugins(value) {
|
|
26
|
+
if (!Array.isArray(value)) {
|
|
27
|
+
throw new Error('[Zenith:Config] Key "plugins" must be an array');
|
|
28
|
+
}
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
return value.map((plugin, index) => {
|
|
31
|
+
if (!isPlainObject(plugin)) {
|
|
32
|
+
throw new Error(`[Zenith:Config] Plugin at index ${index} must be a plain object`);
|
|
33
|
+
}
|
|
34
|
+
for (const key of Object.keys(plugin)) {
|
|
35
|
+
if (!PLUGIN_OBJECT_KEYS.has(key)) {
|
|
36
|
+
throw new Error(`[Zenith:Config] Plugin ${describePlugin(index, plugin)} uses unsupported key "${key}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof plugin.name !== 'string' || plugin.name.trim().length === 0) {
|
|
40
|
+
throw new Error(`[Zenith:Config] Plugin at index ${index} must have a non-empty name`);
|
|
41
|
+
}
|
|
42
|
+
const name = plugin.name.trim();
|
|
43
|
+
if (seen.has(name)) {
|
|
44
|
+
throw new Error(`[Zenith:Config] Duplicate plugin name: "${name}"`);
|
|
45
|
+
}
|
|
46
|
+
seen.add(name);
|
|
47
|
+
if ('config' in plugin && typeof plugin.config !== 'function') {
|
|
48
|
+
throw new Error(`[Zenith:Config] Plugin "${name}" key "config" must be a function`);
|
|
49
|
+
}
|
|
50
|
+
return 'config' in plugin ? { name, config: plugin.config } : { name };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function assertPluginConfigPatch(value) {
|
|
54
|
+
if (!isPlainObject(value)) {
|
|
55
|
+
throw new Error('config hook must return a plain object patch');
|
|
56
|
+
}
|
|
57
|
+
for (const key of Object.keys(value)) {
|
|
58
|
+
if (!PLUGIN_CONFIG_PATCH_KEYS.has(key)) {
|
|
59
|
+
throw new Error(`${key} is not patchable`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function cloneConfigValue(value, seen = new Map()) {
|
|
64
|
+
if (!value || typeof value !== 'object') {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (seen.has(value)) {
|
|
68
|
+
return seen.get(value);
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
const out = [];
|
|
72
|
+
seen.set(value, out);
|
|
73
|
+
for (const item of value) {
|
|
74
|
+
out.push(cloneConfigValue(item, seen));
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
const out = {};
|
|
79
|
+
seen.set(value, out);
|
|
80
|
+
for (const [key, child] of Object.entries(value)) {
|
|
81
|
+
out[key] = cloneConfigValue(child, seen);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export function deepFreeze(value, seen = new Set()) {
|
|
86
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function') || seen.has(value)) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
seen.add(value);
|
|
90
|
+
for (const key of Object.keys(value)) {
|
|
91
|
+
deepFreeze(value[key], seen);
|
|
92
|
+
}
|
|
93
|
+
return freezeObject(value);
|
|
94
|
+
}
|
|
95
|
+
export function pluginHookError(pluginName, hookName, error) {
|
|
96
|
+
const message = error && typeof error.message === 'string'
|
|
97
|
+
? error.message
|
|
98
|
+
: String(error);
|
|
99
|
+
return new Error(`[Zenith plugin ${pluginName}] ${hookName} failed: ${message}`);
|
|
100
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join, resolve } from 'node:path';
|
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { KNOWN_TARGETS } from './adapters/adapter-types.js';
|
|
7
7
|
import { normalizeBasePath } from './base-path.js';
|
|
8
|
+
import { assertPluginConfigPatch, cloneConfigValue, deepFreeze, normalizePlugins, pluginHookError } from './config-plugins.js';
|
|
8
9
|
import { normalizeImageConfig } from './images/shared.js';
|
|
9
10
|
const PACKAGE_REQUIRE = createRequire(import.meta.url);
|
|
10
11
|
const CONFIG_FILES = ['zenith.config.ts', 'zenith.config.js'];
|
|
@@ -19,7 +20,8 @@ export const DEFAULT_CONFIG = {
|
|
|
19
20
|
target: 'static',
|
|
20
21
|
adapter: null,
|
|
21
22
|
strictDomLints: false,
|
|
22
|
-
images: normalizeImageConfig()
|
|
23
|
+
images: normalizeImageConfig(),
|
|
24
|
+
plugins: []
|
|
23
25
|
};
|
|
24
26
|
const TOP_LEVEL_SCHEMA = {
|
|
25
27
|
router: 'boolean',
|
|
@@ -31,7 +33,8 @@ const TOP_LEVEL_SCHEMA = {
|
|
|
31
33
|
target: 'string',
|
|
32
34
|
adapter: 'object',
|
|
33
35
|
strictDomLints: 'boolean',
|
|
34
|
-
images: 'object'
|
|
36
|
+
images: 'object',
|
|
37
|
+
plugins: 'array'
|
|
35
38
|
};
|
|
36
39
|
function attachConfigMeta(config, explicitKeys) {
|
|
37
40
|
Object.defineProperty(config, CONFIG_META, {
|
|
@@ -160,7 +163,7 @@ export function resolveConfigOutDir(projectRoot, config) {
|
|
|
160
163
|
}
|
|
161
164
|
export function validateConfig(config) {
|
|
162
165
|
if (config === null || config === undefined) {
|
|
163
|
-
return attachConfigMeta({ ...DEFAULT_CONFIG, images: normalizeImageConfig() }, []);
|
|
166
|
+
return attachConfigMeta({ ...DEFAULT_CONFIG, images: normalizeImageConfig(), plugins: [] }, []);
|
|
164
167
|
}
|
|
165
168
|
if (typeof config !== 'object' || Array.isArray(config)) {
|
|
166
169
|
throw new Error('[Zenith:Config] Config must be a plain object');
|
|
@@ -175,7 +178,8 @@ export function validateConfig(config) {
|
|
|
175
178
|
}
|
|
176
179
|
const result = {
|
|
177
180
|
...DEFAULT_CONFIG,
|
|
178
|
-
images: normalizeImageConfig(DEFAULT_CONFIG.images)
|
|
181
|
+
images: normalizeImageConfig(DEFAULT_CONFIG.images),
|
|
182
|
+
plugins: []
|
|
179
183
|
};
|
|
180
184
|
for (const [key, expectedType] of Object.entries(TOP_LEVEL_SCHEMA)) {
|
|
181
185
|
if (!(key in config)) {
|
|
@@ -190,6 +194,10 @@ export function validateConfig(config) {
|
|
|
190
194
|
result.adapter = validateAdapterValue(value);
|
|
191
195
|
continue;
|
|
192
196
|
}
|
|
197
|
+
if (key === 'plugins') {
|
|
198
|
+
result.plugins = normalizePlugins(value);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
193
201
|
if (typeof value !== expectedType) {
|
|
194
202
|
throw new Error(`[Zenith:Config] Key "${key}" must be ${expectedType}, got ${typeof value}`);
|
|
195
203
|
}
|
|
@@ -207,6 +215,48 @@ export function validateConfig(config) {
|
|
|
207
215
|
}
|
|
208
216
|
return attachConfigMeta(result, Object.keys(config));
|
|
209
217
|
}
|
|
218
|
+
function normalizeConfigPatch(patch) {
|
|
219
|
+
assertPluginConfigPatch(patch);
|
|
220
|
+
const keys = Object.keys(patch);
|
|
221
|
+
const normalized = validateConfig(patch);
|
|
222
|
+
const out = {};
|
|
223
|
+
for (const key of keys) {
|
|
224
|
+
out[key] = cloneConfigValue(normalized[key]);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
async function runPluginConfigHooks(config, projectRoot) {
|
|
229
|
+
let current = config;
|
|
230
|
+
for (const plugin of current.plugins) {
|
|
231
|
+
if (typeof plugin.config !== 'function') {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
let patch;
|
|
235
|
+
try {
|
|
236
|
+
const snapshot = deepFreeze(cloneConfigValue(current));
|
|
237
|
+
patch = await plugin.config(snapshot, { projectRoot });
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
throw pluginHookError(plugin.name, 'config', error);
|
|
241
|
+
}
|
|
242
|
+
if (patch === undefined || patch === null) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
let normalizedPatch;
|
|
246
|
+
try {
|
|
247
|
+
normalizedPatch = normalizeConfigPatch(patch);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
throw pluginHookError(plugin.name, 'config', error);
|
|
251
|
+
}
|
|
252
|
+
const explicitKeys = new Set(current?.[CONFIG_META]?.explicitKeys || []);
|
|
253
|
+
for (const key of Object.keys(normalizedPatch)) {
|
|
254
|
+
explicitKeys.add(key);
|
|
255
|
+
}
|
|
256
|
+
current = attachConfigMeta({ ...current, ...normalizedPatch, plugins: current.plugins }, explicitKeys);
|
|
257
|
+
}
|
|
258
|
+
return current;
|
|
259
|
+
}
|
|
210
260
|
export async function loadConfig(projectRoot) {
|
|
211
261
|
const resolvedProjectRoot = resolve(projectRoot);
|
|
212
262
|
const configPath = resolveConfigFile(resolvedProjectRoot);
|
|
@@ -216,5 +266,6 @@ export async function loadConfig(projectRoot) {
|
|
|
216
266
|
const mod = configPath.endsWith('.ts')
|
|
217
267
|
? await importTypescriptConfig(configPath, resolvedProjectRoot)
|
|
218
268
|
: await importJavascriptConfig(configPath, resolvedProjectRoot);
|
|
219
|
-
|
|
269
|
+
const config = validateConfig(mod.default || mod);
|
|
270
|
+
return markLoaded(await runPluginConfigHooks(config, resolvedProjectRoot));
|
|
220
271
|
}
|
|
@@ -11,8 +11,18 @@ import { handleImageRequest } from '../images/service.js';
|
|
|
11
11
|
import { resolveRequestRoute } from '../server/resolve-request-route.js';
|
|
12
12
|
import { respondWithDevBuildError } from './build-error-response.js';
|
|
13
13
|
import { handleRouteCheckRequest } from './route-check.js';
|
|
14
|
+
function respondWithMiddlewareSourceError(res, error) {
|
|
15
|
+
logServerException('dev server route execution failed', error);
|
|
16
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
17
|
+
res.end(clientFacingRouteMessage(500));
|
|
18
|
+
}
|
|
19
|
+
function hasRouteScopedServerData(route) {
|
|
20
|
+
return route?.has_scoped_server_data === true &&
|
|
21
|
+
Array.isArray(route?.scoped_server_data) &&
|
|
22
|
+
route.scoped_server_data.length > 0;
|
|
23
|
+
}
|
|
14
24
|
export function createDevRequestHandler(options) {
|
|
15
|
-
const { outDir, projectRoot, imageConfig, configuredBasePath, routeCheckEnabled, isStaticExportTarget, logger, verboseLogging, buildSession, state, serverOrigin, loadRoutesForRequests, readFileForRequest, trace404, looksLikeJsonRequest, classifyNotFound, infer404Cause, buildNotFoundPayload, renderNotFoundHtml, appendSetCookieHeaders, MIME_TYPES, EVENT_STREAM_MIME, LEGACY_DEV_STREAM_PATH, IMAGE_RUNTIME_TAG_RE } = options;
|
|
25
|
+
const { outDir, projectRoot, imageConfig, configuredBasePath, routeCheckEnabled, isStaticExportTarget, logger, verboseLogging, buildSession, state, serverOrigin, loadRoutesForRequests, loadGlobalMiddlewareForRequests, readFileForRequest, trace404, looksLikeJsonRequest, classifyNotFound, infer404Cause, buildNotFoundPayload, renderNotFoundHtml, appendSetCookieHeaders, MIME_TYPES, EVENT_STREAM_MIME, LEGACY_DEV_STREAM_PATH, IMAGE_RUNTIME_TAG_RE } = options;
|
|
16
26
|
return async function handleDevRequest(req, res) {
|
|
17
27
|
const url = new URL(req.url, serverOrigin());
|
|
18
28
|
const pathname = url.pathname;
|
|
@@ -205,6 +215,16 @@ export function createDevRequestHandler(options) {
|
|
|
205
215
|
canonicalUrl.pathname = canonicalPath;
|
|
206
216
|
const resolvedResource = resolveRequestRoute(canonicalUrl, routes.resourceRoutes || []);
|
|
207
217
|
if (resolvedResource.matched && resolvedResource.route) {
|
|
218
|
+
let globalMiddleware = null;
|
|
219
|
+
try {
|
|
220
|
+
globalMiddleware = loadGlobalMiddlewareForRequests
|
|
221
|
+
? await loadGlobalMiddlewareForRequests()
|
|
222
|
+
: null;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
respondWithMiddlewareSourceError(res, error);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
208
228
|
const requestMethod = req.method || 'GET';
|
|
209
229
|
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
210
230
|
? null
|
|
@@ -220,7 +240,9 @@ export function createDevRequestHandler(options) {
|
|
|
220
240
|
routePattern: resolvedResource.route.path,
|
|
221
241
|
routeFile: resolvedResource.route.server_script_path || '',
|
|
222
242
|
routeId: resolvedResource.route.route_id || '',
|
|
223
|
-
routeKind: 'resource'
|
|
243
|
+
routeKind: 'resource',
|
|
244
|
+
globalMiddlewareSource: globalMiddleware?.source || '',
|
|
245
|
+
globalMiddlewareSourcePath: globalMiddleware?.sourcePath || ''
|
|
224
246
|
});
|
|
225
247
|
const descriptor = buildResourceResponseDescriptor(execution?.result, configuredBasePath, Array.isArray(execution?.setCookies) ? execution.setCookies : []);
|
|
226
248
|
res.writeHead(descriptor.status, appendSetCookieHeaders(descriptor.headers, descriptor.setCookies));
|
|
@@ -265,7 +287,19 @@ export function createDevRequestHandler(options) {
|
|
|
265
287
|
}
|
|
266
288
|
let ssrPayload = null;
|
|
267
289
|
let routeExecution = null;
|
|
268
|
-
if (resolved.matched &&
|
|
290
|
+
if (resolved.matched &&
|
|
291
|
+
resolved.route?.prerender !== true &&
|
|
292
|
+
(resolved.route?.server_script || hasRouteScopedServerData(resolved.route))) {
|
|
293
|
+
let globalMiddleware = null;
|
|
294
|
+
try {
|
|
295
|
+
globalMiddleware = loadGlobalMiddlewareForRequests
|
|
296
|
+
? await loadGlobalMiddlewareForRequests()
|
|
297
|
+
: null;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
respondWithMiddlewareSourceError(res, error);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
269
303
|
try {
|
|
270
304
|
const requestMethod = req.method || 'GET';
|
|
271
305
|
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
@@ -281,7 +315,15 @@ export function createDevRequestHandler(options) {
|
|
|
281
315
|
requestBodyBuffer,
|
|
282
316
|
routePattern: resolved.route.path,
|
|
283
317
|
routeFile: resolved.route.server_script_path || '',
|
|
284
|
-
routeId: resolved.route.route_id || ''
|
|
318
|
+
routeId: resolved.route.route_id || '',
|
|
319
|
+
globalMiddlewareSource: globalMiddleware?.source || '',
|
|
320
|
+
globalMiddlewareSourcePath: globalMiddleware?.sourcePath || '',
|
|
321
|
+
scopedServerData: Array.isArray(resolved.route.scoped_server_data)
|
|
322
|
+
? resolved.route.scoped_server_data
|
|
323
|
+
: [],
|
|
324
|
+
scopedServerModuleSources: Array.isArray(resolved.route.scoped_server_modules)
|
|
325
|
+
? resolved.route.scoped_server_modules
|
|
326
|
+
: []
|
|
285
327
|
});
|
|
286
328
|
}
|
|
287
329
|
catch (error) {
|
package/dist/dev-server.js
CHANGED
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
// V0: Uses Node.js http module + fs.watch. No external deps.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
import { createServer } from 'node:http';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
14
15
|
import { readFile } from 'node:fs/promises';
|
|
15
|
-
import { basename, dirname, resolve } from 'node:path';
|
|
16
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
18
|
import { normalizeBasePath } from './base-path.js';
|
|
17
19
|
import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
|
|
18
20
|
import { createDevBuildSession } from './dev-build-session.js';
|
|
21
|
+
import { generateManifest } from './manifest.js';
|
|
22
|
+
import { buildComponentRegistry } from './resolve-components.js';
|
|
19
23
|
import { createStartupProfiler } from './startup-profile.js';
|
|
20
24
|
import { createSilentLogger } from './ui/logger.js';
|
|
21
25
|
import { createTrustedOriginResolver, publicHost } from './request-origin.js';
|
|
@@ -26,6 +30,8 @@ import { buildNotFoundPayload, classifyNotFound, infer404Cause, looksLikeJsonReq
|
|
|
26
30
|
import { createDevRequestHandler } from './dev-server/request-handler.js';
|
|
27
31
|
import { createDevWatcher } from './dev-server/watcher.js';
|
|
28
32
|
import { listenWithPortFallback } from './dev-server/port-fallback.js';
|
|
33
|
+
import { loadDevGlobalMiddlewareSource } from './global-middleware-runtime-source.js';
|
|
34
|
+
const SCOPED_SERVER_DATA_LOWERING_HELPER_UNAVAILABLE = '[Zenith:ScopedServerData] Server-output lowering helper is unavailable. Run the CLI build step before packaging scoped server data modules.';
|
|
29
35
|
const MIME_TYPES = {
|
|
30
36
|
'.html': 'text/html',
|
|
31
37
|
'.js': 'application/javascript',
|
|
@@ -42,6 +48,24 @@ const MIME_TYPES = {
|
|
|
42
48
|
const IMAGE_RUNTIME_TAG_RE = new RegExp('<' + 'script\\b[^>]*\\bid=(["\'])zenith-image-runtime\\1[^>]*>[\\s\\S]*?<\\/' + 'script>', 'i');
|
|
43
49
|
const EVENT_STREAM_MIME = ['text', 'event-stream'].join('/');
|
|
44
50
|
const LEGACY_DEV_STREAM_PATH = ['/__zenith', '_hmr'].join('');
|
|
51
|
+
let scopedServerDataLoweringPromise = null;
|
|
52
|
+
function resolveScopedServerDataLoweringPath() {
|
|
53
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
return [
|
|
55
|
+
join(moduleDir, 'scoped-server-data', 'lowering.js'),
|
|
56
|
+
join(moduleDir, '..', 'dist', 'scoped-server-data', 'lowering.js')
|
|
57
|
+
].find((candidate) => existsSync(candidate)) || null;
|
|
58
|
+
}
|
|
59
|
+
async function getScopedServerDataLowering() {
|
|
60
|
+
const helperPath = resolveScopedServerDataLoweringPath();
|
|
61
|
+
if (!helperPath) {
|
|
62
|
+
throw new Error(SCOPED_SERVER_DATA_LOWERING_HELPER_UNAVAILABLE);
|
|
63
|
+
}
|
|
64
|
+
if (!scopedServerDataLoweringPromise) {
|
|
65
|
+
scopedServerDataLoweringPromise = import(pathToFileURL(helperPath).href);
|
|
66
|
+
}
|
|
67
|
+
return scopedServerDataLoweringPromise;
|
|
68
|
+
}
|
|
45
69
|
function appendSetCookieHeaders(headers, setCookies = []) {
|
|
46
70
|
if (Array.isArray(setCookies) && setCookies.length > 0) {
|
|
47
71
|
headers['Set-Cookie'] = setCookies.slice();
|
|
@@ -65,6 +89,7 @@ export async function createDevServer(options) {
|
|
|
65
89
|
const resolvedTarget = resolveBuildAdapter(config).target;
|
|
66
90
|
const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
|
|
67
91
|
const isStaticExportTarget = resolvedTarget === 'static-export';
|
|
92
|
+
const compilerOpts = { typescriptDefault: config.typescriptDefault === true, experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true, strictDomLints: config.strictDomLints === true };
|
|
68
93
|
const resolvedPagesDir = resolve(pagesDir);
|
|
69
94
|
const resolvedOutDir = resolve(outDir);
|
|
70
95
|
const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
|
|
@@ -178,8 +203,9 @@ export async function createDevServer(options) {
|
|
|
178
203
|
const routeState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
179
204
|
if ((Array.isArray(routeState.pageRoutes) && routeState.pageRoutes.length > 0) ||
|
|
180
205
|
(Array.isArray(routeState.resourceRoutes) && routeState.resourceRoutes.length > 0)) {
|
|
181
|
-
|
|
182
|
-
|
|
206
|
+
const mergedRouteState = await _mergeDevScopedServerData(routeState);
|
|
207
|
+
state.currentRouteState = mergedRouteState;
|
|
208
|
+
return mergedRouteState;
|
|
183
209
|
}
|
|
184
210
|
}
|
|
185
211
|
catch (error) {
|
|
@@ -190,6 +216,67 @@ export async function createDevServer(options) {
|
|
|
190
216
|
}
|
|
191
217
|
return state.currentRouteState;
|
|
192
218
|
}
|
|
219
|
+
async function _mergeDevScopedServerData(routeState) {
|
|
220
|
+
const scopedByPath = await _loadDevScopedServerDataByPath();
|
|
221
|
+
if (scopedByPath.size === 0) {
|
|
222
|
+
return routeState;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
...routeState,
|
|
226
|
+
pageRoutes: (Array.isArray(routeState.pageRoutes) ? routeState.pageRoutes : []).map((route) => {
|
|
227
|
+
const scoped = scopedByPath.get(route.path);
|
|
228
|
+
return scoped ? { ...route, ...scoped } : route;
|
|
229
|
+
})
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function _loadDevScopedServerDataByPath() {
|
|
233
|
+
const srcDir = resolve(resolvedPagesDir, '..');
|
|
234
|
+
const registry = buildComponentRegistry(srcDir);
|
|
235
|
+
const manifest = await generateManifest(resolvedPagesDir, '.zen', {
|
|
236
|
+
srcDir,
|
|
237
|
+
registry,
|
|
238
|
+
compilerOpts
|
|
239
|
+
});
|
|
240
|
+
const pageEntries = manifest.filter((entry) => entry?.route_kind !== 'resource' &&
|
|
241
|
+
entry?.has_scoped_server_data === true &&
|
|
242
|
+
Array.isArray(entry?.scoped_server_data) &&
|
|
243
|
+
entry.scoped_server_data.length > 0);
|
|
244
|
+
const scopedByPath = new Map();
|
|
245
|
+
if (pageEntries.length === 0) {
|
|
246
|
+
return scopedByPath;
|
|
247
|
+
}
|
|
248
|
+
const lowering = await getScopedServerDataLowering();
|
|
249
|
+
for (const entry of pageEntries) {
|
|
250
|
+
const pageFile = resolve(resolvedPagesDir, entry.file);
|
|
251
|
+
const pageSource = await readFile(pageFile, 'utf8');
|
|
252
|
+
const lowered = lowering.lowerRouteScopedServerData({
|
|
253
|
+
pageSource,
|
|
254
|
+
pageFile,
|
|
255
|
+
registry,
|
|
256
|
+
srcDir,
|
|
257
|
+
projectRoot,
|
|
258
|
+
compilerOpts,
|
|
259
|
+
scopedServerData: entry.scoped_server_data
|
|
260
|
+
});
|
|
261
|
+
scopedByPath.set(entry.path, {
|
|
262
|
+
has_scoped_server_data: true,
|
|
263
|
+
scoped_server_data: lowered.scopedServerData,
|
|
264
|
+
scoped_server_modules: lowered.modules.map((module) => ({
|
|
265
|
+
module: module.module,
|
|
266
|
+
source: module.source,
|
|
267
|
+
sourcePath: module.sourcePath
|
|
268
|
+
}))
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return scopedByPath;
|
|
272
|
+
}
|
|
273
|
+
async function _loadGlobalMiddlewareForRequests() {
|
|
274
|
+
return loadDevGlobalMiddlewareSource({
|
|
275
|
+
projectRoot,
|
|
276
|
+
pagesDir: resolvedPagesDir,
|
|
277
|
+
target: resolvedTarget
|
|
278
|
+
});
|
|
279
|
+
}
|
|
193
280
|
function _broadcastEvent(type, payload = {}) {
|
|
194
281
|
const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : state.buildId;
|
|
195
282
|
const data = JSON.stringify({
|
|
@@ -221,7 +308,7 @@ export async function createDevServer(options) {
|
|
|
221
308
|
logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
|
|
222
309
|
const initialBuild = await buildSession.build();
|
|
223
310
|
const cssReady = await _syncCssStateFromBuild(initialBuild, state.buildId);
|
|
224
|
-
state.currentRouteState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
311
|
+
state.currentRouteState = await _mergeDevScopedServerData(await loadRouteSurfaceState(outDir, configuredBasePath));
|
|
225
312
|
state.buildStatus = 'ok';
|
|
226
313
|
state.buildError = null;
|
|
227
314
|
state.lastBuildMs = Date.now();
|
|
@@ -286,6 +373,7 @@ export async function createDevServer(options) {
|
|
|
286
373
|
state,
|
|
287
374
|
serverOrigin: _serverOrigin,
|
|
288
375
|
loadRoutesForRequests: _loadRoutesForRequests,
|
|
376
|
+
loadGlobalMiddlewareForRequests: _loadGlobalMiddlewareForRequests,
|
|
289
377
|
readFileForRequest: _readFileForRequest,
|
|
290
378
|
trace404: _trace404,
|
|
291
379
|
looksLikeJsonRequest,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function loadDevGlobalMiddlewareSource({ projectRoot, pagesDir, target }: {
|
|
2
|
+
projectRoot: any;
|
|
3
|
+
pagesDir: any;
|
|
4
|
+
target: any;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
source: string;
|
|
7
|
+
sourcePath: string;
|
|
8
|
+
} | null>;
|
|
9
|
+
export function loadPreviewGlobalMiddlewareSource({ projectRoot, distDir }: {
|
|
10
|
+
projectRoot: any;
|
|
11
|
+
distDir: any;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
source: string;
|
|
14
|
+
sourcePath: string;
|
|
15
|
+
} | null>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { resolveGlobalMiddleware, validateGlobalMiddlewareSource } from './global-middleware.js';
|
|
4
|
+
const INVALID_PREVIEW_SOURCE_FILE = '[Zenith:Middleware] Invalid global middleware source_file in manifest.';
|
|
5
|
+
function isWithinPath(root, candidate) {
|
|
6
|
+
const relativePath = relative(resolve(root), resolve(candidate));
|
|
7
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
|
|
8
|
+
}
|
|
9
|
+
function assertPreviewSourceFile(projectRoot, sourceFile) {
|
|
10
|
+
if (typeof sourceFile !== 'string' || sourceFile.length === 0 || !sourceFile.endsWith('.ts')) {
|
|
11
|
+
throw new Error(INVALID_PREVIEW_SOURCE_FILE);
|
|
12
|
+
}
|
|
13
|
+
const sourcePath = resolve(projectRoot, sourceFile);
|
|
14
|
+
if (!isWithinPath(projectRoot, sourcePath)) {
|
|
15
|
+
throw new Error(INVALID_PREVIEW_SOURCE_FILE);
|
|
16
|
+
}
|
|
17
|
+
return sourcePath;
|
|
18
|
+
}
|
|
19
|
+
function createReadError(sourceFile) {
|
|
20
|
+
return new Error(`[Zenith:Middleware] Cannot read global middleware source file "${sourceFile}".`);
|
|
21
|
+
}
|
|
22
|
+
async function readMiddlewareSource(sourcePath, sourceFile) {
|
|
23
|
+
try {
|
|
24
|
+
return await readFile(sourcePath, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw createReadError(sourceFile);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function loadDevGlobalMiddlewareSource({ projectRoot, pagesDir, target }) {
|
|
31
|
+
const globalMiddleware = await resolveGlobalMiddleware({ projectRoot, pagesDir, target });
|
|
32
|
+
if (!globalMiddleware) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const source = await readMiddlewareSource(globalMiddleware.sourcePath, globalMiddleware.sourceFile);
|
|
36
|
+
validateGlobalMiddlewareSource(source, globalMiddleware.sourceFile, projectRoot);
|
|
37
|
+
return {
|
|
38
|
+
source,
|
|
39
|
+
sourcePath: globalMiddleware.sourcePath
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function loadPreviewGlobalMiddlewareSource({ projectRoot, distDir }) {
|
|
43
|
+
let manifest = null;
|
|
44
|
+
const manifestDir = basename(resolve(distDir)) === 'static' ? dirname(distDir) : distDir;
|
|
45
|
+
try {
|
|
46
|
+
manifest = JSON.parse(await readFile(join(manifestDir, 'manifest.json'), 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
manifest = null;
|
|
50
|
+
}
|
|
51
|
+
const sourceFile = manifest?.global_middleware?.source_file;
|
|
52
|
+
if (!sourceFile) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const sourcePath = assertPreviewSourceFile(projectRoot, sourceFile);
|
|
56
|
+
const source = await readMiddlewareSource(sourcePath, sourceFile);
|
|
57
|
+
validateGlobalMiddlewareSource(source, sourceFile, projectRoot);
|
|
58
|
+
return {
|
|
59
|
+
source,
|
|
60
|
+
sourcePath
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function validateGlobalMiddlewareSource(source: any, sourceFile: any, projectRoot?: string): void;
|
|
2
|
+
export function normalizeGlobalMiddlewareMetadata(globalMiddleware: any): {
|
|
3
|
+
source_file: any;
|
|
4
|
+
} | null;
|
|
5
|
+
export function assertGlobalMiddlewareTargetSupported(target: any, globalMiddleware: any): void;
|
|
6
|
+
export function resolveGlobalMiddleware({ projectRoot, pagesDir, target }?: {}): Promise<{
|
|
7
|
+
sourcePath: string;
|
|
8
|
+
sourceFile: string;
|
|
9
|
+
root: string;
|
|
10
|
+
metadata: {
|
|
11
|
+
source_file: any;
|
|
12
|
+
};
|
|
13
|
+
} | null>;
|