@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.
Files changed (52) hide show
  1. package/README.md +59 -1
  2. package/dist/adapters/adapter-netlify-static.d.ts +5 -0
  3. package/dist/adapters/adapter-netlify-static.js +39 -0
  4. package/dist/adapters/adapter-netlify.d.ts +5 -0
  5. package/dist/adapters/adapter-netlify.js +129 -0
  6. package/dist/adapters/adapter-node.d.ts +5 -0
  7. package/dist/adapters/adapter-node.js +121 -0
  8. package/dist/adapters/adapter-static.d.ts +5 -0
  9. package/dist/adapters/adapter-static.js +20 -0
  10. package/dist/adapters/adapter-types.d.ts +44 -0
  11. package/dist/adapters/adapter-types.js +65 -0
  12. package/dist/adapters/adapter-vercel-static.d.ts +5 -0
  13. package/dist/adapters/adapter-vercel-static.js +36 -0
  14. package/dist/adapters/adapter-vercel.d.ts +5 -0
  15. package/dist/adapters/adapter-vercel.js +99 -0
  16. package/dist/adapters/resolve-adapter.d.ts +5 -0
  17. package/dist/adapters/resolve-adapter.js +84 -0
  18. package/dist/adapters/route-rules.d.ts +7 -0
  19. package/dist/adapters/route-rules.js +88 -0
  20. package/dist/base-path-html.d.ts +2 -0
  21. package/dist/base-path-html.js +42 -0
  22. package/dist/base-path.d.ts +8 -0
  23. package/dist/base-path.js +74 -0
  24. package/dist/build/compiler-runtime.d.ts +2 -1
  25. package/dist/build/compiler-runtime.js +4 -1
  26. package/dist/build/page-loop.d.ts +2 -2
  27. package/dist/build/page-loop.js +3 -3
  28. package/dist/build-output-manifest.d.ts +28 -0
  29. package/dist/build-output-manifest.js +100 -0
  30. package/dist/build.js +42 -11
  31. package/dist/config.d.ts +10 -46
  32. package/dist/config.js +162 -28
  33. package/dist/dev-build-session.d.ts +1 -0
  34. package/dist/dev-build-session.js +4 -5
  35. package/dist/framework-components/Image.zen +31 -9
  36. package/dist/images/payload.d.ts +2 -1
  37. package/dist/images/payload.js +3 -2
  38. package/dist/images/runtime.js +6 -5
  39. package/dist/images/service.js +2 -2
  40. package/dist/images/shared.d.ts +4 -2
  41. package/dist/images/shared.js +8 -3
  42. package/dist/index.js +36 -15
  43. package/dist/manifest.d.ts +14 -2
  44. package/dist/manifest.js +49 -6
  45. package/dist/preview.js +61 -25
  46. package/dist/server-output.d.ts +26 -0
  47. package/dist/server-output.js +297 -0
  48. package/dist/server-runtime/node-server.d.ts +2 -0
  49. package/dist/server-runtime/node-server.js +354 -0
  50. package/dist/server-runtime/route-render.d.ts +64 -0
  51. package/dist/server-runtime/route-render.js +273 -0
  52. 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.1",
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.1",
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
  },