@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function buildPublicPath(routePath: any, params?: {}, basePath?: string): string;
|
|
2
|
+
export function extractInternalParams(requestUrl: any, route: any): {};
|
|
3
|
+
/**
|
|
4
|
+
* @param {{
|
|
5
|
+
* request: Request,
|
|
6
|
+
* route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null },
|
|
7
|
+
* params: Record<string, string>,
|
|
8
|
+
* routeModulePath: string,
|
|
9
|
+
* guardOnly?: boolean
|
|
10
|
+
* }} options
|
|
11
|
+
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
|
|
12
|
+
*/
|
|
13
|
+
export function executeRouteRequest(options: {
|
|
14
|
+
request: Request;
|
|
15
|
+
route: {
|
|
16
|
+
path: string;
|
|
17
|
+
params?: string[];
|
|
18
|
+
route_id?: string | null;
|
|
19
|
+
server_script_path?: string | null;
|
|
20
|
+
file?: string | null;
|
|
21
|
+
};
|
|
22
|
+
params: Record<string, string>;
|
|
23
|
+
routeModulePath: string;
|
|
24
|
+
guardOnly?: boolean;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
publicUrl: URL;
|
|
27
|
+
result: {
|
|
28
|
+
kind: string;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
trace: {
|
|
32
|
+
guard: string;
|
|
33
|
+
load: string;
|
|
34
|
+
};
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* @param {{
|
|
38
|
+
* request: Request,
|
|
39
|
+
* route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null },
|
|
40
|
+
* params: Record<string, string>,
|
|
41
|
+
* routeModulePath: string,
|
|
42
|
+
* shellHtmlPath: string,
|
|
43
|
+
* pageAssetPath?: string | null,
|
|
44
|
+
* imageManifestPath?: string | null,
|
|
45
|
+
* imageConfig?: Record<string, unknown>
|
|
46
|
+
* }} options
|
|
47
|
+
* @returns {Promise<Response>}
|
|
48
|
+
*/
|
|
49
|
+
export function renderRouteRequest(options: {
|
|
50
|
+
request: Request;
|
|
51
|
+
route: {
|
|
52
|
+
path: string;
|
|
53
|
+
params?: string[];
|
|
54
|
+
route_id?: string | null;
|
|
55
|
+
server_script_path?: string | null;
|
|
56
|
+
file?: string | null;
|
|
57
|
+
};
|
|
58
|
+
params: Record<string, string>;
|
|
59
|
+
routeModulePath: string;
|
|
60
|
+
shellHtmlPath: string;
|
|
61
|
+
pageAssetPath?: string | null;
|
|
62
|
+
imageManifestPath?: string | null;
|
|
63
|
+
imageConfig?: Record<string, unknown>;
|
|
64
|
+
}): Promise<Response>;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { appLocalRedirectLocation, normalizeBasePath, prependBasePath } from '../base-path.js';
|
|
4
|
+
import { createImageRuntimePayload, injectImageRuntimePayload } from '../images/payload.js';
|
|
5
|
+
import { materializeImageMarkup } from '../images/materialize.js';
|
|
6
|
+
import { allow, data, deny, redirect, resolveRouteResult } from '../server-contract.js';
|
|
7
|
+
const MODULE_CACHE = new Map();
|
|
8
|
+
const INTERNAL_QUERY_PREFIX = '__zenith_param_';
|
|
9
|
+
function parseCookies(rawCookieHeader) {
|
|
10
|
+
const out = Object.create(null);
|
|
11
|
+
const raw = String(rawCookieHeader || '');
|
|
12
|
+
if (!raw) {
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
const pairs = raw.split(';');
|
|
16
|
+
for (const part of pairs) {
|
|
17
|
+
const eq = part.indexOf('=');
|
|
18
|
+
if (eq <= 0) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const key = part.slice(0, eq).trim();
|
|
22
|
+
if (!key) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const value = part.slice(eq + 1).trim();
|
|
26
|
+
try {
|
|
27
|
+
out[key] = decodeURIComponent(value);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
out[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function escapeInlineJson(payload) {
|
|
36
|
+
return JSON.stringify(payload)
|
|
37
|
+
.replace(/</g, '\\u003C')
|
|
38
|
+
.replace(/>/g, '\\u003E')
|
|
39
|
+
.replace(/\//g, '\\u002F')
|
|
40
|
+
.replace(/\u2028/g, '\\u2028')
|
|
41
|
+
.replace(/\u2029/g, '\\u2029');
|
|
42
|
+
}
|
|
43
|
+
function defaultRouteDenyMessage(status) {
|
|
44
|
+
if (status === 401) {
|
|
45
|
+
return 'Unauthorized';
|
|
46
|
+
}
|
|
47
|
+
if (status === 403) {
|
|
48
|
+
return 'Forbidden';
|
|
49
|
+
}
|
|
50
|
+
if (status === 404) {
|
|
51
|
+
return 'Not Found';
|
|
52
|
+
}
|
|
53
|
+
return 'Internal Server Error';
|
|
54
|
+
}
|
|
55
|
+
function createTextResponse(status, message) {
|
|
56
|
+
return new Response(message || defaultRouteDenyMessage(status), {
|
|
57
|
+
status,
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'text/plain; charset=utf-8'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function injectSsrPayload(html, payload) {
|
|
64
|
+
const serialized = escapeInlineJson(payload);
|
|
65
|
+
const scriptTag = `<script id="zenith-ssr-data">window.__zenith_ssr_data = ${serialized};</script>`;
|
|
66
|
+
const existingTagRe = /<script\b[^>]*\bid=(["'])zenith-ssr-data\1[^>]*>[\s\S]*?<\/script>/i;
|
|
67
|
+
if (existingTagRe.test(html)) {
|
|
68
|
+
return html.replace(existingTagRe, scriptTag);
|
|
69
|
+
}
|
|
70
|
+
if (/<\/head>/i.test(html)) {
|
|
71
|
+
return html.replace(/<\/head>/i, `${scriptTag}</head>`);
|
|
72
|
+
}
|
|
73
|
+
const bodyOpen = html.match(/<body\b[^>]*>/i);
|
|
74
|
+
if (bodyOpen) {
|
|
75
|
+
return html.replace(bodyOpen[0], `${bodyOpen[0]}${scriptTag}`);
|
|
76
|
+
}
|
|
77
|
+
return `${scriptTag}${html}`;
|
|
78
|
+
}
|
|
79
|
+
function splitRoutePath(routePath) {
|
|
80
|
+
return String(routePath || '').split('/').filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
export function buildPublicPath(routePath, params = {}, basePath = '/') {
|
|
83
|
+
const segments = splitRoutePath(routePath);
|
|
84
|
+
if (segments.length === 0) {
|
|
85
|
+
return prependBasePath(basePath, '/');
|
|
86
|
+
}
|
|
87
|
+
const materialized = [];
|
|
88
|
+
for (const segment of segments) {
|
|
89
|
+
if (segment.startsWith(':')) {
|
|
90
|
+
const value = String(params[segment.slice(1)] || '');
|
|
91
|
+
materialized.push(value);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (segment.startsWith('*')) {
|
|
95
|
+
const optional = segment.endsWith('?');
|
|
96
|
+
const key = optional ? segment.slice(1, -1) : segment.slice(1);
|
|
97
|
+
const value = String(params[key] || '').trim();
|
|
98
|
+
if (!value) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
materialized.push(...value.split('/').filter(Boolean));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
materialized.push(segment);
|
|
105
|
+
}
|
|
106
|
+
return prependBasePath(basePath, `/${materialized.join('/')}`);
|
|
107
|
+
}
|
|
108
|
+
export function extractInternalParams(requestUrl, route) {
|
|
109
|
+
const url = requestUrl instanceof URL ? requestUrl : new URL(String(requestUrl));
|
|
110
|
+
const params = {};
|
|
111
|
+
for (const key of route.params || []) {
|
|
112
|
+
const queryKey = `${INTERNAL_QUERY_PREFIX}${key}`;
|
|
113
|
+
if (url.searchParams.has(queryKey)) {
|
|
114
|
+
params[key] = url.searchParams.get(queryKey) || '';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return params;
|
|
118
|
+
}
|
|
119
|
+
function buildPublicUrl(requestUrl, route, params) {
|
|
120
|
+
const incoming = requestUrl instanceof URL ? requestUrl : new URL(String(requestUrl));
|
|
121
|
+
const publicUrl = new URL(incoming.toString());
|
|
122
|
+
publicUrl.pathname = buildPublicPath(route.path, params, normalizeBasePath(route.base_path || '/'));
|
|
123
|
+
const filtered = new URLSearchParams();
|
|
124
|
+
for (const [key, value] of incoming.searchParams.entries()) {
|
|
125
|
+
if (key.startsWith(INTERNAL_QUERY_PREFIX)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
filtered.append(key, value);
|
|
129
|
+
}
|
|
130
|
+
publicUrl.search = filtered.toString();
|
|
131
|
+
return publicUrl;
|
|
132
|
+
}
|
|
133
|
+
async function loadImageManifest(imageManifestPath) {
|
|
134
|
+
if (!imageManifestPath) {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(await readFile(imageManifestPath, 'utf8'));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function loadRouteExports(routeModulePath) {
|
|
145
|
+
const cacheKey = pathToFileURL(routeModulePath).href;
|
|
146
|
+
if (MODULE_CACHE.has(cacheKey)) {
|
|
147
|
+
return MODULE_CACHE.get(cacheKey);
|
|
148
|
+
}
|
|
149
|
+
const mod = await import(cacheKey);
|
|
150
|
+
const value = mod && typeof mod === 'object' ? mod : {};
|
|
151
|
+
MODULE_CACHE.set(cacheKey, value);
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function createRouteContext({ request, route, params, publicUrl }) {
|
|
155
|
+
const requestHeaders = Object.fromEntries(request.headers.entries());
|
|
156
|
+
return {
|
|
157
|
+
params: { ...params },
|
|
158
|
+
url: publicUrl,
|
|
159
|
+
headers: { ...requestHeaders },
|
|
160
|
+
cookies: parseCookies(request.headers.get('cookie') || ''),
|
|
161
|
+
request,
|
|
162
|
+
method: request.method,
|
|
163
|
+
route: {
|
|
164
|
+
id: route.route_id || route.path,
|
|
165
|
+
pattern: route.path,
|
|
166
|
+
file: route.file || route.server_script_path || route.route_id || route.path
|
|
167
|
+
},
|
|
168
|
+
env: {},
|
|
169
|
+
auth: {
|
|
170
|
+
async getSession() {
|
|
171
|
+
return null;
|
|
172
|
+
},
|
|
173
|
+
async requireSession() {
|
|
174
|
+
throw redirect('/login', 302);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
allow,
|
|
178
|
+
redirect,
|
|
179
|
+
deny,
|
|
180
|
+
data
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* @param {{
|
|
185
|
+
* request: Request,
|
|
186
|
+
* route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null },
|
|
187
|
+
* params: Record<string, string>,
|
|
188
|
+
* routeModulePath: string,
|
|
189
|
+
* guardOnly?: boolean
|
|
190
|
+
* }} options
|
|
191
|
+
* @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
|
|
192
|
+
*/
|
|
193
|
+
export async function executeRouteRequest(options) {
|
|
194
|
+
const { request, route, params, routeModulePath, guardOnly = false } = options;
|
|
195
|
+
const publicUrl = buildPublicUrl(request.url, route, params);
|
|
196
|
+
const ctx = createRouteContext({ request, route, params, publicUrl });
|
|
197
|
+
const exports = await loadRouteExports(routeModulePath);
|
|
198
|
+
const resolved = await resolveRouteResult({
|
|
199
|
+
exports,
|
|
200
|
+
ctx,
|
|
201
|
+
filePath: route.file || route.server_script_path || route.path,
|
|
202
|
+
guardOnly
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
publicUrl,
|
|
206
|
+
result: resolved.result,
|
|
207
|
+
trace: resolved.trace
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* @param {{
|
|
212
|
+
* request: Request,
|
|
213
|
+
* route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null },
|
|
214
|
+
* params: Record<string, string>,
|
|
215
|
+
* routeModulePath: string,
|
|
216
|
+
* shellHtmlPath: string,
|
|
217
|
+
* pageAssetPath?: string | null,
|
|
218
|
+
* imageManifestPath?: string | null,
|
|
219
|
+
* imageConfig?: Record<string, unknown>
|
|
220
|
+
* }} options
|
|
221
|
+
* @returns {Promise<Response>}
|
|
222
|
+
*/
|
|
223
|
+
export async function renderRouteRequest(options) {
|
|
224
|
+
const { request, route, params, routeModulePath, shellHtmlPath, pageAssetPath = null, imageManifestPath = null, imageConfig = {} } = options;
|
|
225
|
+
try {
|
|
226
|
+
const { publicUrl, result } = await executeRouteRequest({
|
|
227
|
+
request,
|
|
228
|
+
route,
|
|
229
|
+
params,
|
|
230
|
+
routeModulePath
|
|
231
|
+
});
|
|
232
|
+
if (result.kind === 'redirect') {
|
|
233
|
+
return new Response('', {
|
|
234
|
+
status: Number.isInteger(result.status) ? result.status : 302,
|
|
235
|
+
headers: {
|
|
236
|
+
Location: appLocalRedirectLocation(result.location, route.base_path || '/'),
|
|
237
|
+
'Cache-Control': 'no-store'
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (result.kind === 'deny') {
|
|
242
|
+
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
243
|
+
return createTextResponse(status, result.message || defaultRouteDenyMessage(status));
|
|
244
|
+
}
|
|
245
|
+
const ssrPayload = result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)
|
|
246
|
+
? result.data
|
|
247
|
+
: {};
|
|
248
|
+
const localImages = await loadImageManifest(imageManifestPath);
|
|
249
|
+
const imagePayload = createImageRuntimePayload(imageConfig, localImages, 'passthrough', route.base_path || '/');
|
|
250
|
+
let html = await readFile(shellHtmlPath, 'utf8');
|
|
251
|
+
if (pageAssetPath) {
|
|
252
|
+
html = await materializeImageMarkup({
|
|
253
|
+
html,
|
|
254
|
+
pageAssetPath,
|
|
255
|
+
payload: imagePayload,
|
|
256
|
+
ssrData: ssrPayload,
|
|
257
|
+
routePathname: publicUrl.pathname
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
html = injectSsrPayload(html, ssrPayload);
|
|
261
|
+
html = injectImageRuntimePayload(html, imagePayload);
|
|
262
|
+
return new Response(html, {
|
|
263
|
+
status: 200,
|
|
264
|
+
headers: {
|
|
265
|
+
'Content-Type': 'text/html; charset=utf-8'
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
const message = String(error);
|
|
271
|
+
return createTextResponse(500, message || defaultRouteDenyMessage(500));
|
|
272
|
+
}
|
|
273
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Deterministic project orchestrator for Zenith framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,11 +30,13 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ./scripts/copy-framework-components.mjs && node -e \"const fs=require('fs');if(fs.existsSync('dist/index.js')) fs.chmodSync('dist/index.js',0o755)\"",
|
|
32
32
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"test:deployment-smoke": "bun run build && node --experimental-vm-modules $(node -e \"const path=require('node:path');const pkg=require.resolve('jest/package.json');process.stdout.write(path.join(path.dirname(pkg),'bin/jest.js'))\") --config jest.config.js --forceExit --runInBand tests/adapter-platform-static.spec.js tests/adapter-platform-server.spec.js tests/adapter-platform-node.spec.js tests/server-output-contract.spec.js tests/manifest-contract.spec.js",
|
|
33
34
|
"test": "bun run build && node --experimental-vm-modules $(node -e \"const path=require('node:path');const pkg=require.resolve('jest/package.json');process.stdout.write(path.join(path.dirname(pkg),'bin/jest.js'))\") --config jest.config.js --forceExit --runInBand",
|
|
34
35
|
"prepublishOnly": "npm run build"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@zenithbuild/compiler": "0.7.
|
|
38
|
+
"@zenithbuild/compiler": "0.7.3",
|
|
39
|
+
"@zenithbuild/bundler": "0.7.3",
|
|
38
40
|
"picocolors": "^1.1.1",
|
|
39
41
|
"sharp": "^0.34.4"
|
|
40
42
|
},
|