@zenithbuild/cli 0.7.1 → 0.7.3
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 +59 -1
- package/dist/adapters/adapter-netlify-static.d.ts +5 -0
- package/dist/adapters/adapter-netlify-static.js +39 -0
- package/dist/adapters/adapter-netlify.d.ts +5 -0
- package/dist/adapters/adapter-netlify.js +129 -0
- package/dist/adapters/adapter-node.d.ts +5 -0
- package/dist/adapters/adapter-node.js +121 -0
- package/dist/adapters/adapter-static.d.ts +5 -0
- package/dist/adapters/adapter-static.js +20 -0
- package/dist/adapters/adapter-types.d.ts +44 -0
- package/dist/adapters/adapter-types.js +65 -0
- package/dist/adapters/adapter-vercel-static.d.ts +5 -0
- package/dist/adapters/adapter-vercel-static.js +36 -0
- package/dist/adapters/adapter-vercel.d.ts +5 -0
- package/dist/adapters/adapter-vercel.js +99 -0
- package/dist/adapters/resolve-adapter.d.ts +5 -0
- package/dist/adapters/resolve-adapter.js +84 -0
- package/dist/adapters/route-rules.d.ts +7 -0
- package/dist/adapters/route-rules.js +88 -0
- package/dist/base-path-html.d.ts +2 -0
- package/dist/base-path-html.js +42 -0
- package/dist/base-path.d.ts +8 -0
- package/dist/base-path.js +74 -0
- package/dist/build/compiler-runtime.d.ts +2 -1
- package/dist/build/compiler-runtime.js +4 -1
- package/dist/build/page-loop.d.ts +2 -2
- package/dist/build/page-loop.js +3 -3
- package/dist/build-output-manifest.d.ts +28 -0
- package/dist/build-output-manifest.js +100 -0
- package/dist/build.js +42 -11
- package/dist/config.d.ts +10 -46
- package/dist/config.js +162 -28
- package/dist/dev-build-session.d.ts +1 -0
- package/dist/dev-build-session.js +4 -5
- package/dist/framework-components/Image.zen +31 -9
- package/dist/images/payload.d.ts +2 -1
- package/dist/images/payload.js +3 -2
- package/dist/images/runtime.js +6 -5
- package/dist/images/service.js +2 -2
- package/dist/images/shared.d.ts +4 -2
- package/dist/images/shared.js +8 -3
- package/dist/index.js +36 -15
- package/dist/manifest.d.ts +14 -2
- package/dist/manifest.js +49 -6
- package/dist/preview.js +61 -25
- package/dist/server-output.d.ts +26 -0
- package/dist/server-output.js +297 -0
- package/dist/server-runtime/node-server.d.ts +2 -0
- package/dist/server-runtime/node-server.js +354 -0
- package/dist/server-runtime/route-render.d.ts +64 -0
- package/dist/server-runtime/route-render.js +273 -0
- package/package.json +4 -2
package/dist/images/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { imageEndpointPath, normalizeBasePath, prependBasePath } from '../base-path.js';
|
|
1
2
|
const DEFAULT_DEVICE_SIZES = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
2
3
|
const DEFAULT_IMAGE_SIZES = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
3
4
|
const DEFAULT_FORMATS = ['webp', 'avif'];
|
|
@@ -253,11 +254,14 @@ export function buildLocalImageKey(publicPath) {
|
|
|
253
254
|
}
|
|
254
255
|
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
255
256
|
}
|
|
256
|
-
export function
|
|
257
|
+
export function buildLocalVariantAssetPath(publicPath, width, quality, format) {
|
|
257
258
|
const key = buildLocalImageKey(publicPath);
|
|
258
259
|
return `/_zenith/image/local/${key}/w${width}-q${quality}.${normalizeImageFormat(format)}`;
|
|
259
260
|
}
|
|
260
|
-
export function
|
|
261
|
+
export function buildLocalVariantPath(publicPath, width, quality, format, basePath = '/') {
|
|
262
|
+
return prependBasePath(basePath, buildLocalVariantAssetPath(publicPath, width, quality, format));
|
|
263
|
+
}
|
|
264
|
+
export function buildRemoteVariantPath(remoteUrl, width, quality, format, basePath = '/') {
|
|
261
265
|
const query = new URLSearchParams();
|
|
262
266
|
query.set('url', String(remoteUrl || ''));
|
|
263
267
|
query.set('w', String(width));
|
|
@@ -265,7 +269,7 @@ export function buildRemoteVariantPath(remoteUrl, width, quality, format) {
|
|
|
265
269
|
if (format) {
|
|
266
270
|
query.set('f', normalizeImageFormat(format));
|
|
267
271
|
}
|
|
268
|
-
return
|
|
272
|
+
return `${imageEndpointPath(basePath)}?${query.toString()}`;
|
|
269
273
|
}
|
|
270
274
|
export function resolveWidthCandidates(width, sizes, config, manifestEntry) {
|
|
271
275
|
const base = new Set([
|
|
@@ -300,6 +304,7 @@ export function normalizeImageRuntimePayload(payload) {
|
|
|
300
304
|
}
|
|
301
305
|
return {
|
|
302
306
|
mode: payload.mode === 'endpoint' ? 'endpoint' : 'passthrough',
|
|
307
|
+
basePath: normalizeBasePath(payload.basePath || '/'),
|
|
303
308
|
config: normalizeImageConfig(payload.config || {}),
|
|
304
309
|
localImages: isPlainObject(payload.localImages) ? payload.localImages : {}
|
|
305
310
|
};
|
package/dist/index.js
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Commands:
|
|
6
6
|
// zenith dev — Development server + HMR
|
|
7
|
-
// zenith build — Static site generation to
|
|
8
|
-
// zenith preview — Serve
|
|
7
|
+
// zenith build — Static site generation to the configured output dir
|
|
8
|
+
// zenith preview — Serve the configured output dir statically
|
|
9
9
|
//
|
|
10
10
|
// Minimal arg parsing. No heavy dependencies.
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
|
-
import { resolve, join, dirname } from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { resolve, join, dirname, relative } from 'node:path';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
15
|
import { createZenithLogger } from './ui/logger.js';
|
|
16
|
-
import { loadConfig } from './config.js';
|
|
16
|
+
import { loadConfig, resolveConfigOutDir, resolveConfigPagesDir } from './config.js';
|
|
17
17
|
const COMMANDS = ['dev', 'build', 'preview'];
|
|
18
18
|
const DEFAULT_VERSION = '0.0.0';
|
|
19
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -32,14 +32,24 @@ function printUsage(logger) {
|
|
|
32
32
|
logger.heading('V0');
|
|
33
33
|
logger.print('Usage:');
|
|
34
34
|
logger.print(' zenith dev [port|--port <port>] Start development server');
|
|
35
|
-
logger.print(' zenith build Build static site to
|
|
36
|
-
logger.print(' zenith preview [port|--port <port>] Preview
|
|
35
|
+
logger.print(' zenith build Build static site to the configured output dir');
|
|
36
|
+
logger.print(' zenith preview [port|--port <port>] Preview the configured output dir statically');
|
|
37
37
|
logger.print('');
|
|
38
38
|
logger.print('Options:');
|
|
39
39
|
logger.print(' -h, --help Show this help message');
|
|
40
40
|
logger.print(' -v, --version Print Zenith CLI version');
|
|
41
41
|
logger.print('');
|
|
42
42
|
}
|
|
43
|
+
function formatOutputDir(projectRoot, outDir) {
|
|
44
|
+
const relativePath = relative(projectRoot, outDir);
|
|
45
|
+
if (!relativePath || relativePath.length === 0) {
|
|
46
|
+
return '.';
|
|
47
|
+
}
|
|
48
|
+
if (relativePath.startsWith('..')) {
|
|
49
|
+
return outDir;
|
|
50
|
+
}
|
|
51
|
+
return `./${relativePath}`;
|
|
52
|
+
}
|
|
43
53
|
function resolvePort(args, fallback) {
|
|
44
54
|
if (!Array.isArray(args) || args.length === 0) {
|
|
45
55
|
return fallback;
|
|
@@ -83,11 +93,9 @@ export async function cli(args, cwd) {
|
|
|
83
93
|
process.exit(command ? 1 : 0);
|
|
84
94
|
}
|
|
85
95
|
const projectRoot = resolve(cwd || process.cwd());
|
|
86
|
-
const rootPagesDir = join(projectRoot, 'pages');
|
|
87
|
-
const srcPagesDir = join(projectRoot, 'src', 'pages');
|
|
88
|
-
const pagesDir = existsSync(rootPagesDir) ? rootPagesDir : srcPagesDir;
|
|
89
|
-
const outDir = join(projectRoot, 'dist');
|
|
90
96
|
const config = await loadConfig(projectRoot);
|
|
97
|
+
const pagesDir = resolveConfigPagesDir(projectRoot, config);
|
|
98
|
+
const outDir = resolveConfigOutDir(projectRoot, config);
|
|
91
99
|
if (command === 'build' || command === 'dev') {
|
|
92
100
|
const { maybeWarnAboutZenithVersionMismatch } = await import('./version-check.js');
|
|
93
101
|
await maybeWarnAboutZenithVersionMismatch({
|
|
@@ -101,7 +109,7 @@ export async function cli(args, cwd) {
|
|
|
101
109
|
logger.build('Building…');
|
|
102
110
|
const result = await build({ pagesDir, outDir, config, logger, showBundlerInfo: false });
|
|
103
111
|
logger.ok(`Built ${result.pages} page(s), ${result.assets.length} asset(s)`);
|
|
104
|
-
logger.summary([{ label: 'Output', value:
|
|
112
|
+
logger.summary([{ label: 'Output', value: formatOutputDir(projectRoot, outDir) }], 'BUILD');
|
|
105
113
|
}
|
|
106
114
|
if (command === 'dev') {
|
|
107
115
|
const { createDevServer } = await import('./dev-server.js');
|
|
@@ -123,11 +131,24 @@ export async function cli(args, cwd) {
|
|
|
123
131
|
});
|
|
124
132
|
}
|
|
125
133
|
if (command === 'preview') {
|
|
126
|
-
const { createPreviewServer } = await import('./preview.js');
|
|
127
134
|
const port = resolvePort(args.slice(1), 4000);
|
|
128
135
|
const host = process.env.ZENITH_PREVIEW_HOST || '127.0.0.1';
|
|
129
136
|
logger.dev('Starting preview server…');
|
|
130
|
-
const preview =
|
|
137
|
+
const preview = config.target === 'node'
|
|
138
|
+
? await import(pathToFileURL(join(outDir, 'index.js')).href).then(async (mod) => {
|
|
139
|
+
if (typeof mod.createNodeServer !== 'function') {
|
|
140
|
+
throw new Error('[Zenith:Preview] Node target output is missing createNodeServer()');
|
|
141
|
+
}
|
|
142
|
+
return mod.createNodeServer({ distDir: outDir, port, host });
|
|
143
|
+
})
|
|
144
|
+
: await import('./preview.js').then((mod) => mod.createPreviewServer({
|
|
145
|
+
distDir: outDir,
|
|
146
|
+
port,
|
|
147
|
+
host,
|
|
148
|
+
logger,
|
|
149
|
+
config,
|
|
150
|
+
projectRoot
|
|
151
|
+
}));
|
|
131
152
|
logger.ok(`http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${preview.port}`);
|
|
132
153
|
process.on('SIGINT', () => {
|
|
133
154
|
preview.close();
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {{
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* path: string,
|
|
4
|
+
* file: string,
|
|
5
|
+
* path_kind: 'static' | 'dynamic',
|
|
6
|
+
* render_mode: 'prerender' | 'server',
|
|
7
|
+
* params: string[]
|
|
8
|
+
* }} ManifestEntry
|
|
3
9
|
*/
|
|
4
10
|
/**
|
|
5
11
|
* Scan a pages directory and produce a deterministic RouteManifest.
|
|
6
12
|
*
|
|
7
13
|
* @param {string} pagesDir - Absolute path to /pages directory
|
|
8
14
|
* @param {string} [extension='.zen'] - File extension to scan for
|
|
15
|
+
* @param {{ compilerOpts?: object }} [options]
|
|
9
16
|
* @returns {Promise<ManifestEntry[]>}
|
|
10
17
|
*/
|
|
11
|
-
export function generateManifest(pagesDir: string, extension?: string
|
|
18
|
+
export function generateManifest(pagesDir: string, extension?: string, options?: {
|
|
19
|
+
compilerOpts?: object;
|
|
20
|
+
}): Promise<ManifestEntry[]>;
|
|
12
21
|
/**
|
|
13
22
|
* Generate a JavaScript module string from manifest entries.
|
|
14
23
|
* Used for writing the manifest file to disk.
|
|
@@ -20,4 +29,7 @@ export function serializeManifest(entries: ManifestEntry[]): string;
|
|
|
20
29
|
export type ManifestEntry = {
|
|
21
30
|
path: string;
|
|
22
31
|
file: string;
|
|
32
|
+
path_kind: "static" | "dynamic";
|
|
33
|
+
render_mode: "prerender" | "server";
|
|
34
|
+
params: string[];
|
|
23
35
|
};
|
package/dist/manifest.js
CHANGED
|
@@ -14,20 +14,30 @@
|
|
|
14
14
|
// - Deterministic precedence: static > :param > *catchall
|
|
15
15
|
// - Tie-breaker: lexicographic route path
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
17
18
|
import { readdir, stat } from 'node:fs/promises';
|
|
18
19
|
import { join, relative, sep, basename, extname, dirname } from 'node:path';
|
|
20
|
+
import { extractServerScript } from './build/server-script.js';
|
|
21
|
+
import { composeServerScriptEnvelope, resolveAdjacentServerModules } from './server-script-composition.js';
|
|
19
22
|
/**
|
|
20
|
-
* @typedef {{
|
|
23
|
+
* @typedef {{
|
|
24
|
+
* path: string,
|
|
25
|
+
* file: string,
|
|
26
|
+
* path_kind: 'static' | 'dynamic',
|
|
27
|
+
* render_mode: 'prerender' | 'server',
|
|
28
|
+
* params: string[]
|
|
29
|
+
* }} ManifestEntry
|
|
21
30
|
*/
|
|
22
31
|
/**
|
|
23
32
|
* Scan a pages directory and produce a deterministic RouteManifest.
|
|
24
33
|
*
|
|
25
34
|
* @param {string} pagesDir - Absolute path to /pages directory
|
|
26
35
|
* @param {string} [extension='.zen'] - File extension to scan for
|
|
36
|
+
* @param {{ compilerOpts?: object }} [options]
|
|
27
37
|
* @returns {Promise<ManifestEntry[]>}
|
|
28
38
|
*/
|
|
29
|
-
export async function generateManifest(pagesDir, extension = '.zen') {
|
|
30
|
-
const entries = await _scanDir(pagesDir, pagesDir, extension);
|
|
39
|
+
export async function generateManifest(pagesDir, extension = '.zen', options = {}) {
|
|
40
|
+
const entries = await _scanDir(pagesDir, pagesDir, extension, options.compilerOpts || {});
|
|
31
41
|
// Validate: no repeated param names in any single route
|
|
32
42
|
for (const entry of entries) {
|
|
33
43
|
_validateParams(entry.path);
|
|
@@ -44,7 +54,7 @@ export async function generateManifest(pagesDir, extension = '.zen') {
|
|
|
44
54
|
* @param {string} ext - Extension to match
|
|
45
55
|
* @returns {Promise<ManifestEntry[]>}
|
|
46
56
|
*/
|
|
47
|
-
async function _scanDir(dir, root, ext) {
|
|
57
|
+
async function _scanDir(dir, root, ext, compilerOpts) {
|
|
48
58
|
/** @type {ManifestEntry[]} */
|
|
49
59
|
const entries = [];
|
|
50
60
|
let items;
|
|
@@ -60,16 +70,49 @@ async function _scanDir(dir, root, ext) {
|
|
|
60
70
|
const fullPath = join(dir, item);
|
|
61
71
|
const info = await stat(fullPath);
|
|
62
72
|
if (info.isDirectory()) {
|
|
63
|
-
const nested = await _scanDir(fullPath, root, ext);
|
|
73
|
+
const nested = await _scanDir(fullPath, root, ext, compilerOpts);
|
|
64
74
|
entries.push(...nested);
|
|
65
75
|
}
|
|
66
76
|
else if (item.endsWith(ext)) {
|
|
67
77
|
const routePath = _fileToRoute(fullPath, root, ext);
|
|
68
|
-
entries.push({
|
|
78
|
+
entries.push(buildManifestEntry({
|
|
79
|
+
fullPath,
|
|
80
|
+
root,
|
|
81
|
+
routePath,
|
|
82
|
+
compilerOpts
|
|
83
|
+
}));
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
return entries;
|
|
72
87
|
}
|
|
88
|
+
function buildManifestEntry({ fullPath, root, routePath, compilerOpts }) {
|
|
89
|
+
const rawSource = readFileSync(fullPath, 'utf8');
|
|
90
|
+
const inlineServerScript = extractServerScript(rawSource, fullPath, compilerOpts).serverScript;
|
|
91
|
+
const { guardPath, loadPath } = resolveAdjacentServerModules(fullPath);
|
|
92
|
+
const composed = composeServerScriptEnvelope({
|
|
93
|
+
sourceFile: fullPath,
|
|
94
|
+
inlineServerScript,
|
|
95
|
+
adjacentGuardPath: guardPath,
|
|
96
|
+
adjacentLoadPath: loadPath
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
path: routePath,
|
|
100
|
+
file: relative(root, fullPath),
|
|
101
|
+
path_kind: _isDynamic(routePath) ? 'dynamic' : 'static',
|
|
102
|
+
render_mode: composed.serverScript && composed.serverScript.prerender !== true ? 'server' : 'prerender',
|
|
103
|
+
params: extractRouteParams(routePath)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function extractRouteParams(routePath) {
|
|
107
|
+
return routePath
|
|
108
|
+
.split('/')
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.filter((segment) => segment.startsWith(':') || segment.startsWith('*'))
|
|
111
|
+
.map((segment) => {
|
|
112
|
+
const raw = segment.slice(1);
|
|
113
|
+
return raw.endsWith('?') ? raw.slice(0, -1) : raw;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
73
116
|
/**
|
|
74
117
|
* Convert a file path to a route path.
|
|
75
118
|
*
|
package/dist/preview.js
CHANGED
|
@@ -13,7 +13,8 @@ import { createServer } from 'node:http';
|
|
|
13
13
|
import { access, readFile } from 'node:fs/promises';
|
|
14
14
|
import { extname, join, normalize, resolve, sep, dirname } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import {
|
|
16
|
+
import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from './base-path.js';
|
|
17
|
+
import { isConfigKeyExplicit, loadConfig, validateConfig } from './config.js';
|
|
17
18
|
import { materializeImageMarkup } from './images/materialize.js';
|
|
18
19
|
import { createImageRuntimePayload, injectImageRuntimePayload } from './images/payload.js';
|
|
19
20
|
import { handleImageRequest } from './images/service.js';
|
|
@@ -391,14 +392,25 @@ try {
|
|
|
391
392
|
*/
|
|
392
393
|
export async function createPreviewServer(options) {
|
|
393
394
|
const resolvedProjectRoot = options?.projectRoot ? resolve(options.projectRoot) : resolve(options.distDir, '..');
|
|
395
|
+
const loadedConfig = await loadConfig(resolvedProjectRoot);
|
|
394
396
|
const resolvedConfig = options?.config && typeof options.config === 'object'
|
|
395
|
-
?
|
|
396
|
-
|
|
397
|
+
? (() => {
|
|
398
|
+
const overrideConfig = validateConfig(options.config);
|
|
399
|
+
const mergedConfig = { ...loadedConfig };
|
|
400
|
+
for (const key of Object.keys(overrideConfig)) {
|
|
401
|
+
if (isConfigKeyExplicit(overrideConfig, key)) {
|
|
402
|
+
mergedConfig[key] = overrideConfig[key];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return mergedConfig;
|
|
406
|
+
})()
|
|
407
|
+
: loadedConfig;
|
|
397
408
|
const { distDir, port = 4000, host = '127.0.0.1', logger: providedLogger = null } = options;
|
|
398
409
|
const projectRoot = resolvedProjectRoot;
|
|
399
410
|
const config = resolvedConfig;
|
|
400
411
|
const logger = providedLogger || createSilentLogger();
|
|
401
412
|
const verboseLogging = logger.mode?.logLevel === 'verbose';
|
|
413
|
+
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
402
414
|
let actualPort = port;
|
|
403
415
|
async function loadImageManifest() {
|
|
404
416
|
try {
|
|
@@ -424,8 +436,10 @@ export async function createPreviewServer(options) {
|
|
|
424
436
|
? `http://${req.headers.host}`
|
|
425
437
|
: serverOrigin();
|
|
426
438
|
const url = new URL(req.url, requestBase);
|
|
439
|
+
const { basePath, routes } = await loadRouteManifestState(distDir, configuredBasePath);
|
|
440
|
+
const canonicalPath = stripBasePath(url.pathname, basePath);
|
|
427
441
|
try {
|
|
428
|
-
if (url.pathname ===
|
|
442
|
+
if (url.pathname === routeCheckPath(basePath)) {
|
|
429
443
|
// Security: Require explicitly designated header to prevent public oracle probing
|
|
430
444
|
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
431
445
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
@@ -445,8 +459,15 @@ export async function createPreviewServer(options) {
|
|
|
445
459
|
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
446
460
|
return;
|
|
447
461
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
462
|
+
const canonicalTargetPath = stripBasePath(targetUrl.pathname, basePath);
|
|
463
|
+
if (canonicalTargetPath === null) {
|
|
464
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
465
|
+
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
469
|
+
canonicalTargetUrl.pathname = canonicalTargetPath;
|
|
470
|
+
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes);
|
|
450
471
|
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
451
472
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
452
473
|
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
@@ -466,16 +487,17 @@ export async function createPreviewServer(options) {
|
|
|
466
487
|
});
|
|
467
488
|
// Security: Enforce relative or same-origin redirects
|
|
468
489
|
if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
|
|
469
|
-
const loc =
|
|
490
|
+
const loc = appLocalRedirectLocation(checkResult.result.location || '/', basePath);
|
|
491
|
+
checkResult.result.location = loc;
|
|
470
492
|
if (loc.includes('://') || loc.startsWith('//')) {
|
|
471
493
|
try {
|
|
472
494
|
const parsedLoc = new URL(loc);
|
|
473
495
|
if (parsedLoc.origin !== targetUrl.origin) {
|
|
474
|
-
checkResult.result.location = '/';
|
|
496
|
+
checkResult.result.location = appLocalRedirectLocation('/', basePath);
|
|
475
497
|
}
|
|
476
498
|
}
|
|
477
499
|
catch {
|
|
478
|
-
checkResult.result.location = '/';
|
|
500
|
+
checkResult.result.location = appLocalRedirectLocation('/', basePath);
|
|
479
501
|
}
|
|
480
502
|
}
|
|
481
503
|
}
|
|
@@ -493,7 +515,7 @@ export async function createPreviewServer(options) {
|
|
|
493
515
|
}));
|
|
494
516
|
return;
|
|
495
517
|
}
|
|
496
|
-
if (url.pathname ===
|
|
518
|
+
if (url.pathname === imageEndpointPath(basePath)) {
|
|
497
519
|
await handleImageRequest(req, res, {
|
|
498
520
|
requestUrl: url,
|
|
499
521
|
projectRoot,
|
|
@@ -501,8 +523,11 @@ export async function createPreviewServer(options) {
|
|
|
501
523
|
});
|
|
502
524
|
return;
|
|
503
525
|
}
|
|
504
|
-
if (
|
|
505
|
-
|
|
526
|
+
if (canonicalPath === null) {
|
|
527
|
+
throw new Error('not found');
|
|
528
|
+
}
|
|
529
|
+
if (extname(canonicalPath) && extname(canonicalPath) !== '.html') {
|
|
530
|
+
const staticPath = resolveWithinDist(distDir, canonicalPath);
|
|
506
531
|
if (!staticPath || !(await fileExists(staticPath))) {
|
|
507
532
|
throw new Error('not found');
|
|
508
533
|
}
|
|
@@ -512,8 +537,9 @@ export async function createPreviewServer(options) {
|
|
|
512
537
|
res.end(content);
|
|
513
538
|
return;
|
|
514
539
|
}
|
|
515
|
-
const
|
|
516
|
-
|
|
540
|
+
const canonicalUrl = new URL(url.toString());
|
|
541
|
+
canonicalUrl.pathname = canonicalPath;
|
|
542
|
+
const resolved = resolveRequestRoute(canonicalUrl, routes);
|
|
517
543
|
let htmlPath = null;
|
|
518
544
|
if (resolved.matched && resolved.route) {
|
|
519
545
|
if (verboseLogging) {
|
|
@@ -563,7 +589,7 @@ export async function createPreviewServer(options) {
|
|
|
563
589
|
if (result && result.kind === 'redirect') {
|
|
564
590
|
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
565
591
|
res.writeHead(status, {
|
|
566
|
-
Location: result.location,
|
|
592
|
+
Location: appLocalRedirectLocation(result.location, basePath),
|
|
567
593
|
'Cache-Control': 'no-store'
|
|
568
594
|
});
|
|
569
595
|
res.end('');
|
|
@@ -585,15 +611,15 @@ export async function createPreviewServer(options) {
|
|
|
585
611
|
html = await materializeImageMarkup({
|
|
586
612
|
html,
|
|
587
613
|
pageAssetPath,
|
|
588
|
-
payload: createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint'),
|
|
614
|
+
payload: createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath),
|
|
589
615
|
ssrData: ssrPayload,
|
|
590
|
-
routePathname:
|
|
616
|
+
routePathname: url.pathname
|
|
591
617
|
});
|
|
592
618
|
}
|
|
593
619
|
if (ssrPayload) {
|
|
594
620
|
html = injectSsrPayload(html, ssrPayload);
|
|
595
621
|
}
|
|
596
|
-
html = injectImageRuntimePayload(html, createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint'));
|
|
622
|
+
html = injectImageRuntimePayload(html, createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath));
|
|
597
623
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
598
624
|
res.end(html);
|
|
599
625
|
}
|
|
@@ -636,20 +662,30 @@ export async function createPreviewServer(options) {
|
|
|
636
662
|
* @returns {Promise<PreviewRoute[]>}
|
|
637
663
|
*/
|
|
638
664
|
export async function loadRouteManifest(distDir) {
|
|
665
|
+
const state = await loadRouteManifestState(distDir, '/');
|
|
666
|
+
return state.routes;
|
|
667
|
+
}
|
|
668
|
+
async function loadRouteManifestState(distDir, fallbackBasePath = '/') {
|
|
639
669
|
const manifestPath = join(distDir, 'assets', 'router-manifest.json');
|
|
640
670
|
try {
|
|
641
671
|
const source = await readFile(manifestPath, 'utf8');
|
|
642
672
|
const parsed = JSON.parse(source);
|
|
643
673
|
const routes = Array.isArray(parsed?.routes) ? parsed.routes : [];
|
|
644
|
-
return
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
674
|
+
return {
|
|
675
|
+
basePath: normalizeBasePath(parsed?.base_path || fallbackBasePath || '/'),
|
|
676
|
+
routes: routes
|
|
677
|
+
.filter((entry) => entry &&
|
|
678
|
+
typeof entry === 'object' &&
|
|
679
|
+
typeof entry.path === 'string' &&
|
|
680
|
+
typeof entry.output === 'string')
|
|
681
|
+
.sort((a, b) => compareRouteSpecificity(a.path, b.path))
|
|
682
|
+
};
|
|
650
683
|
}
|
|
651
684
|
catch {
|
|
652
|
-
return
|
|
685
|
+
return {
|
|
686
|
+
basePath: normalizeBasePath(fallbackBasePath || '/'),
|
|
687
|
+
routes: []
|
|
688
|
+
};
|
|
653
689
|
}
|
|
654
690
|
}
|
|
655
691
|
export const matchRoute = matchManifestRoute;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function writeServerOutput({ coreOutputDir, staticDir, projectRoot, config, basePath }: {
|
|
2
|
+
coreOutputDir: any;
|
|
3
|
+
staticDir: any;
|
|
4
|
+
projectRoot: any;
|
|
5
|
+
config: any;
|
|
6
|
+
basePath?: string | undefined;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
serverDir: string;
|
|
9
|
+
routes: {
|
|
10
|
+
name: any;
|
|
11
|
+
path: any;
|
|
12
|
+
output: any;
|
|
13
|
+
base_path: string;
|
|
14
|
+
page_asset: any;
|
|
15
|
+
page_asset_file: string | null;
|
|
16
|
+
route_id: any;
|
|
17
|
+
server_script_path: any;
|
|
18
|
+
guard_module_ref: any;
|
|
19
|
+
load_module_ref: any;
|
|
20
|
+
has_guard: boolean;
|
|
21
|
+
has_load: boolean;
|
|
22
|
+
params: string[];
|
|
23
|
+
image_manifest_file: string | null;
|
|
24
|
+
image_config: any;
|
|
25
|
+
}[];
|
|
26
|
+
}>;
|