@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.
Files changed (87) hide show
  1. package/README.md +10 -1
  2. package/dist/adapters/adapter-netlify-static.d.ts +2 -5
  3. package/dist/adapters/adapter-netlify.d.ts +2 -5
  4. package/dist/adapters/adapter-netlify.js +22 -5
  5. package/dist/adapters/adapter-types.d.ts +32 -13
  6. package/dist/adapters/adapter-types.js +0 -59
  7. package/dist/adapters/adapter-vercel-static.d.ts +2 -5
  8. package/dist/adapters/adapter-vercel.d.ts +2 -5
  9. package/dist/adapters/adapter-vercel.js +21 -6
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
  11. package/dist/adapters/copy-hosted-page-runtime.js +68 -3
  12. package/dist/adapters/resolve-adapter.d.ts +6 -4
  13. package/dist/build/expression-rewrites.d.ts +3 -1
  14. package/dist/build/expression-rewrites.js +14 -2
  15. package/dist/build/page-component-loop.d.ts +1 -0
  16. package/dist/build/page-component-loop.js +66 -6
  17. package/dist/build/page-ir-normalization.js +7 -0
  18. package/dist/build/page-loop-state.d.ts +2 -1
  19. package/dist/build/page-loop-state.js +9 -2
  20. package/dist/build/page-loop.js +10 -1
  21. package/dist/build/scoped-expression-context.d.ts +5 -0
  22. package/dist/build/scoped-expression-context.js +133 -0
  23. package/dist/build/type-declarations.d.ts +2 -1
  24. package/dist/build/type-declarations.js +31 -1
  25. package/dist/build-output-manifest.d.ts +10 -6
  26. package/dist/build-output-manifest.js +4 -1
  27. package/dist/build.js +11 -2
  28. package/dist/component-instance-ir.js +1 -0
  29. package/dist/component-occurrences.d.ts +9 -0
  30. package/dist/component-occurrences.js +18 -0
  31. package/dist/config-plugins.d.ts +12 -0
  32. package/dist/config-plugins.js +100 -0
  33. package/dist/config.d.ts +1 -0
  34. package/dist/config.js +56 -5
  35. package/dist/dev-server/request-handler.js +46 -4
  36. package/dist/dev-server.js +92 -4
  37. package/dist/global-middleware-runtime-source.d.ts +15 -0
  38. package/dist/global-middleware-runtime-source.js +62 -0
  39. package/dist/global-middleware.d.ts +13 -0
  40. package/dist/global-middleware.js +252 -0
  41. package/dist/manifest.d.ts +9 -1
  42. package/dist/manifest.js +66 -26
  43. package/dist/preview/request-handler.js +78 -5
  44. package/dist/preview/server-runner.d.ts +7 -2
  45. package/dist/preview/server-runner.js +19 -6
  46. package/dist/preview/server-script-runner-template.js +97 -29
  47. package/dist/route-classification.d.ts +2 -1
  48. package/dist/route-classification.js +6 -2
  49. package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
  50. package/dist/scoped-server-data/analyze-owner-file.js +149 -0
  51. package/dist/scoped-server-data/diagnostics.d.ts +18 -0
  52. package/dist/scoped-server-data/diagnostics.js +32 -0
  53. package/dist/scoped-server-data/lowering.d.ts +27 -0
  54. package/dist/scoped-server-data/lowering.js +242 -0
  55. package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
  56. package/dist/scoped-server-data/manifest-integration.js +125 -0
  57. package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
  58. package/dist/scoped-server-data/owner-scanner.js +55 -0
  59. package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
  60. package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
  61. package/dist/scoped-server-data/runtime.d.ts +24 -0
  62. package/dist/scoped-server-data/runtime.js +121 -0
  63. package/dist/scoped-server-data/serialization-set.d.ts +2 -0
  64. package/dist/scoped-server-data/serialization-set.js +52 -0
  65. package/dist/scoped-server-data/static-props.d.ts +12 -0
  66. package/dist/scoped-server-data/static-props.js +307 -0
  67. package/dist/scoped-server-data/type-declarations.d.ts +10 -0
  68. package/dist/scoped-server-data/type-declarations.js +368 -0
  69. package/dist/scoped-server-data/types.d.ts +74 -0
  70. package/dist/scoped-server-data/types.js +1 -0
  71. package/dist/server-contract/auth-control-flow.d.ts +1 -0
  72. package/dist/server-contract/auth-control-flow.js +10 -0
  73. package/dist/server-contract/resolve.d.ts +19 -0
  74. package/dist/server-contract/resolve.js +85 -13
  75. package/dist/server-contract/resolved-envelope.d.ts +9 -0
  76. package/dist/server-contract/resolved-envelope.js +14 -0
  77. package/dist/server-contract/stage.js +1 -10
  78. package/dist/server-module-output.d.ts +9 -0
  79. package/dist/server-module-output.js +250 -0
  80. package/dist/server-output.d.ts +7 -1
  81. package/dist/server-output.js +138 -179
  82. package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
  83. package/dist/server-runtime/matched-route-pipeline.js +1 -0
  84. package/dist/server-runtime/node-server.js +21 -1
  85. package/dist/server-runtime/route-render.d.ts +12 -3
  86. package/dist/server-runtime/route-render.js +67 -13
  87. 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
@@ -27,4 +27,5 @@ export namespace DEFAULT_CONFIG {
27
27
  minimumCacheTTL: number;
28
28
  dangerouslyAllowLocalNetwork: boolean;
29
29
  };
30
+ let plugins: never[];
30
31
  }
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
- return markLoaded(validateConfig(mod.default || mod));
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 && resolved.route?.server_script && resolved.route.prerender !== true) {
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) {
@@ -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
- state.currentRouteState = routeState;
182
- return routeState;
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>;