@zenithbuild/cli 0.7.10 → 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 (111) hide show
  1. package/README.md +14 -2
  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/compiler-runtime.js +3 -0
  14. package/dist/build/expression-rewrites.d.ts +3 -1
  15. package/dist/build/expression-rewrites.js +14 -2
  16. package/dist/build/page-component-loop.d.ts +1 -0
  17. package/dist/build/page-component-loop.js +66 -6
  18. package/dist/build/page-ir-normalization.js +7 -0
  19. package/dist/build/page-loop-state.d.ts +2 -4
  20. package/dist/build/page-loop-state.js +17 -9
  21. package/dist/build/page-loop.js +18 -8
  22. package/dist/build/scoped-expression-context.d.ts +5 -0
  23. package/dist/build/scoped-expression-context.js +133 -0
  24. package/dist/build/server-script.js +13 -36
  25. package/dist/build/type-declarations.d.ts +2 -1
  26. package/dist/build/type-declarations.js +29 -52
  27. package/dist/build-output-manifest.d.ts +10 -6
  28. package/dist/build-output-manifest.js +4 -1
  29. package/dist/build.js +11 -2
  30. package/dist/component-instance-ir.js +1 -0
  31. package/dist/component-occurrences.d.ts +9 -0
  32. package/dist/component-occurrences.js +18 -0
  33. package/dist/config-plugins.d.ts +12 -0
  34. package/dist/config-plugins.js +100 -0
  35. package/dist/config.d.ts +1 -0
  36. package/dist/config.js +56 -5
  37. package/dist/dev-build-session/helpers.js +27 -7
  38. package/dist/dev-build-session/session.js +19 -10
  39. package/dist/dev-server/build-error-response.d.ts +21 -0
  40. package/dist/dev-server/build-error-response.js +48 -0
  41. package/dist/dev-server/port-fallback.d.ts +15 -0
  42. package/dist/dev-server/port-fallback.js +61 -0
  43. package/dist/dev-server/request-handler.js +58 -5
  44. package/dist/dev-server/watcher.js +15 -0
  45. package/dist/dev-server.d.ts +5 -2
  46. package/dist/dev-server.js +129 -49
  47. package/dist/global-middleware-runtime-source.d.ts +15 -0
  48. package/dist/global-middleware-runtime-source.js +62 -0
  49. package/dist/global-middleware.d.ts +13 -0
  50. package/dist/global-middleware.js +252 -0
  51. package/dist/images/remote-fetch.d.ts +12 -0
  52. package/dist/images/remote-fetch.js +257 -0
  53. package/dist/images/service.d.ts +10 -0
  54. package/dist/images/service.js +9 -46
  55. package/dist/index.js +12 -2
  56. package/dist/manifest.d.ts +9 -1
  57. package/dist/manifest.js +70 -25
  58. package/dist/preview/request-handler.js +78 -5
  59. package/dist/preview/server-runner.d.ts +7 -2
  60. package/dist/preview/server-runner.js +19 -6
  61. package/dist/preview/server-script-runner-template.js +97 -29
  62. package/dist/resource-response.js +25 -8
  63. package/dist/resource-route-module.js +5 -22
  64. package/dist/route-classification.d.ts +11 -0
  65. package/dist/route-classification.js +21 -0
  66. package/dist/route-handler-export-analysis.d.ts +22 -0
  67. package/dist/route-handler-export-analysis.js +41 -0
  68. package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
  69. package/dist/scoped-server-data/analyze-owner-file.js +149 -0
  70. package/dist/scoped-server-data/diagnostics.d.ts +18 -0
  71. package/dist/scoped-server-data/diagnostics.js +32 -0
  72. package/dist/scoped-server-data/lowering.d.ts +27 -0
  73. package/dist/scoped-server-data/lowering.js +242 -0
  74. package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
  75. package/dist/scoped-server-data/manifest-integration.js +125 -0
  76. package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
  77. package/dist/scoped-server-data/owner-scanner.js +55 -0
  78. package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
  79. package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
  80. package/dist/scoped-server-data/runtime.d.ts +24 -0
  81. package/dist/scoped-server-data/runtime.js +121 -0
  82. package/dist/scoped-server-data/serialization-set.d.ts +2 -0
  83. package/dist/scoped-server-data/serialization-set.js +52 -0
  84. package/dist/scoped-server-data/static-props.d.ts +12 -0
  85. package/dist/scoped-server-data/static-props.js +307 -0
  86. package/dist/scoped-server-data/type-declarations.d.ts +10 -0
  87. package/dist/scoped-server-data/type-declarations.js +368 -0
  88. package/dist/scoped-server-data/types.d.ts +74 -0
  89. package/dist/scoped-server-data/types.js +1 -0
  90. package/dist/server-contract/auth-control-flow.d.ts +1 -0
  91. package/dist/server-contract/auth-control-flow.js +10 -0
  92. package/dist/server-contract/resolve.d.ts +19 -0
  93. package/dist/server-contract/resolve.js +85 -13
  94. package/dist/server-contract/resolved-envelope.d.ts +9 -0
  95. package/dist/server-contract/resolved-envelope.js +14 -0
  96. package/dist/server-contract/stage.js +1 -10
  97. package/dist/server-module-output.d.ts +9 -0
  98. package/dist/server-module-output.js +250 -0
  99. package/dist/server-output.d.ts +7 -1
  100. package/dist/server-output.js +144 -195
  101. package/dist/server-route-names.d.ts +2 -0
  102. package/dist/server-route-names.js +38 -0
  103. package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
  104. package/dist/server-runtime/matched-route-pipeline.js +1 -0
  105. package/dist/server-runtime/node-server.js +26 -3
  106. package/dist/server-runtime/route-render.d.ts +12 -3
  107. package/dist/server-runtime/route-render.js +67 -13
  108. package/dist/types/generate-env-dts.js +2 -44
  109. package/dist/types/zenith-env-dts.d.ts +4 -0
  110. package/dist/types/zenith-env-dts.js +96 -0
  111. package/package.json +3 -6
