@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.
- package/README.md +14 -2
- 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/compiler-runtime.js +3 -0
- 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 -4
- package/dist/build/page-loop-state.js +17 -9
- package/dist/build/page-loop.js +18 -8
- package/dist/build/scoped-expression-context.d.ts +5 -0
- package/dist/build/scoped-expression-context.js +133 -0
- package/dist/build/server-script.js +13 -36
- package/dist/build/type-declarations.d.ts +2 -1
- package/dist/build/type-declarations.js +29 -52
- 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-build-session/helpers.js +27 -7
- package/dist/dev-build-session/session.js +19 -10
- package/dist/dev-server/build-error-response.d.ts +21 -0
- package/dist/dev-server/build-error-response.js +48 -0
- package/dist/dev-server/port-fallback.d.ts +15 -0
- package/dist/dev-server/port-fallback.js +61 -0
- package/dist/dev-server/request-handler.js +58 -5
- package/dist/dev-server/watcher.js +15 -0
- package/dist/dev-server.d.ts +5 -2
- package/dist/dev-server.js +129 -49
- 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/images/remote-fetch.d.ts +12 -0
- package/dist/images/remote-fetch.js +257 -0
- package/dist/images/service.d.ts +10 -0
- package/dist/images/service.js +9 -46
- package/dist/index.js +12 -2
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +70 -25
- 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/resource-response.js +25 -8
- package/dist/resource-route-module.js +5 -22
- package/dist/route-classification.d.ts +11 -0
- package/dist/route-classification.js +21 -0
- package/dist/route-handler-export-analysis.d.ts +22 -0
- package/dist/route-handler-export-analysis.js +41 -0
- 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 +144 -195
- package/dist/server-route-names.d.ts +2 -0
- package/dist/server-route-names.js +38 -0
- 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 +26 -3
- package/dist/server-runtime/route-render.d.ts +12 -3
- package/dist/server-runtime/route-render.js +67 -13
- package/dist/types/generate-env-dts.js +2 -44
- package/dist/types/zenith-env-dts.d.ts +4 -0
- package/dist/types/zenith-env-dts.js +96 -0
- 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 =
|
|
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 &&
|
|
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) {
|
package/dist/dev-server.d.ts
CHANGED
|
@@ -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
|
}>;
|
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';
|
|
@@ -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
|
|
97
|
+
const inferredProjectRoot = basename(pagesParentDir) === 'src'
|
|
72
98
|
? dirname(pagesParentDir)
|
|
73
99
|
: pagesParentDir;
|
|
74
|
-
const
|
|
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 =
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
417
|
+
try {
|
|
418
|
+
server.close();
|
|
419
|
+
}
|
|
420
|
+
catch { }
|
|
328
421
|
};
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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>;
|