@zenithbuild/cli 0.7.3 → 0.7.5
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 +18 -13
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +56 -13
- package/dist/adapters/adapter-node.js +8 -0
- 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 +70 -13
- package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
- package/dist/adapters/copy-hosted-page-runtime.js +49 -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 +10 -9
- package/dist/build/compiler-runtime.js +58 -2
- package/dist/build/compiler-signal-expression.d.ts +1 -0
- package/dist/build/compiler-signal-expression.js +155 -0
- package/dist/build/expression-rewrites.d.ts +1 -6
- package/dist/build/expression-rewrites.js +61 -65
- package/dist/build/page-component-loop.d.ts +3 -13
- package/dist/build/page-component-loop.js +21 -46
- package/dist/build/page-ir-normalization.d.ts +0 -8
- package/dist/build/page-ir-normalization.js +13 -234
- package/dist/build/page-loop-state.d.ts +6 -9
- package/dist/build/page-loop-state.js +9 -8
- package/dist/build/page-loop.js +27 -22
- package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
- package/dist/build/scoped-identifier-rewrite.js +28 -128
- package/dist/build/server-script.d.ts +3 -1
- package/dist/build/server-script.js +35 -5
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +32 -18
- package/dist/component-instance-ir.js +158 -52
- package/dist/dev-build-session.js +20 -6
- package/dist/dev-server.js +152 -55
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/framework-components/Image.zen +1 -1
- package/dist/images/materialization-plan.d.ts +1 -0
- package/dist/images/materialization-plan.js +6 -0
- package/dist/images/materialize.d.ts +5 -3
- package/dist/images/materialize.js +24 -109
- package/dist/images/router-manifest.d.ts +1 -0
- package/dist/images/router-manifest.js +49 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/index.js +8 -2
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +27 -7
- package/dist/preview.d.ts +13 -4
- package/dist/preview.js +261 -101
- package/dist/request-body.d.ts +1 -0
- package/dist/request-body.js +7 -0
- package/dist/request-origin.d.ts +2 -0
- package/dist/request-origin.js +45 -0
- package/dist/resource-manifest.d.ts +16 -0
- package/dist/resource-manifest.js +53 -0
- package/dist/resource-response.d.ts +34 -0
- package/dist/resource-response.js +71 -0
- package/dist/resource-route-module.d.ts +15 -0
- package/dist/resource-route-module.js +129 -0
- package/dist/route-check-support.d.ts +1 -0
- package/dist/route-check-support.js +4 -0
- package/dist/server-contract.d.ts +29 -6
- package/dist/server-contract.js +304 -42
- package/dist/server-error.d.ts +4 -0
- package/dist/server-error.js +36 -0
- package/dist/server-output.d.ts +4 -1
- package/dist/server-output.js +71 -10
- package/dist/server-runtime/node-server.js +67 -31
- package/dist/server-runtime/route-render.d.ts +27 -3
- package/dist/server-runtime/route-render.js +94 -53
- package/dist/server-script-composition.d.ts +13 -5
- package/dist/server-script-composition.js +29 -11
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +6 -3
|
@@ -1,9 +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';
|
|
6
|
-
import {
|
|
7
|
+
import { buildResourceResponseDescriptor } from '../resource-response.js';
|
|
8
|
+
import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException } from '../server-error.js';
|
|
9
|
+
import { allow, data, deny, download, invalid, json, redirect, resolveRouteResult, text } from '../server-contract.js';
|
|
7
10
|
const MODULE_CACHE = new Map();
|
|
8
11
|
const INTERNAL_QUERY_PREFIX = '__zenith_param_';
|
|
9
12
|
function parseCookies(rawCookieHeader) {
|
|
@@ -40,24 +43,29 @@ function escapeInlineJson(payload) {
|
|
|
40
43
|
.replace(/\u2028/g, '\\u2028')
|
|
41
44
|
.replace(/\u2029/g, '\\u2029');
|
|
42
45
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
function appendSetCookieHeaders(headers, setCookies = []) {
|
|
47
|
+
for (const value of Array.isArray(setCookies) ? setCookies : []) {
|
|
48
|
+
headers.append('Set-Cookie', value);
|
|
46
49
|
}
|
|
47
|
-
|
|
48
|
-
return 'Forbidden';
|
|
49
|
-
}
|
|
50
|
-
if (status === 404) {
|
|
51
|
-
return 'Not Found';
|
|
52
|
-
}
|
|
53
|
-
return 'Internal Server Error';
|
|
50
|
+
return headers;
|
|
54
51
|
}
|
|
55
|
-
function createTextResponse(status, message) {
|
|
52
|
+
function createTextResponse(status, message, setCookies = []) {
|
|
53
|
+
const headers = new Headers({
|
|
54
|
+
'Content-Type': 'text/plain; charset=utf-8'
|
|
55
|
+
});
|
|
56
|
+
appendSetCookieHeaders(headers, setCookies);
|
|
56
57
|
return new Response(message || defaultRouteDenyMessage(status), {
|
|
57
58
|
status,
|
|
58
|
-
headers
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
69
|
});
|
|
62
70
|
}
|
|
63
71
|
function injectSsrPayload(html, payload) {
|
|
@@ -151,9 +159,9 @@ async function loadRouteExports(routeModulePath) {
|
|
|
151
159
|
MODULE_CACHE.set(cacheKey, value);
|
|
152
160
|
return value;
|
|
153
161
|
}
|
|
154
|
-
function createRouteContext({ request, route, params, publicUrl }) {
|
|
162
|
+
function createRouteContext({ request, route, params, publicUrl, guardOnly = false }) {
|
|
155
163
|
const requestHeaders = Object.fromEntries(request.headers.entries());
|
|
156
|
-
|
|
164
|
+
const ctx = {
|
|
157
165
|
params: { ...params },
|
|
158
166
|
url: publicUrl,
|
|
159
167
|
headers: { ...requestHeaders },
|
|
@@ -166,19 +174,23 @@ function createRouteContext({ request, route, params, publicUrl }) {
|
|
|
166
174
|
file: route.file || route.server_script_path || route.route_id || route.path
|
|
167
175
|
},
|
|
168
176
|
env: {},
|
|
169
|
-
|
|
170
|
-
async getSession() {
|
|
171
|
-
return null;
|
|
172
|
-
},
|
|
173
|
-
async requireSession() {
|
|
174
|
-
throw redirect('/login', 302);
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
+
action: null,
|
|
177
178
|
allow,
|
|
178
179
|
redirect,
|
|
179
180
|
deny,
|
|
180
|
-
|
|
181
|
+
invalid,
|
|
182
|
+
data,
|
|
183
|
+
json,
|
|
184
|
+
text,
|
|
185
|
+
download
|
|
181
186
|
};
|
|
187
|
+
attachRouteAuth(ctx, {
|
|
188
|
+
requestUrl: publicUrl,
|
|
189
|
+
guardOnly,
|
|
190
|
+
redirect,
|
|
191
|
+
deny
|
|
192
|
+
});
|
|
193
|
+
return ctx;
|
|
182
194
|
}
|
|
183
195
|
/**
|
|
184
196
|
* @param {{
|
|
@@ -188,23 +200,26 @@ function createRouteContext({ request, route, params, publicUrl }) {
|
|
|
188
200
|
* routeModulePath: string,
|
|
189
201
|
* guardOnly?: boolean
|
|
190
202
|
* }} options
|
|
191
|
-
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
|
|
203
|
+
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
|
|
192
204
|
*/
|
|
193
205
|
export async function executeRouteRequest(options) {
|
|
194
206
|
const { request, route, params, routeModulePath, guardOnly = false } = options;
|
|
195
207
|
const publicUrl = buildPublicUrl(request.url, route, params);
|
|
196
|
-
const ctx = createRouteContext({ request, route, params, publicUrl });
|
|
208
|
+
const ctx = createRouteContext({ request, route, params, publicUrl, guardOnly });
|
|
197
209
|
const exports = await loadRouteExports(routeModulePath);
|
|
198
210
|
const resolved = await resolveRouteResult({
|
|
199
211
|
exports,
|
|
200
212
|
ctx,
|
|
201
213
|
filePath: route.file || route.server_script_path || route.path,
|
|
202
|
-
guardOnly
|
|
214
|
+
guardOnly,
|
|
215
|
+
routeKind: route.route_kind === 'resource' ? 'resource' : 'page'
|
|
203
216
|
});
|
|
204
217
|
return {
|
|
205
218
|
publicUrl,
|
|
206
219
|
result: resolved.result,
|
|
207
|
-
trace: resolved.trace
|
|
220
|
+
trace: resolved.trace,
|
|
221
|
+
status: resolved.status,
|
|
222
|
+
setCookies: Array.isArray(resolved.setCookies) ? resolved.setCookies : []
|
|
208
223
|
};
|
|
209
224
|
}
|
|
210
225
|
/**
|
|
@@ -214,33 +229,34 @@ export async function executeRouteRequest(options) {
|
|
|
214
229
|
* params: Record<string, string>,
|
|
215
230
|
* routeModulePath: string,
|
|
216
231
|
* shellHtmlPath: string,
|
|
217
|
-
* pageAssetPath?: string | null,
|
|
218
232
|
* imageManifestPath?: string | null,
|
|
219
233
|
* imageConfig?: Record<string, unknown>
|
|
220
234
|
* }} options
|
|
221
235
|
* @returns {Promise<Response>}
|
|
222
236
|
*/
|
|
223
237
|
export async function renderRouteRequest(options) {
|
|
224
|
-
const { request, route, params, routeModulePath, shellHtmlPath,
|
|
238
|
+
const { request, route, params, routeModulePath, shellHtmlPath, imageManifestPath = null, imageConfig = {} } = options;
|
|
225
239
|
try {
|
|
226
|
-
const { publicUrl, result } = await executeRouteRequest({
|
|
240
|
+
const { publicUrl, result, status, setCookies = [] } = await executeRouteRequest({
|
|
227
241
|
request,
|
|
228
242
|
route,
|
|
229
243
|
params,
|
|
230
244
|
routeModulePath
|
|
231
245
|
});
|
|
232
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);
|
|
233
252
|
return new Response('', {
|
|
234
253
|
status: Number.isInteger(result.status) ? result.status : 302,
|
|
235
|
-
headers
|
|
236
|
-
Location: appLocalRedirectLocation(result.location, route.base_path || '/'),
|
|
237
|
-
'Cache-Control': 'no-store'
|
|
238
|
-
}
|
|
254
|
+
headers
|
|
239
255
|
});
|
|
240
256
|
}
|
|
241
257
|
if (result.kind === 'deny') {
|
|
242
258
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
243
|
-
return createTextResponse(status, result.message
|
|
259
|
+
return createTextResponse(status, clientFacingRouteMessage(status, result.message), setCookies);
|
|
244
260
|
}
|
|
245
261
|
const ssrPayload = result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)
|
|
246
262
|
? result.data
|
|
@@ -248,26 +264,51 @@ export async function renderRouteRequest(options) {
|
|
|
248
264
|
const localImages = await loadImageManifest(imageManifestPath);
|
|
249
265
|
const imagePayload = createImageRuntimePayload(imageConfig, localImages, 'passthrough', route.base_path || '/');
|
|
250
266
|
let html = await readFile(shellHtmlPath, 'utf8');
|
|
251
|
-
|
|
252
|
-
html
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
}
|
|
267
|
+
html = await materializeImageMarkup({
|
|
268
|
+
html,
|
|
269
|
+
payload: imagePayload,
|
|
270
|
+
imageMaterialization: Array.isArray(route.image_materialization)
|
|
271
|
+
? route.image_materialization
|
|
272
|
+
: []
|
|
273
|
+
});
|
|
260
274
|
html = injectSsrPayload(html, ssrPayload);
|
|
261
275
|
html = injectImageRuntimePayload(html, imagePayload);
|
|
276
|
+
const headers = new Headers({
|
|
277
|
+
'Content-Type': 'text/html; charset=utf-8'
|
|
278
|
+
});
|
|
279
|
+
appendSetCookieHeaders(headers, setCookies);
|
|
262
280
|
return new Response(html, {
|
|
263
|
-
status: 200,
|
|
264
|
-
headers
|
|
265
|
-
|
|
266
|
-
|
|
281
|
+
status: Number.isInteger(status) ? status : 200,
|
|
282
|
+
headers
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
logServerException('node route render failed', error);
|
|
287
|
+
return createTextResponse(500, defaultRouteDenyMessage(500));
|
|
288
|
+
}
|
|
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
|
|
267
307
|
});
|
|
308
|
+
return createResourceResponse(result, route.base_path || '/', setCookies);
|
|
268
309
|
}
|
|
269
310
|
catch (error) {
|
|
270
|
-
|
|
271
|
-
return createTextResponse(500,
|
|
311
|
+
logServerException('node resource route render failed', error);
|
|
312
|
+
return createTextResponse(500, defaultRouteDenyMessage(500));
|
|
272
313
|
}
|
|
273
314
|
}
|
|
@@ -1,39 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @param {{
|
|
3
3
|
* sourceFile: string,
|
|
4
|
-
* inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: 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
|
-
* adjacentLoadPath?: string | null
|
|
6
|
+
* adjacentLoadPath?: string | null,
|
|
7
|
+
* adjacentActionPath?: string | null
|
|
7
8
|
* }} input
|
|
8
|
-
* @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null, guardPath: string | null, loadPath: 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 }}
|
|
9
10
|
*/
|
|
10
|
-
export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath }: {
|
|
11
|
+
export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath, adjacentActionPath }: {
|
|
11
12
|
sourceFile: string;
|
|
12
13
|
inlineServerScript?: {
|
|
13
14
|
source: string;
|
|
14
15
|
prerender: boolean;
|
|
15
16
|
has_guard: boolean;
|
|
16
17
|
has_load: boolean;
|
|
18
|
+
has_action: boolean;
|
|
17
19
|
source_path: string;
|
|
20
|
+
export_paths?: string[];
|
|
18
21
|
} | null;
|
|
19
22
|
adjacentGuardPath?: string | null;
|
|
20
23
|
adjacentLoadPath?: string | null;
|
|
24
|
+
adjacentActionPath?: string | null;
|
|
21
25
|
}): {
|
|
22
26
|
serverScript: {
|
|
23
27
|
source: string;
|
|
24
28
|
prerender: boolean;
|
|
25
29
|
has_guard: boolean;
|
|
26
30
|
has_load: boolean;
|
|
31
|
+
has_action: boolean;
|
|
27
32
|
source_path: string;
|
|
33
|
+
export_paths?: string[];
|
|
28
34
|
} | null;
|
|
29
35
|
guardPath: string | null;
|
|
30
36
|
loadPath: string | null;
|
|
37
|
+
actionPath: string | null;
|
|
31
38
|
};
|
|
32
39
|
/**
|
|
33
40
|
* @param {string} sourceFile
|
|
34
|
-
* @returns {{ guardPath: string | null, loadPath: string | null }}
|
|
41
|
+
* @returns {{ guardPath: string | null, loadPath: string | null, actionPath: string | null }}
|
|
35
42
|
*/
|
|
36
43
|
export function resolveAdjacentServerModules(sourceFile: string): {
|
|
37
44
|
guardPath: string | null;
|
|
38
45
|
loadPath: string | null;
|
|
46
|
+
actionPath: string | null;
|
|
39
47
|
};
|
|
@@ -4,7 +4,7 @@ const DATA_EXPORT_RE = /\bexport\s+const\s+data\b/;
|
|
|
4
4
|
const LEGACY_EXPORT_RE = /\bexport\s+const\s+(?:ssr_data|props|ssr)\b/;
|
|
5
5
|
/**
|
|
6
6
|
* @param {string} sourceFile
|
|
7
|
-
* @param {'guard' | 'load'} kind
|
|
7
|
+
* @param {'guard' | 'load' | 'action'} kind
|
|
8
8
|
* @returns {string[]}
|
|
9
9
|
*/
|
|
10
10
|
function adjacentModuleCandidates(sourceFile, kind) {
|
|
@@ -22,7 +22,7 @@ function adjacentModuleCandidates(sourceFile, kind) {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* @param {string} sourceFile
|
|
25
|
-
* @param {'guard' | 'load'} kind
|
|
25
|
+
* @param {'guard' | 'load' | 'action'} kind
|
|
26
26
|
* @returns {string | null}
|
|
27
27
|
*/
|
|
28
28
|
function resolveAdjacentModule(sourceFile, kind) {
|
|
@@ -65,16 +65,18 @@ function classifyInlineServerSource(source) {
|
|
|
65
65
|
/**
|
|
66
66
|
* @param {{
|
|
67
67
|
* sourceFile: string,
|
|
68
|
-
* inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: 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
|
-
* adjacentLoadPath?: string | null
|
|
70
|
+
* adjacentLoadPath?: string | null,
|
|
71
|
+
* adjacentActionPath?: string | null
|
|
71
72
|
* }} input
|
|
72
|
-
* @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null, guardPath: string | null, loadPath: 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 }}
|
|
73
74
|
*/
|
|
74
|
-
export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = null, adjacentGuardPath = null, adjacentLoadPath = null }) {
|
|
75
|
+
export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = null, adjacentGuardPath = null, adjacentLoadPath = null, adjacentActionPath = null }) {
|
|
75
76
|
const inlineSource = String(inlineServerScript?.source || '').trim();
|
|
76
77
|
const inlineHasGuard = inlineServerScript?.has_guard === true;
|
|
77
78
|
const inlineHasLoad = inlineServerScript?.has_load === true;
|
|
79
|
+
const inlineHasAction = inlineServerScript?.has_action === true;
|
|
78
80
|
const { hasData, hasLegacy } = classifyInlineServerSource(inlineSource);
|
|
79
81
|
if (inlineHasGuard && adjacentGuardPath) {
|
|
80
82
|
throw new Error(`Zenith server script contract violation:\n` +
|
|
@@ -88,6 +90,12 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
|
|
|
88
90
|
` Reason: load is defined both inline and in an adjacent module\n` +
|
|
89
91
|
` Example: keep load in either <script server> or ${basename(adjacentLoadPath)}, not both`);
|
|
90
92
|
}
|
|
93
|
+
if (inlineHasAction && adjacentActionPath) {
|
|
94
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
95
|
+
` File: ${sourceFile}\n` +
|
|
96
|
+
` Reason: action is defined both inline and in an adjacent module\n` +
|
|
97
|
+
` Example: keep action in either <script server> or ${basename(adjacentActionPath)}, not both`);
|
|
98
|
+
}
|
|
91
99
|
if (adjacentLoadPath && (hasData || hasLegacy)) {
|
|
92
100
|
throw new Error(`Zenith server script contract violation:\n` +
|
|
93
101
|
` File: ${sourceFile}\n` +
|
|
@@ -101,12 +109,16 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
|
|
|
101
109
|
if (adjacentLoadPath) {
|
|
102
110
|
prologue.push(`export { load } from '${renderRelativeSpecifier(sourceFile, adjacentLoadPath)}';`);
|
|
103
111
|
}
|
|
112
|
+
if (adjacentActionPath) {
|
|
113
|
+
prologue.push(`export { action } from '${renderRelativeSpecifier(sourceFile, adjacentActionPath)}';`);
|
|
114
|
+
}
|
|
104
115
|
const mergedSource = [...prologue, inlineSource].filter(Boolean).join('\n');
|
|
105
116
|
if (!mergedSource.trim()) {
|
|
106
117
|
return {
|
|
107
118
|
serverScript: null,
|
|
108
119
|
guardPath: adjacentGuardPath,
|
|
109
|
-
loadPath: adjacentLoadPath
|
|
120
|
+
loadPath: adjacentLoadPath,
|
|
121
|
+
actionPath: adjacentActionPath
|
|
110
122
|
};
|
|
111
123
|
}
|
|
112
124
|
return {
|
|
@@ -115,19 +127,25 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
|
|
|
115
127
|
prerender: inlineServerScript?.prerender === true,
|
|
116
128
|
has_guard: inlineHasGuard || Boolean(adjacentGuardPath),
|
|
117
129
|
has_load: inlineHasLoad || Boolean(adjacentLoadPath),
|
|
118
|
-
|
|
130
|
+
has_action: inlineHasAction || Boolean(adjacentActionPath),
|
|
131
|
+
source_path: sourceFile,
|
|
132
|
+
export_paths: Array.isArray(inlineServerScript?.export_paths)
|
|
133
|
+
? [...inlineServerScript.export_paths]
|
|
134
|
+
: []
|
|
119
135
|
},
|
|
120
136
|
guardPath: adjacentGuardPath,
|
|
121
|
-
loadPath: adjacentLoadPath
|
|
137
|
+
loadPath: adjacentLoadPath,
|
|
138
|
+
actionPath: adjacentActionPath
|
|
122
139
|
};
|
|
123
140
|
}
|
|
124
141
|
/**
|
|
125
142
|
* @param {string} sourceFile
|
|
126
|
-
* @returns {{ guardPath: string | null, loadPath: string | null }}
|
|
143
|
+
* @returns {{ guardPath: string | null, loadPath: string | null, actionPath: string | null }}
|
|
127
144
|
*/
|
|
128
145
|
export function resolveAdjacentServerModules(sourceFile) {
|
|
129
146
|
return {
|
|
130
147
|
guardPath: resolveAdjacentModule(sourceFile, 'guard'),
|
|
131
|
-
loadPath: resolveAdjacentModule(sourceFile, 'load')
|
|
148
|
+
loadPath: resolveAdjacentModule(sourceFile, 'load'),
|
|
149
|
+
actionPath: resolveAdjacentModule(sourceFile, 'action')
|
|
132
150
|
};
|
|
133
151
|
}
|
|
@@ -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,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Deterministic project orchestrator for Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"zenith": "./dist/index.js"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": "./dist/index.js"
|
|
10
13
|
},
|
|
@@ -35,8 +38,8 @@
|
|
|
35
38
|
"prepublishOnly": "npm run build"
|
|
36
39
|
},
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"@zenithbuild/compiler": "0.7.
|
|
39
|
-
"@zenithbuild/bundler": "0.7.
|
|
41
|
+
"@zenithbuild/compiler": "0.7.5",
|
|
42
|
+
"@zenithbuild/bundler": "0.7.5",
|
|
40
43
|
"picocolors": "^1.1.1",
|
|
41
44
|
"sharp": "^0.34.4"
|
|
42
45
|
},
|