@@ -0,0 +1,61 @@
1
+ const MAX_PORT = 65535;
2
+ const DEFAULT_MAX_ATTEMPTS = 20;
3
+ export function isPortConflict(error) {
4
+ return error && error.code === 'EADDRINUSE';
5
+ }
6
+ function normalizeRequestedPort(port) {
7
+ return Number.isInteger(port) && port >= 0 ? port : 3000;
8
+ }
9
+ function waitForListen(server, port, host) {
10
+ return new Promise((resolve, reject) => {
11
+ const cleanup = () => {
12
+ server.off('error', onError);
13
+ server.off('listening', onListening);
14
+ };
15
+ const onError = (error) => {
16
+ cleanup();
17
+ reject(error);
18
+ };
19
+ const onListening = () => {
20
+ cleanup();
21
+ resolve();
22
+ };
23
+ server.once('error', onError);
24
+ server.once('listening', onListening);
25
+ server.listen(port, host);
26
+ });
27
+ }
28
+ export async function listenWithPortFallback({ server, port, host, maxAttempts = DEFAULT_MAX_ATTEMPTS }) {
29
+ const requestedPort = normalizeRequestedPort(port);
30
+ let candidatePort = requestedPort;
31
+ const occupiedPorts = [];
32
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
33
+ try {
34
+ await waitForListen(server, candidatePort, host);
35
+ const address = server.address();
36
+ const actualPort = address && typeof address === 'object' ? address.port : candidatePort;
37
+ return {
38
+ port: actualPort,
39
+ requestedPort,
40
+ portFallback: occupiedPorts.length > 0
41
+ ? {
42
+ requestedPort,
43
+ occupiedPorts: occupiedPorts.slice(),
44
+ finalPort: actualPort
45
+ }
46
+ : null
47
+ };
48
+ }
49
+ catch (error) {
50
+ if (requestedPort === 0 ||
51
+ !isPortConflict(error) ||
52
+ candidatePort >= MAX_PORT ||
53
+ attempt >= maxAttempts) {
54
+ throw error;
55
+ }
56
+ occupiedPorts.push(candidatePort);
57
+ candidatePort += 1;
58
+ }
59
+ }
60
+ throw new Error(`Unable to bind dev server after ${maxAttempts + 1} attempts`);
61
+ }
@@ -9,9 +9,20 @@ import { materializeImageMarkup } from '../images/materialize.js';
9
9
  import { injectImageRuntimePayload } from '../images/payload.js';
