@zenithbuild/cli 0.7.4 → 0.7.7
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 +5 -3
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +48 -14
- package/dist/adapters/adapter-static-export.d.ts +5 -0
- package/dist/adapters/adapter-static-export.js +115 -0
- package/dist/adapters/adapter-types.d.ts +3 -1
- package/dist/adapters/adapter-types.js +5 -2
- package/dist/adapters/adapter-vercel.d.ts +1 -1
- package/dist/adapters/adapter-vercel.js +67 -19
- package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
- package/dist/adapters/copy-hosted-page-runtime.js +50 -0
- package/dist/adapters/resolve-adapter.js +4 -0
- package/dist/adapters/route-rules.d.ts +5 -0
- package/dist/adapters/route-rules.js +9 -0
- package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
- package/dist/adapters/validate-hosted-resource-routes.js +13 -0
- package/dist/auth/route-auth.d.ts +6 -0
- package/dist/auth/route-auth.js +236 -0
- package/dist/build/compiler-runtime.d.ts +1 -1
- package/dist/build/compiler-runtime.js +8 -2
- package/dist/build/hoisted-code-transforms.d.ts +4 -1
- package/dist/build/hoisted-code-transforms.js +5 -3
- package/dist/build/page-ir-normalization.d.ts +1 -1
- package/dist/build/page-ir-normalization.js +33 -3
- package/dist/build/page-loop-state.js +1 -1
- package/dist/build/page-loop.js +46 -2
- package/dist/build/server-script.d.ts +2 -1
- package/dist/build/server-script.js +7 -3
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +29 -17
- package/dist/dev-build-session/helpers.d.ts +29 -0
- package/dist/dev-build-session/helpers.js +223 -0
- package/dist/dev-build-session/session.d.ts +24 -0
- package/dist/dev-build-session/session.js +204 -0
- package/dist/dev-build-session/state.d.ts +37 -0
- package/dist/dev-build-session/state.js +17 -0
- package/dist/dev-build-session.d.ts +1 -24
- package/dist/dev-build-session.js +1 -434
- package/dist/dev-server/css-state.d.ts +7 -0
- package/dist/dev-server/css-state.js +92 -0
- package/dist/dev-server/not-found.d.ts +23 -0
- package/dist/dev-server/not-found.js +129 -0
- package/dist/dev-server/request-handler.d.ts +1 -0
- package/dist/dev-server/request-handler.js +376 -0
- package/dist/dev-server/route-check.d.ts +9 -0
- package/dist/dev-server/route-check.js +100 -0
- package/dist/dev-server/watcher.d.ts +5 -0
- package/dist/dev-server/watcher.js +216 -0
- package/dist/dev-server.js +136 -883
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/images/payload.js +4 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +70 -6
- package/dist/preview/create-preview-server.d.ts +18 -0
- package/dist/preview/create-preview-server.js +71 -0
- package/dist/preview/manifest.d.ts +42 -0
- package/dist/preview/manifest.js +57 -0
- package/dist/preview/paths.d.ts +3 -0
- package/dist/preview/paths.js +38 -0
- package/dist/preview/payload.d.ts +6 -0
- package/dist/preview/payload.js +34 -0
- package/dist/preview/request-handler.d.ts +1 -0
- package/dist/preview/request-handler.js +300 -0
- package/dist/preview/server-runner.d.ts +49 -0
- package/dist/preview/server-runner.js +220 -0
- package/dist/preview/server-script-runner-template.d.ts +1 -0
- package/dist/preview/server-script-runner-template.js +425 -0
- package/dist/preview.d.ts +5 -104
- package/dist/preview.js +7 -993
- package/dist/request-body.d.ts +0 -1
- package/dist/request-body.js +0 -6
- package/dist/resource-manifest.d.ts +16 -0
- package/dist/resource-manifest.js +53 -0
- package/dist/resource-response.d.ts +49 -0
- package/dist/resource-response.js +160 -0
- package/dist/resource-route-module.d.ts +15 -0
- package/dist/resource-route-module.js +129 -0
- package/dist/route-check-support.js +1 -1
- package/dist/server-contract/constants.d.ts +5 -0
- package/dist/server-contract/constants.js +5 -0
- package/dist/server-contract/export-validation.d.ts +5 -0
- package/dist/server-contract/export-validation.js +59 -0
- package/dist/server-contract/json-serializable.d.ts +1 -0
- package/dist/server-contract/json-serializable.js +52 -0
- package/dist/server-contract/resolve.d.ts +15 -0
- package/dist/server-contract/resolve.js +271 -0
- package/dist/server-contract/result-helpers.d.ts +51 -0
- package/dist/server-contract/result-helpers.js +59 -0
- package/dist/server-contract/route-result-validation.d.ts +2 -0
- package/dist/server-contract/route-result-validation.js +73 -0
- package/dist/server-contract/stage.d.ts +6 -0
- package/dist/server-contract/stage.js +22 -0
- package/dist/server-contract.d.ts +6 -54
- package/dist/server-contract.js +9 -301
- package/dist/server-error.d.ts +1 -1
- package/dist/server-error.js +2 -0
- package/dist/server-middleware.d.ts +10 -0
- package/dist/server-middleware.js +30 -0
- package/dist/server-output.d.ts +2 -1
- package/dist/server-output.js +72 -12
- package/dist/server-runtime/node-server.js +59 -7
- package/dist/server-runtime/route-render.d.ts +25 -1
- package/dist/server-runtime/route-render.js +81 -29
- package/dist/server-script-composition.d.ts +4 -2
- package/dist/server-script-composition.js +6 -3
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +3 -3
|
@@ -7,7 +7,7 @@ import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCh
|
|
|
7
7
|
import { handleImageRequest } from '../images/service.js';
|
|
8
8
|
import { createTrustedOriginResolver } from '../request-origin.js';
|
|
9
9
|
import { defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from '../server-error.js';
|
|
10
|
-
import { executeRouteRequest, renderRouteRequest } from './route-render.js';
|
|
10
|
+
import { executeRouteRequest, renderResourceRouteRequest, renderRouteRequest } from './route-render.js';
|
|
11
11
|
import { resolveRequestRoute } from './resolve-request-route.js';
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = dirname(__filename);
|
|
@@ -97,17 +97,53 @@ async function createWebRequest(req, url) {
|
|
|
97
97
|
}
|
|
98
98
|
return new Request(url.toString(), init);
|
|
99
99
|
}
|
|
100
|
+
function getSetCookieValues(response) {
|
|
101
|
+
if (typeof response?.headers?.getSetCookie === 'function') {
|
|
102
|
+
return response.headers.getSetCookie();
|
|
103
|
+
}
|
|
104
|
+
const value = response?.headers?.get?.('set-cookie');
|
|
105
|
+
return typeof value === 'string' && value.length > 0 ? [value] : [];
|
|
106
|
+
}
|
|
100
107
|
async function sendFetchResponse(res, response, method) {
|
|
101
108
|
res.statusCode = response.status;
|
|
109
|
+
const setCookies = getSetCookieValues(response);
|
|
110
|
+
if (setCookies.length > 0) {
|
|
111
|
+
res.setHeader('set-cookie', setCookies);
|
|
112
|
+
}
|
|
102
113
|
for (const [key, value] of response.headers.entries()) {
|
|
114
|
+
if (key.toLowerCase() === 'set-cookie') {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
103
117
|
res.setHeader(key, value);
|
|
104
118
|
}
|
|
105
|
-
if (String(method || 'GET').toUpperCase() === 'HEAD') {
|
|
119
|
+
if (String(method || 'GET').toUpperCase() === 'HEAD' || !response.body) {
|
|
106
120
|
res.end();
|
|
107
121
|
return;
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
try {
|
|
124
|
+
const bodyStream = Readable.fromWeb(response.body);
|
|
125
|
+
bodyStream.pipe(res);
|
|
126
|
+
bodyStream.on('error', (err) => {
|
|
127
|
+
logServerException('node response stream failed', err);
|
|
128
|
+
if (!res.headersSent) {
|
|
129
|
+
res.statusCode = 500;
|
|
130
|
+
res.end();
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
res.destroy();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
logServerException('node response pipe creation failed', err);
|
|
139
|
+
if (!res.headersSent) {
|
|
140
|
+
res.statusCode = 500;
|
|
141
|
+
res.end();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
res.destroy();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
111
147
|
}
|
|
112
148
|
async function sendStaticFile(res, filePath, method) {
|
|
113
149
|
const body = await readFile(filePath);
|
|
@@ -155,13 +191,16 @@ async function loadRuntimeContext(options = {}) {
|
|
|
155
191
|
base_path: '/'
|
|
156
192
|
});
|
|
157
193
|
const serverManifest = await readJson(join(serverDir, 'manifest.json'), { routes: [] });
|
|
194
|
+
const allServerRoutes = Array.isArray(serverManifest.routes) ? serverManifest.routes : [];
|
|
158
195
|
return {
|
|
159
196
|
distDir,
|
|
160
197
|
serverDir,
|
|
161
198
|
staticDir: resolve(serverDir, config.static_dir || '../static'),
|
|
162
199
|
buildManifest,
|
|
163
200
|
buildRoutes: Array.isArray(buildManifest.routes) ? buildManifest.routes : [],
|
|
164
|
-
serverRoutes:
|
|
201
|
+
serverRoutes: allServerRoutes,
|
|
202
|
+
pageServerRoutes: allServerRoutes.filter((route) => route?.route_kind !== 'resource'),
|
|
203
|
+
resourceServerRoutes: allServerRoutes.filter((route) => route?.route_kind === 'resource'),
|
|
165
204
|
images: config.images || {},
|
|
166
205
|
basePath: normalizeBasePath(config.base_path || '/')
|
|
167
206
|
};
|
|
@@ -206,7 +245,7 @@ async function handleRouteCheck(req, res, url, context) {
|
|
|
206
245
|
}
|
|
207
246
|
let result = { kind: 'allow' };
|
|
208
247
|
let routeId = buildResolved.route.path || '';
|
|
209
|
-
const serverResolved = resolveRequestRoute(canonicalTargetUrl, context.
|
|
248
|
+
const serverResolved = resolveRequestRoute(canonicalTargetUrl, context.pageServerRoutes);
|
|
210
249
|
if (serverResolved.matched && serverResolved.route) {
|
|
211
250
|
routeId = serverResolved.route.route_id || serverResolved.route.name || serverResolved.route.path || routeId;
|
|
212
251
|
try {
|
|
@@ -275,7 +314,20 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
|
|
|
275
314
|
await sendStaticFile(res, assetPath, req.method);
|
|
276
315
|
return;
|
|
277
316
|
}
|
|
278
|
-
const
|
|
317
|
+
const resourceResolved = resolveRequestRoute(canonicalUrl, context.resourceServerRoutes);
|
|
318
|
+
if (resourceResolved.matched && resourceResolved.route) {
|
|
319
|
+
const routeDir = join(context.serverDir, 'routes', resourceResolved.route.name);
|
|
320
|
+
const request = await createWebRequest(req, url);
|
|
321
|
+
const response = await renderResourceRouteRequest({
|
|
322
|
+
request,
|
|
323
|
+
route: resourceResolved.route,
|
|
324
|
+
params: resourceResolved.params,
|
|
325
|
+
routeModulePath: join(routeDir, 'route', 'entry.js')
|
|
326
|
+
});
|
|
327
|
+
await sendFetchResponse(res, response, req.method);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const serverResolved = resolveRequestRoute(canonicalUrl, context.pageServerRoutes);
|
|
279
331
|
if (serverResolved.matched && serverResolved.route) {
|
|
280
332
|
const routeDir = join(context.serverDir, 'routes', serverResolved.route.name);
|
|
281
333
|
const request = await createWebRequest(req, url);
|
|
@@ -8,7 +8,7 @@ export function extractInternalParams(requestUrl: any, route: any): {};
|
|
|
8
8
|
* routeModulePath: string,
|
|
9
9
|
* guardOnly?: boolean
|
|
10
10
|
* }} options
|
|
11
|
-
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
|
|
11
|
+
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
|
|
12
12
|
*/
|
|
13
13
|
export function executeRouteRequest(options: {
|
|
14
14
|
request: Request;
|
|
@@ -34,6 +34,7 @@ export function executeRouteRequest(options: {
|
|
|
34
34
|
load: string;
|
|
35
35
|
};
|
|
36
36
|
status?: number;
|
|
37
|
+
setCookies?: string[];
|
|
37
38
|
}>;
|
|
38
39
|
/**
|
|
39
40
|
* @param {{
|
|
@@ -62,3 +63,26 @@ export function renderRouteRequest(options: {
|
|
|
62
63
|
imageManifestPath?: string | null;
|
|
63
64
|
imageConfig?: Record<string, unknown>;
|
|
64
65
|
}): Promise<Response>;
|
|
66
|
+
/**
|
|
67
|
+
* @param {{
|
|
68
|
+
* request: Request,
|
|
69
|
+
* route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null, route_kind?: string | null, base_path?: string | null },
|
|
70
|
+
* params: Record<string, string>,
|
|
71
|
+
* routeModulePath: string
|
|
72
|
+
* }} options
|
|
73
|
+
* @returns {Promise<Response>}
|
|
74
|
+
*/
|
|
75
|
+
export function renderResourceRouteRequest(options: {
|
|
76
|
+
request: Request;
|
|
77
|
+
route: {
|
|
78
|
+
path: string;
|
|
79
|
+
params?: string[];
|
|
80
|
+
route_id?: string | null;
|
|
81
|
+
server_script_path?: string | null;
|
|
82
|
+
file?: string | null;
|
|
83
|
+
route_kind?: string | null;
|
|
84
|
+
base_path?: string | null;
|
|
85
|
+
};
|
|
86
|
+
params: Record<string, string>;
|
|
87
|
+
routeModulePath: string;
|
|
88
|
+
}): Promise<Response>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { attachRouteAuth } from '../auth/route-auth.js';
|
|
3
4
|
import { appLocalRedirectLocation, normalizeBasePath, prependBasePath } from '../base-path.js';
|
|
4
5
|
import { createImageRuntimePayload, injectImageRuntimePayload } from '../images/payload.js';
|
|
5
6
|
import { materializeImageMarkup } from '../images/materialize.js';
|
|
7
|
+
import { buildResourceResponseDescriptor } from '../resource-response.js';
|
|
6
8
|
import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException } from '../server-error.js';
|
|
7
|
-
import { allow, data, deny, invalid, redirect, resolveRouteResult } from '../server-contract.js';
|
|
9
|
+
import { allow, data, deny, download, invalid, json, redirect, resolveRouteResult, text } from '../server-contract.js';
|
|
8
10
|
const MODULE_CACHE = new Map();
|
|
9
11
|
const INTERNAL_QUERY_PREFIX = '__zenith_param_';
|
|
10
12
|
function parseCookies(rawCookieHeader) {
|
|
@@ -41,12 +43,29 @@ function escapeInlineJson(payload) {
|
|
|
41
43
|
.replace(/\u2028/g, '\\u2028')
|
|
42
44
|
.replace(/\u2029/g, '\\u2029');
|
|
43
45
|
}
|
|
44
|
-
function
|
|
46
|
+
function appendSetCookieHeaders(headers, setCookies = []) {
|
|
47
|
+
for (const value of Array.isArray(setCookies) ? setCookies : []) {
|
|
48
|
+
headers.append('Set-Cookie', value);
|
|
49
|
+
}
|
|
50
|
+
return headers;
|
|
51
|
+
}
|
|
52
|
+
function createTextResponse(status, message, setCookies = []) {
|
|
53
|
+
const headers = new Headers({
|
|
54
|
+
'Content-Type': 'text/plain; charset=utf-8'
|
|
55
|
+
});
|
|
56
|
+
appendSetCookieHeaders(headers, setCookies);
|
|
45
57
|
return new Response(message || defaultRouteDenyMessage(status), {
|
|
46
58
|
status,
|
|
47
|
-
headers
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
headers
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function createResourceResponse(result, basePath, setCookies = []) {
|
|
63
|
+
const descriptor = buildResourceResponseDescriptor(result, basePath, setCookies);
|
|
64
|
+
const headers = new Headers(descriptor.headers);
|
|
65
|
+
appendSetCookieHeaders(headers, descriptor.setCookies);
|
|
66
|
+
return new Response(descriptor.body, {
|
|
67
|
+
status: descriptor.status,
|
|
68
|
+
headers
|
|
50
69
|
});
|
|
51
70
|
}
|
|
52
71
|
function injectSsrPayload(html, payload) {
|
|
@@ -140,9 +159,9 @@ async function loadRouteExports(routeModulePath) {
|
|
|
140
159
|
MODULE_CACHE.set(cacheKey, value);
|
|
141
160
|
return value;
|
|
142
161
|
}
|
|
143
|
-
function createRouteContext({ request, route, params, publicUrl }) {
|
|
162
|
+
function createRouteContext({ request, route, params, publicUrl, guardOnly = false }) {
|
|
144
163
|
const requestHeaders = Object.fromEntries(request.headers.entries());
|
|
145
|
-
|
|
164
|
+
const ctx = {
|
|
146
165
|
params: { ...params },
|
|
147
166
|
url: publicUrl,
|
|
148
167
|
headers: { ...requestHeaders },
|
|
@@ -156,20 +175,22 @@ function createRouteContext({ request, route, params, publicUrl }) {
|
|
|
156
175
|
},
|
|
157
176
|
env: {},
|
|
158
177
|
action: null,
|
|
159
|
-
auth: {
|
|
160
|
-
async getSession() {
|
|
161
|
-
return null;
|
|
162
|
-
},
|
|
163
|
-
async requireSession() {
|
|
164
|
-
throw redirect('/login', 302);
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
178
|
allow,
|
|
168
179
|
redirect,
|
|
169
180
|
deny,
|
|
170
181
|
invalid,
|
|
171
|
-
data
|
|
182
|
+
data,
|
|
183
|
+
json,
|
|
184
|
+
text,
|
|
185
|
+
download
|
|
172
186
|
};
|
|
187
|
+
attachRouteAuth(ctx, {
|
|
188
|
+
requestUrl: publicUrl,
|
|
189
|
+
guardOnly,
|
|
190
|
+
redirect,
|
|
191
|
+
deny
|
|
192
|
+
});
|
|
193
|
+
return ctx;
|
|
173
194
|
}
|
|
174
195
|
/**
|
|
175
196
|
* @param {{
|
|
@@ -179,24 +200,26 @@ function createRouteContext({ request, route, params, publicUrl }) {
|
|
|
179
200
|
* routeModulePath: string,
|
|
180
201
|
* guardOnly?: boolean
|
|
181
202
|
* }} options
|
|
182
|
-
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
|
|
203
|
+
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
|
|
183
204
|
*/
|
|
184
205
|
export async function executeRouteRequest(options) {
|
|
185
206
|
const { request, route, params, routeModulePath, guardOnly = false } = options;
|
|
186
207
|
const publicUrl = buildPublicUrl(request.url, route, params);
|
|
187
|
-
const ctx = createRouteContext({ request, route, params, publicUrl });
|
|
208
|
+
const ctx = createRouteContext({ request, route, params, publicUrl, guardOnly });
|
|
188
209
|
const exports = await loadRouteExports(routeModulePath);
|
|
189
210
|
const resolved = await resolveRouteResult({
|
|
190
211
|
exports,
|
|
191
212
|
ctx,
|
|
192
213
|
filePath: route.file || route.server_script_path || route.path,
|
|
193
|
-
guardOnly
|
|
214
|
+
guardOnly,
|
|
215
|
+
routeKind: route.route_kind === 'resource' ? 'resource' : 'page'
|
|
194
216
|
});
|
|
195
217
|
return {
|
|
196
218
|
publicUrl,
|
|
197
219
|
result: resolved.result,
|
|
198
220
|
trace: resolved.trace,
|
|
199
|
-
status: resolved.status
|
|
221
|
+
status: resolved.status,
|
|
222
|
+
setCookies: Array.isArray(resolved.setCookies) ? resolved.setCookies : []
|
|
200
223
|
};
|
|
201
224
|
}
|
|
202
225
|
/**
|
|
@@ -214,24 +237,26 @@ export async function executeRouteRequest(options) {
|
|
|
214
237
|
export async function renderRouteRequest(options) {
|
|
215
238
|
const { request, route, params, routeModulePath, shellHtmlPath, imageManifestPath = null, imageConfig = {} } = options;
|
|
216
239
|
try {
|
|
217
|
-
const { publicUrl, result, status } = await executeRouteRequest({
|
|
240
|
+
const { publicUrl, result, status, setCookies = [] } = await executeRouteRequest({
|
|
218
241
|
request,
|
|
219
242
|
route,
|
|
220
243
|
params,
|
|
221
244
|
routeModulePath
|
|
222
245
|
});
|
|
223
246
|
if (result.kind === 'redirect') {
|
|
247
|
+
const headers = new Headers({
|
|
248
|
+
Location: appLocalRedirectLocation(result.location, route.base_path || '/'),
|
|
249
|
+
'Cache-Control': 'no-store'
|
|
250
|
+
});
|
|
251
|
+
appendSetCookieHeaders(headers, setCookies);
|
|
224
252
|
return new Response('', {
|
|
225
253
|
status: Number.isInteger(result.status) ? result.status : 302,
|
|
226
|
-
headers
|
|
227
|
-
Location: appLocalRedirectLocation(result.location, route.base_path || '/'),
|
|
228
|
-
'Cache-Control': 'no-store'
|
|
229
|
-
}
|
|
254
|
+
headers
|
|
230
255
|
});
|
|
231
256
|
}
|
|
232
257
|
if (result.kind === 'deny') {
|
|
233
258
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
234
|
-
return createTextResponse(status, clientFacingRouteMessage(status, result.message));
|
|
259
|
+
return createTextResponse(status, clientFacingRouteMessage(status, result.message), setCookies);
|
|
235
260
|
}
|
|
236
261
|
const ssrPayload = result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)
|
|
237
262
|
? result.data
|
|
@@ -248,11 +273,13 @@ export async function renderRouteRequest(options) {
|
|
|
248
273
|
});
|
|
249
274
|
html = injectSsrPayload(html, ssrPayload);
|
|
250
275
|
html = injectImageRuntimePayload(html, imagePayload);
|
|
276
|
+
const headers = new Headers({
|
|
277
|
+
'Content-Type': 'text/html; charset=utf-8'
|
|
278
|
+
});
|
|
279
|
+
appendSetCookieHeaders(headers, setCookies);
|
|
251
280
|
return new Response(html, {
|
|
252
281
|
status: Number.isInteger(status) ? status : 200,
|
|
253
|
-
headers
|
|
254
|
-
'Content-Type': 'text/html; charset=utf-8'
|
|
255
|
-
}
|
|
282
|
+
headers
|
|
256
283
|
});
|
|
257
284
|
}
|
|
258
285
|
catch (error) {
|
|
@@ -260,3 +287,28 @@ export async function renderRouteRequest(options) {
|
|
|
260
287
|
return createTextResponse(500, defaultRouteDenyMessage(500));
|
|
261
288
|
}
|
|
262
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* @param {{
|
|
292
|
+
* request: Request,
|
|
293
|
+
* route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null, route_kind?: string | null, base_path?: string | null },
|
|
294
|
+
* params: Record<string, string>,
|
|
295
|
+
* routeModulePath: string
|
|
296
|
+
* }} options
|
|
297
|
+
* @returns {Promise<Response>}
|
|
298
|
+
*/
|
|
299
|
+
export async function renderResourceRouteRequest(options) {
|
|
300
|
+
const { request, route, params, routeModulePath } = options;
|
|
301
|
+
try {
|
|
302
|
+
const { result, setCookies = [] } = await executeRouteRequest({
|
|
303
|
+
request,
|
|
304
|
+
route: { ...route, route_kind: 'resource' },
|
|
305
|
+
params,
|
|
306
|
+
routeModulePath
|
|
307
|
+
});
|
|
308
|
+
return createResourceResponse(result, route.base_path || '/', setCookies);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
logServerException('node resource route render failed', error);
|
|
312
|
+
return createTextResponse(500, defaultRouteDenyMessage(500));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @param {{
|
|
3
3
|
* sourceFile: string,
|
|
4
|
-
* inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null,
|
|
4
|
+
* inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null,
|
|
5
5
|
* adjacentGuardPath?: string | null,
|
|
6
6
|
* adjacentLoadPath?: string | null,
|
|
7
7
|
* adjacentActionPath?: string | null
|
|
8
8
|
* }} input
|
|
9
|
-
* @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
|
|
9
|
+
* @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
|
|
10
10
|
*/
|
|
11
11
|
export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath, adjacentActionPath }: {
|
|
12
12
|
sourceFile: string;
|
|
@@ -17,6 +17,7 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, ad
|
|
|
17
17
|
has_load: boolean;
|
|
18
18
|
has_action: boolean;
|
|
19
19
|
source_path: string;
|
|
20
|
+
export_paths?: string[];
|
|
20
21
|
} | null;
|
|
21
22
|
adjacentGuardPath?: string | null;
|
|
22
23
|
adjacentLoadPath?: string | null;
|
|
@@ -29,6 +30,7 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, ad
|
|
|
29
30
|
has_load: boolean;
|
|
30
31
|
has_action: boolean;
|
|
31
32
|
source_path: string;
|
|
33
|
+
export_paths?: string[];
|
|
32
34
|
} | null;
|
|
33
35
|
guardPath: string | null;
|
|
34
36
|
loadPath: string | null;
|
|
@@ -65,12 +65,12 @@ function classifyInlineServerSource(source) {
|
|
|
65
65
|
/**
|
|
66
66
|
* @param {{
|
|
67
67
|
* sourceFile: string,
|
|
68
|
-
* inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null,
|
|
68
|
+
* inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null,
|
|
69
69
|
* adjacentGuardPath?: string | null,
|
|
70
70
|
* adjacentLoadPath?: string | null,
|
|
71
71
|
* adjacentActionPath?: string | null
|
|
72
72
|
* }} input
|
|
73
|
-
* @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
|
|
73
|
+
* @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
|
|
74
74
|
*/
|
|
75
75
|
export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = null, adjacentGuardPath = null, adjacentLoadPath = null, adjacentActionPath = null }) {
|
|
76
76
|
const inlineSource = String(inlineServerScript?.source || '').trim();
|
|
@@ -128,7 +128,10 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
|
|
|
128
128
|
has_guard: inlineHasGuard || Boolean(adjacentGuardPath),
|
|
129
129
|
has_load: inlineHasLoad || Boolean(adjacentLoadPath),
|
|
130
130
|
has_action: inlineHasAction || Boolean(adjacentActionPath),
|
|
131
|
-
source_path: sourceFile
|
|
131
|
+
source_path: sourceFile,
|
|
132
|
+
export_paths: Array.isArray(inlineServerScript?.export_paths)
|
|
133
|
+
? [...inlineServerScript.export_paths]
|
|
134
|
+
: []
|
|
132
135
|
},
|
|
133
136
|
guardPath: adjacentGuardPath,
|
|
134
137
|
loadPath: adjacentLoadPath,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { resolveRequestRoute } from './server/resolve-request-route.js';
|
|
2
|
+
function skipWhitespace(source, start) {
|
|
3
|
+
let index = start;
|
|
4
|
+
while (index < source.length) {
|
|
5
|
+
const char = source[index];
|
|
6
|
+
if (/\s/.test(char)) {
|
|
7
|
+
index += 1;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (source.startsWith('//', index)) {
|
|
11
|
+
const nextLine = source.indexOf('\n', index + 2);
|
|
12
|
+
return nextLine === -1 ? source.length : skipWhitespace(source, nextLine + 1);
|
|
13
|
+
}
|
|
14
|
+
if (source.startsWith('/*', index)) {
|
|
15
|
+
const close = source.indexOf('*/', index + 2);
|
|
16
|
+
if (close === -1) {
|
|
17
|
+
throw new Error('[Zenith] Unterminated block comment in exportPaths literal.');
|
|
18
|
+
}
|
|
19
|
+
index = close + 2;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
return index;
|
|
25
|
+
}
|
|
26
|
+
function parseQuotedStringLiteral(source, start, sourceFile) {
|
|
27
|
+
const quote = source[start];
|
|
28
|
+
if (quote !== '"' && quote !== '\'') {
|
|
29
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths must be a literal array of string paths.`);
|
|
30
|
+
}
|
|
31
|
+
let index = start + 1;
|
|
32
|
+
let value = '';
|
|
33
|
+
while (index < source.length) {
|
|
34
|
+
const char = source[index];
|
|
35
|
+
if (char === '\\') {
|
|
36
|
+
const next = source[index + 1];
|
|
37
|
+
if (next === undefined) {
|
|
38
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths contains an invalid escape sequence.`);
|
|
39
|
+
}
|
|
40
|
+
value += char + next;
|
|
41
|
+
index += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === quote) {
|
|
45
|
+
try {
|
|
46
|
+
return {
|
|
47
|
+
value: JSON.parse(`"${value.replace(/"/g, '\\"')}"`),
|
|
48
|
+
nextIndex: index + 1
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths contains an invalid string literal.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (char === '\n' || char === '\r') {
|
|
56
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths string literals must stay on one line.`);
|
|
57
|
+
}
|
|
58
|
+
value += char;
|
|
59
|
+
index += 1;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths contains an unterminated string literal.`);
|
|
62
|
+
}
|
|
63
|
+
function parseStringArrayLiteral(source, start, sourceFile) {
|
|
64
|
+
if (source[start] !== '[') {
|
|
65
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths must be assigned a literal array of string paths.`);
|
|
66
|
+
}
|
|
67
|
+
const values = [];
|
|
68
|
+
let index = start + 1;
|
|
69
|
+
while (index < source.length) {
|
|
70
|
+
index = skipWhitespace(source, index);
|
|
71
|
+
if (source[index] === ']') {
|
|
72
|
+
return { values, nextIndex: index + 1 };
|
|
73
|
+
}
|
|
74
|
+
const parsed = parseQuotedStringLiteral(source, index, sourceFile);
|
|
75
|
+
values.push(parsed.value);
|
|
76
|
+
index = skipWhitespace(source, parsed.nextIndex);
|
|
77
|
+
if (source[index] === ',') {
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (source[index] === ']') {
|
|
82
|
+
return { values, nextIndex: index + 1 };
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths must be a comma-delimited literal array of string paths.`);
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths array is missing a closing "]".`);
|
|
87
|
+
}
|
|
88
|
+
function normalizeConcretePath(value, sourceFile) {
|
|
89
|
+
if (typeof value !== 'string') {
|
|
90
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be strings.`);
|
|
91
|
+
}
|
|
92
|
+
const trimmed = value.trim();
|
|
93
|
+
if (!trimmed.startsWith('/')) {
|
|
94
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must start with "/".`);
|
|
95
|
+
}
|
|
96
|
+
if (trimmed.includes('://') || trimmed.startsWith('//')) {
|
|
97
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be same-origin pathnames.`);
|
|
98
|
+
}
|
|
99
|
+
if (trimmed.includes('?') || trimmed.includes('#') || /[\r\n]/.test(trimmed)) {
|
|
100
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be pathnames without query or hash.`);
|
|
101
|
+
}
|
|
102
|
+
const segments = trimmed
|
|
103
|
+
.split('/')
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.map((segment) => segment.trim())
|
|
106
|
+
.filter((segment) => segment.length > 0);
|
|
107
|
+
for (const segment of segments) {
|
|
108
|
+
if (segment === '.' || segment === '..') {
|
|
109
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must not contain path traversal segments.`);
|
|
110
|
+
}
|
|
111
|
+
if (segment.startsWith(':') || segment.startsWith('*')) {
|
|
112
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be concrete public URLs.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return segments.length === 0 ? '/' : `/${segments.join('/')}`;
|
|
116
|
+
}
|
|
117
|
+
export function extractStaticExportPaths(source, sourceFile) {
|
|
118
|
+
const match = /\bexport\s+const\s+exportPaths\b/.exec(String(source || ''));
|
|
119
|
+
if (!match) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const equalsIndex = String(source || '').indexOf('=', match.index + match[0].length);
|
|
123
|
+
if (equalsIndex === -1) {
|
|
124
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths must use the form export const exportPaths = [...].`);
|
|
125
|
+
}
|
|
126
|
+
const valueStart = skipWhitespace(String(source || ''), equalsIndex + 1);
|
|
127
|
+
const { values } = parseStringArrayLiteral(String(source || ''), valueStart, sourceFile);
|
|
128
|
+
return values.map((value) => normalizeConcretePath(value, sourceFile));
|
|
129
|
+
}
|
|
130
|
+
export function validateStaticExportPaths(routePath, exportPaths, sourceFile) {
|
|
131
|
+
if (!Array.isArray(exportPaths)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const deduped = [];
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
for (const rawPath of exportPaths) {
|
|
137
|
+
const concretePath = normalizeConcretePath(rawPath, sourceFile);
|
|
138
|
+
if (seen.has(concretePath)) {
|
|
139
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths contains a duplicate path "${concretePath}".`);
|
|
140
|
+
}
|
|
141
|
+
seen.add(concretePath);
|
|
142
|
+
const resolved = resolveRequestRoute(new URL(concretePath, 'http://localhost'), [{ path: routePath }]);
|
|
143
|
+
if (!resolved.matched || resolved.route?.path !== routePath) {
|
|
144
|
+
throw new Error(`[Zenith] ${sourceFile}: exportPaths entry "${concretePath}" does not match route "${routePath}".`);
|
|
145
|
+
}
|
|
146
|
+
deduped.push(concretePath);
|
|
147
|
+
}
|
|
148
|
+
return deduped;
|
|
149
|
+
}
|
|
150
|
+
export function toStaticHtmlFilePath(pathname) {
|
|
151
|
+
const normalized = normalizeConcretePath(pathname, 'static-export');
|
|
152
|
+
if (normalized === '/') {
|
|
153
|
+
return 'index.html';
|
|
154
|
+
}
|
|
155
|
+
const relativePath = normalized.replace(/^\//, '');
|
|
156
|
+
if (/\.[a-zA-Z0-9]+$/.test(relativePath)) {
|
|
157
|
+
return relativePath;
|
|
158
|
+
}
|
|
159
|
+
return `${relativePath}/index.html`;
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Deterministic project orchestrator for Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"prepublishOnly": "npm run build"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@zenithbuild/compiler": "0.7.
|
|
42
|
-
"@zenithbuild/bundler": "0.7.
|
|
41
|
+
"@zenithbuild/compiler": "0.7.7",
|
|
42
|
+
"@zenithbuild/bundler": "0.7.7",
|
|
43
43
|
"picocolors": "^1.1.1",
|
|
44
44
|
"sharp": "^0.34.4"
|
|
45
45
|
},
|