10
10
  import { handleImageRequest } from '../images/service.js';
11
11
  import { resolveRequestRoute } from '../server/resolve-request-route.js';
12
+ import { respondWithDevBuildError } from './build-error-response.js';
12
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
+ }
13
24
  export function createDevRequestHandler(options) {
14
- 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;
15
26
  return async function handleDevRequest(req, res) {
16
27
  const url = new URL(req.url, serverOrigin());
17
28
  const pathname = url.pathname;
@@ -204,6 +215,16 @@ export function createDevRequestHandler(options) {
204
215
  canonicalUrl.pathname = canonicalPath;
205
216
  const resolvedResource = resolveRequestRoute(canonicalUrl, routes.resourceRoutes || []);
206
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
+ }
207
228
  const requestMethod = req.method || 'GET';
208
229
  const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
209
230
  ? null
@@ -219,7 +240,9 @@ export function createDevRequestHandler(options) {
219
240
  routePattern: resolvedResource.route.path,
220
241
  routeFile: resolvedResource.route.server_script_path || '',
221
242
  routeId: resolvedResource.route.route_id || '',
222
- routeKind: 'resource'
243
+ routeKind: 'resource',
244
+ globalMiddlewareSource: globalMiddleware?.source || '',
245
+ globalMiddlewareSourcePath: globalMiddleware?.sourcePath || ''
223
246
  });
224
247
  const descriptor = buildResourceResponseDescriptor(execution?.result, configuredBasePath, Array.isArray(execution?.setCookies) ? execution.setCookies : []);
225
248
  res.writeHead(descriptor.status, appendSetCookieHeaders(descriptor.headers, descriptor.setCookies));
@@ -230,6 +253,16 @@ export function createDevRequestHandler(options) {
230
253
  res.end(descriptor.body);
231
254
  return;
232
255
  }
256
+ if (state.buildStatus === 'error' && (!requestExt || requestExt === '.html')) {
257
+ respondWithDevBuildError({
258
+ req,
259
+ res,
260
+ pathname,
261
+ state,
262
+ looksLikeJsonRequest
263
+ });
264
+ return;
265
+ }
233
266
  const resolved = resolveRequestRoute(canonicalUrl, routes.pageRoutes || []);
234
267
  let filePath = null;
235
268
  if (isStaticExportTarget) {
@@ -245,7 +278,7 @@ export function createDevRequestHandler(options) {
245
278
  filePath = resolveWithinDist(outDir, output);
246
279
  }
247
280
  else {
248
- filePath = toStaticFilePath(outDir, canonicalPath);
281
+ filePath = null;
249
282
  }
250
283
  resolvedPathFor404 = filePath;
251
284
  staticRootFor404 = outDir;
@@ -254,7 +287,19 @@ export function createDevRequestHandler(options) {
254
287
  }
255
288
  let ssrPayload = null;
256
289
  let routeExecution = null;
257
- 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
+ }
258
303
  try {
259
304
  const requestMethod = req.method || 'GET';
260
305
  const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
@@ -270,7 +315,15 @@ export function createDevRequestHandler(options) {
270
315
  requestBodyBuffer,
271
316
  routePattern: resolved.route.path,
272
317
  routeFile: resolved.route.server_script_path || '',
273
- 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
+ : []
274
327
  });
275
328
  }
276
329
  catch (error) {
@@ -3,6 +3,8 @@ import { performance } from 'node:perf_hooks';
3
3
  import { isAbsolute, relative, resolve } from 'node:path';
4
4
  import { readChangeFingerprint } from '../dev-watch.js';
5
5
  import { loadRouteSurfaceState } from '../preview.js';
6
+ const CONFIG_FILE_NAMES = new Set(['zenith.config.js', 'zenith.config.ts']);
7
+ const CONFIG_CHANGED_MESSAGE = 'Config changed. Restart `zenith dev` to apply config updates.';
6
8
  export function createDevWatcher(options) {
7
9
  const { watchRoots, resolvedOutDir, resolvedOutDirTmp, projectRoot, rebuildDebounceMs, queuedRebuildDebounceMs, buildSession, outDir, configuredBasePath, logger, startupProfile, state, syncCssStateFromBuild, broadcastEvent, trace } = options;
8
10
  /** @type {import('fs').FSWatcher[]} */
@@ -42,6 +44,10 @@ export function createDevWatcher(options) {
42
44
  || segments.includes('target')
43
45
  || segments.includes('.turbo');
44
46
  }
47
+ function isConfigFileChange(absPath) {
48
+ const rel = relative(projectRoot, absPath).replace(/\\/g, '/');
49
+ return CONFIG_FILE_NAMES.has(rel);
50
+ }
45
51
  const triggerBuildDrain = (delayMs = rebuildDebounceMs) => {
46
52
  if (buildDebounce !== null) {
47
53
  clearTimeout(buildDebounce);
@@ -166,6 +172,15 @@ export function createDevWatcher(options) {
166
172
  if (shouldIgnoreChange(changedPath)) {
167
173
  return;
168
174
  }
175
+ if (isConfigFileChange(changedPath)) {
176
+ logger.warn(CONFIG_CHANGED_MESSAGE, {
177
+ onceKey: `config-change:${changedPath}`
178
+ });
179
+ trace('config_change_restart_required', {
180
+ path: toDisplayPath(changedPath)
181
+ });
182
+ return;
183
+ }
169
184
  void (async () => {
170
185
  const fingerprint = await readChangeFingerprint(changedPath);
171
186
  if (lastQueuedFingerprints.get(changedPath) === fingerprint) {
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Create and start a development server.
3
3
  *
4
- * @param {{ pagesDir: string, outDir: string, port?: number, host?: string, config?: object, logger?: object | null }} options
5
- * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
4
+ * @param {{ pagesDir: string, outDir: string, projectRoot?: string, port?: number, host?: string, config?: object, logger?: object | null }} options
5
+ * @returns {Promise<{ server: import('http').Server, port: number, requestedPort: number, portFallback: object | null, close: () => void }>}
6
6
  */
7
7
  export function createDevServer(options: {
8
8
  pagesDir: string;
9
9
  outDir: string;
10
+ projectRoot?: string;
10
11
  port?: number;
11
12
  host?: string;
12
13
  config?: object;
@@ -14,5 +15,7 @@ export function createDevServer(options: {
14
15
  }): Promise<{
15
16
  server: import("http").Server;
16
17
  port: number;
18
+ requestedPort: number;
19
+ portFallback: object | null;
17
20
  close: () => void;
18
21
  }>;
@@ -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';
@@ -25,6 +29,9 @@ import { syncCssStateFromBuild } from './dev-server/css-state.js';
25
29
  import { buildNotFoundPayload, classifyNotFound, infer404Cause, looksLikeJsonRequest, renderNotFoundHtml, traceNotFound } from './dev-server/not-found.js';
26
30
  import { createDevRequestHandler } from './dev-server/request-handler.js';
27
31
  import { createDevWatcher } from './dev-server/watcher.js';
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.';
28
35
  const MIME_TYPES = {
29
36
  '.html': 'text/html',
30
37
  '.js': 'application/javascript',
@@ -41,6 +48,24 @@ const MIME_TYPES = {
41
48
  const IMAGE_RUNTIME_TAG_RE = new RegExp('<' + 'script\\b[^>]*\\bid=(["\'])zenith-image-runtime\\1[^>]*>[\\s\\S]*?<\\/' + 'script>', 'i');
42
49
  const EVENT_STREAM_MIME = ['text', 'event-stream'].join('/');
43
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
+ }
44
69
  function appendSetCookieHeaders(headers, setCookies = []) {
45
70
  if (Array.isArray(setCookies) && setCookies.length > 0) {
46
71
  headers['Set-Cookie'] = setCookies.slice();
@@ -52,26 +77,28 @@ function appendSetCookieHeaders(headers, setCookies = []) {
52
77
  /**
53
78
  * Create and start a development server.
54
79
  *
55
- * @param {{ pagesDir: string, outDir: string, port?: number, host?: string, config?: object, logger?: object | null }} options
56
- * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
80
+ * @param {{ pagesDir: string, outDir: string, projectRoot?: string, port?: number, host?: string, config?: object, logger?: object | null }} options
81
+ * @returns {Promise<{ server: import('http').Server, port: number, requestedPort: number, portFallback: object | null, close: () => void }>}
57
82
  */
58
83
  export async function createDevServer(options) {
59
84
  const startupProfile = createStartupProfiler('cli-dev-server');
60
- const { pagesDir, outDir, port = 3000, host = '127.0.0.1', config = {}, logger: providedLogger = null } = options;
85
+ const { pagesDir, outDir, projectRoot: providedProjectRoot = null, port = 3000, host = '127.0.0.1', config = {}, logger: providedLogger = null } = options;
61
86
  const logger = providedLogger || createSilentLogger();
62
87
  const buildSession = createDevBuildSession({ pagesDir, outDir, config, logger });
63
88
  const configuredBasePath = normalizeBasePath(config.basePath || '/');
64
89
  const resolvedTarget = resolveBuildAdapter(config).target;
65
90
  const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
66
91
  const isStaticExportTarget = resolvedTarget === 'static-export';
92
+ const compilerOpts = { typescriptDefault: config.typescriptDefault === true, experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true, strictDomLints: config.strictDomLints === true };
67
93
  const resolvedPagesDir = resolve(pagesDir);
68
94
  const resolvedOutDir = resolve(outDir);
69
95
  const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
70
96
  const pagesParentDir = dirname(resolvedPagesDir);
71
- const projectRoot = basename(pagesParentDir) === 'src'
97
+ const inferredProjectRoot = basename(pagesParentDir) === 'src'
72
98
  ? dirname(pagesParentDir)
73
99
  : pagesParentDir;
74
- const watchRoots = new Set([pagesParentDir]);
100
+ const projectRoot = resolve(providedProjectRoot || inferredProjectRoot);
101
+ const watchRoots = new Set([projectRoot, pagesParentDir]);
75
102
  /** @type {import('http').ServerResponse[]} */
76
103
  const hmrClients = [];
77
104
  const sseHeartbeat = setInterval(() => {
@@ -97,7 +124,7 @@ export async function createDevServer(options) {
97
124
  currentCssContent: '',
98
125
  currentRouteState: { pageRoutes: [], resourceRoutes: [] }
99
126
  };
100
- const traceEnabled = config.devTrace === true || process.env.ZENITH_DEV_TRACE === '1';
127
+ const traceEnabled = process.env.ZENITH_DEV_TRACE === '1';
101
128
  const verboseLogging = traceEnabled || logger.mode?.logLevel === 'verbose';
102
129
  let actualPort = port;
103
130
  const resolveServerOrigin = createTrustedOriginResolver({
@@ -176,8 +203,9 @@ export async function createDevServer(options) {
176
203
  const routeState = await loadRouteSurfaceState(outDir, configuredBasePath);
177
204
  if ((Array.isArray(routeState.pageRoutes) && routeState.pageRoutes.length > 0) ||
178
205
  (Array.isArray(routeState.resourceRoutes) && routeState.resourceRoutes.length > 0)) {
179
- state.currentRouteState = routeState;
180
- return routeState;
206
+ const mergedRouteState = await _mergeDevScopedServerData(routeState);
207
+ state.currentRouteState = mergedRouteState;
208
+ return mergedRouteState;
181
209
  }
182
210
  }
183
211
  catch (error) {
@@ -188,6 +216,67 @@ export async function createDevServer(options) {
188
216
  }
189
217
  return state.currentRouteState;
190
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
+ }
191
280
  function _broadcastEvent(type, payload = {}) {
192
281
  const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : state.buildId;
193
282
  const data = JSON.stringify({
@@ -219,7 +308,7 @@ export async function createDevServer(options) {
219
308
  logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
220
309
  const initialBuild = await buildSession.build();
221
310
  const cssReady = await _syncCssStateFromBuild(initialBuild, state.buildId);
222
- state.currentRouteState = await loadRouteSurfaceState(outDir, configuredBasePath);
311
+ state.currentRouteState = await _mergeDevScopedServerData(await loadRouteSurfaceState(outDir, configuredBasePath));
223
312
  state.buildStatus = 'ok';
224
313
  state.buildError = null;
225
314
  state.lastBuildMs = Date.now();
@@ -284,6 +373,7 @@ export async function createDevServer(options) {
284
373
  state,
285
374
  serverOrigin: _serverOrigin,
286
375
  loadRoutesForRequests: _loadRoutesForRequests,
376
+ loadGlobalMiddlewareForRequests: _loadGlobalMiddlewareForRequests,
287
377
  readFileForRequest: _readFileForRequest,
288
378
  trace404: _trace404,
289
379
  looksLikeJsonRequest,
@@ -324,46 +414,36 @@ export async function createDevServer(options) {
324
414
  catch { }
325
415
  }
326
416
  hmrClients.length = 0;
327
- server.close();
417
+ try {
418
+ server.close();
419
+ }
420
+ catch { }
328
421
  };
329
- return new Promise((resolve, reject) => {
330
- let settled = false;
331
- server.once('error', (error) => {
332
- if (!settled) {
333
- settled = true;
334
- reject(error);
335
- }
422
+ try {
423
+ const listenResult = await listenWithPortFallback({ server, port, host });
424
+ actualPort = listenResult.port;
425
+ startupProfile.emit('server_bound', {
426
+ host: _publicHost(),
427
+ port: actualPort,
428
+ buildStatus: state.buildStatus
336
429
  });
337
- server.listen(port, host, async () => {
338
- actualPort = server.address().port;
339
- startupProfile.emit('server_bound', {
340
- host: _publicHost(),
341
- port: actualPort,
342
- buildStatus: state.buildStatus
343
- });
344
- _trace('server_bound', {
345
- host: _publicHost(),
346
- port: actualPort,
347
- buildStatus: state.buildStatus
348
- });
349
- try {
350
- await _runInitialBuild();
351
- watcherController.start();
352
- if (!settled) {
353
- settled = true;
354
- resolve({
355
- server,
356
- port: actualPort,
357
- close: closeServer
358
- });
359
- }
360
- }
361
- catch (error) {
362
- if (!settled) {
363
- settled = true;
364
- reject(error);
365
- }
366
- }
430
+ _trace('server_bound', {
431
+ host: _publicHost(),
432
+ port: actualPort,
433
+ buildStatus: state.buildStatus
367
434
  });
368
- });
435
+ await _runInitialBuild();
436
+ watcherController.start();
437
+ return {
438
+ server,
439
+ port: actualPort,
440
+ requestedPort: listenResult.requestedPort,
441
+ portFallback: listenResult.portFallback,
442
+ close: closeServer
443
+ };
444
+ }
445
+ catch (error) {
446
+ closeServer();
447
+ throw error;
448
+ }
369
449
  }
@@ -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>;