@zenithbuild/cli 0.7.5 → 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/dist/adapters/adapter-netlify.js +0 -8
- package/dist/adapters/adapter-vercel.js +6 -14
- package/dist/adapters/copy-hosted-page-runtime.js +2 -1
- 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.js +46 -2
- 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 +123 -924
- package/dist/images/payload.js +4 -0
- package/dist/manifest.js +46 -1
- 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 -112
- package/dist/preview.js +7 -1119
- package/dist/resource-response.d.ts +15 -0
- package/dist/resource-response.js +91 -2
- 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 -62
- package/dist/server-contract.js +9 -493
- package/dist/server-middleware.d.ts +10 -0
- package/dist/server-middleware.js +30 -0
- package/dist/server-output.js +13 -1
- package/dist/server-runtime/node-server.js +25 -3
- package/package.json +3 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource' }} input
|
|
3
|
+
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
|
|
4
|
+
*/
|
|
5
|
+
export function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind, guardOnly }: {
|
|
6
|
+
source: string;
|
|
7
|
+
sourcePath: string;
|
|
8
|
+
params: Record<string, string>;
|
|
9
|
+
requestUrl?: string;
|
|
10
|
+
requestMethod?: string;
|
|
11
|
+
requestHeaders?: Record<string, string | string[] | undefined>;
|
|
12
|
+
requestBodyBuffer?: Buffer | null;
|
|
13
|
+
routePattern?: string;
|
|
14
|
+
routeFile?: string;
|
|
15
|
+
routeId?: string;
|
|
16
|
+
routeKind?: "page" | "resource";
|
|
17
|
+
}): Promise<{
|
|
18
|
+
result: {
|
|
19
|
+
kind: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
trace: {
|
|
23
|
+
guard: string;
|
|
24
|
+
action: string;
|
|
25
|
+
load: string;
|
|
26
|
+
};
|
|
27
|
+
status?: number;
|
|
28
|
+
setCookies?: string[];
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
32
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
33
|
+
*/
|
|
34
|
+
export function executeServerScript(input: {
|
|
35
|
+
source: string;
|
|
36
|
+
sourcePath: string;
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
requestUrl?: string;
|
|
39
|
+
requestMethod?: string;
|
|
40
|
+
requestHeaders?: Record<string, string | string[] | undefined>;
|
|
41
|
+
routePattern?: string;
|
|
42
|
+
routeFile?: string;
|
|
43
|
+
routeId?: string;
|
|
44
|
+
}): Promise<Record<string, unknown> | null>;
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} sourcePath
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function routeIdFromSourcePath(sourcePath: string): string;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { clientFacingRouteMessage, defaultRouteDenyMessage } from '../server-error.js';
|
|
5
|
+
import { SERVER_SCRIPT_RUNNER } from './server-script-runner-template.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource' }} input
|
|
9
|
+
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
|
|
10
|
+
*/
|
|
11
|
+
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind = 'page', guardOnly = false }) {
|
|
12
|
+
if (!source || !String(source).trim()) {
|
|
13
|
+
return {
|
|
14
|
+
result: { kind: 'data', data: {} },
|
|
15
|
+
trace: { guard: 'none', action: 'none', load: 'none' }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const payload = await spawnNodeServerRunner({
|
|
19
|
+
source,
|
|
20
|
+
sourcePath,
|
|
21
|
+
params,
|
|
22
|
+
requestUrl: requestUrl || 'http://localhost/',
|
|
23
|
+
requestMethod: requestMethod || 'GET',
|
|
24
|
+
requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
|
|
25
|
+
requestBodyBuffer: Buffer.isBuffer(requestBodyBuffer) ? requestBodyBuffer : null,
|
|
26
|
+
routePattern: routePattern || '',
|
|
27
|
+
routeFile: routeFile || sourcePath || '',
|
|
28
|
+
routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
|
|
29
|
+
routeKind,
|
|
30
|
+
guardOnly
|
|
31
|
+
});
|
|
32
|
+
if (payload === null || payload === undefined) {
|
|
33
|
+
return {
|
|
34
|
+
result: { kind: 'data', data: {} },
|
|
35
|
+
trace: { guard: 'none', action: 'none', load: 'none' }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (typeof payload !== 'object' || Array.isArray(payload)) {
|
|
39
|
+
throw new Error('[zenith-preview] server script payload must be an object');
|
|
40
|
+
}
|
|
41
|
+
const errorEnvelope = payload.__zenith_error;
|
|
42
|
+
if (errorEnvelope && typeof errorEnvelope === 'object') {
|
|
43
|
+
return {
|
|
44
|
+
result: {
|
|
45
|
+
kind: 'deny',
|
|
46
|
+
status: 500,
|
|
47
|
+
message: defaultRouteDenyMessage(500)
|
|
48
|
+
},
|
|
49
|
+
trace: { guard: 'none', action: 'none', load: 'deny' }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const result = payload.result;
|
|
53
|
+
const trace = payload.trace;
|
|
54
|
+
if (result && typeof result === 'object' && !Array.isArray(result) && typeof result.kind === 'string') {
|
|
55
|
+
return {
|
|
56
|
+
result,
|
|
57
|
+
trace: trace && typeof trace === 'object'
|
|
58
|
+
? {
|
|
59
|
+
guard: String(trace.guard || 'none'),
|
|
60
|
+
action: String(trace.action || 'none'),
|
|
61
|
+
load: String(trace.load || 'none')
|
|
62
|
+
}
|
|
63
|
+
: { guard: 'none', action: 'none', load: 'none' },
|
|
64
|
+
status: Number.isInteger(payload.status) ? payload.status : undefined,
|
|
65
|
+
setCookies: Array.isArray(payload.setCookies)
|
|
66
|
+
? payload.setCookies.filter((value) => typeof value === 'string' && value.length > 0)
|
|
67
|
+
: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
result: {
|
|
72
|
+
kind: 'data',
|
|
73
|
+
data: payload
|
|
74
|
+
},
|
|
75
|
+
trace: { guard: 'none', action: 'none', load: 'data' }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
80
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
81
|
+
*/
|
|
82
|
+
export async function executeServerScript(input) {
|
|
83
|
+
const execution = await executeServerRoute(input);
|
|
84
|
+
const result = execution?.result;
|
|
85
|
+
if (!result || typeof result !== 'object') {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
89
|
+
return result.data;
|
|
90
|
+
}
|
|
91
|
+
if (result.kind === 'redirect') {
|
|
92
|
+
return {
|
|
93
|
+
__zenith_error: {
|
|
94
|
+
status: Number.isInteger(result.status) ? result.status : 302,
|
|
95
|
+
code: 'REDIRECT',
|
|
96
|
+
message: `Redirect to ${String(result.location || '')}`
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (result.kind === 'deny') {
|
|
101
|
+
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
102
|
+
return {
|
|
103
|
+
__zenith_error: {
|
|
104
|
+
status,
|
|
105
|
+
code: status >= 500 ? 'LOAD_FAILED' : (status === 404 ? 'NOT_FOUND' : 'ACCESS_DENIED'),
|
|
106
|
+
message: clientFacingRouteMessage(status, result.message)
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, requestBodyBuffer?: Buffer | null, routePattern: string, routeFile: string, routeId: string, routeKind?: 'page' | 'resource' }} input
|
|
114
|
+
* @returns {Promise<unknown>}
|
|
115
|
+
*/
|
|
116
|
+
function spawnNodeServerRunner(input) {
|
|
117
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
118
|
+
const child = spawn(process.execPath, ['--experimental-vm-modules', '--input-type=module', '-e', SERVER_SCRIPT_RUNNER], {
|
|
119
|
+
env: {
|
|
120
|
+
...process.env,
|
|
121
|
+
ZENITH_SERVER_SOURCE: input.source,
|
|
122
|
+
ZENITH_SERVER_SOURCE_PATH: input.sourcePath || '',
|
|
123
|
+
ZENITH_SERVER_PARAMS: JSON.stringify(input.params || {}),
|
|
124
|
+
ZENITH_SERVER_REQUEST_URL: input.requestUrl || 'http://localhost/',
|
|
125
|
+
ZENITH_SERVER_REQUEST_METHOD: input.requestMethod || 'GET',
|
|
126
|
+
ZENITH_SERVER_REQUEST_HEADERS: JSON.stringify(input.requestHeaders || {}),
|
|
127
|
+
ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
|
|
128
|
+
ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
|
|
129
|
+
ZENITH_SERVER_ROUTE_ID: input.routeId || '',
|
|
130
|
+
ZENITH_SERVER_ROUTE_KIND: input.routeKind || 'page',
|
|
131
|
+
ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
|
|
132
|
+
ZENITH_SERVER_CONTRACT_PATH: join(__dirname, '..', 'server-contract.js'),
|
|
133
|
+
ZENITH_SERVER_ROUTE_AUTH_PATH: join(__dirname, '..', 'auth', 'route-auth.js')
|
|
134
|
+
},
|
|
135
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
136
|
+
});
|
|
137
|
+
const runnerRequestBody = Buffer.isBuffer(input.requestBodyBuffer) ? input.requestBodyBuffer : null;
|
|
138
|
+
child.stdin.on('error', () => {
|
|
139
|
+
// ignore broken pipes when the runner exits before consuming stdin
|
|
140
|
+
});
|
|
141
|
+
child.stdin.end(runnerRequestBody && runnerRequestBody.length > 0 ? runnerRequestBody : undefined);
|
|
142
|
+
let stdout = '';
|
|
143
|
+
let stderr = '';
|
|
144
|
+
child.stdout.on('data', (chunk) => {
|
|
145
|
+
stdout += String(chunk);
|
|
146
|
+
});
|
|
147
|
+
child.stderr.on('data', (chunk) => {
|
|
148
|
+
stderr += String(chunk);
|
|
149
|
+
});
|
|
150
|
+
child.on('error', (error) => {
|
|
151
|
+
rejectPromise(error);
|
|
152
|
+
});
|
|
153
|
+
child.on('close', (code) => {
|
|
154
|
+
if (code !== 0) {
|
|
155
|
+
rejectPromise(new Error(`[zenith-preview] server script execution failed (${code}): ${stderr.trim() || stdout.trim()}`));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const stderrOutput = stderr.trim();
|
|
159
|
+
const internalErrorIndex = stderrOutput.indexOf('[Zenith:Server]');
|
|
160
|
+
if (internalErrorIndex >= 0) {
|
|
161
|
+
console.error(stderrOutput.slice(internalErrorIndex).trim());
|
|
162
|
+
}
|
|
163
|
+
const raw = stdout.trim();
|
|
164
|
+
if (!raw || raw === 'null') {
|
|
165
|
+
resolvePromise(null);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
resolvePromise(JSON.parse(raw));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
rejectPromise(new Error(`[zenith-preview] invalid server payload JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* @param {Record<string, string | string[] | undefined>} headers
|
|
179
|
+
* @returns {Record<string, string>}
|
|
180
|
+
*/
|
|
181
|
+
function sanitizeRequestHeaders(headers) {
|
|
182
|
+
const out = Object.create(null);
|
|
183
|
+
const denyExact = new Set(['proxy-authorization', 'set-cookie']);
|
|
184
|
+
const denyPrefixes = ['x-forwarded-', 'cf-'];
|
|
185
|
+
for (const [rawKey, rawValue] of Object.entries(headers || {})) {
|
|
186
|
+
const key = String(rawKey || '').toLowerCase();
|
|
187
|
+
if (!key)
|
|
188
|
+
continue;
|
|
189
|
+
if (denyExact.has(key))
|
|
190
|
+
continue;
|
|
191
|
+
if (denyPrefixes.some((prefix) => key.startsWith(prefix)))
|
|
192
|
+
continue;
|
|
193
|
+
let value = '';
|
|
194
|
+
if (Array.isArray(rawValue)) {
|
|
195
|
+
value = rawValue.filter((entry) => entry !== undefined).map(String).join(', ');
|
|
196
|
+
}
|
|
197
|
+
else if (rawValue !== undefined) {
|
|
198
|
+
value = String(rawValue);
|
|
199
|
+
}
|
|
200
|
+
out[key] = value;
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* @param {string} sourcePath
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
export function routeIdFromSourcePath(sourcePath) {
|
|
209
|
+
const normalized = String(sourcePath || '').replaceAll('\\', '/');
|
|
210
|
+
const marker = '/pages/';
|
|
211
|
+
const markerIndex = normalized.lastIndexOf(marker);
|
|
212
|
+
let routeId = markerIndex >= 0
|
|
213
|
+
? normalized.slice(markerIndex + marker.length)
|
|
214
|
+
: normalized.split('/').pop() || normalized;
|
|
215
|
+
routeId = routeId.replace(/\.zen$/i, '');
|
|
216
|
+
if (routeId.endsWith('/index')) {
|
|
217
|
+
routeId = routeId.slice(0, -('/index'.length));
|
|
218
|
+
}
|
|
219
|
+
return routeId || 'index';
|
|
220
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SERVER_SCRIPT_RUNNER: string;
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
export const SERVER_SCRIPT_RUNNER = String.raw `
|
|
2
|
+
import vm from 'node:vm';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const source = process.env.ZENITH_SERVER_SOURCE || '';
|
|
8
|
+
const sourcePath = process.env.ZENITH_SERVER_SOURCE_PATH || '';
|
|
9
|
+
const params = JSON.parse(process.env.ZENITH_SERVER_PARAMS || '{}');
|
|
10
|
+
const requestUrl = process.env.ZENITH_SERVER_REQUEST_URL || 'http://localhost/';
|
|
11
|
+
const requestMethod = String(process.env.ZENITH_SERVER_REQUEST_METHOD || 'GET').toUpperCase();
|
|
12
|
+
const requestHeaders = JSON.parse(process.env.ZENITH_SERVER_REQUEST_HEADERS || '{}');
|
|
13
|
+
const routePattern = process.env.ZENITH_SERVER_ROUTE_PATTERN || '';
|
|
14
|
+
const routeFile = process.env.ZENITH_SERVER_ROUTE_FILE || sourcePath || '';
|
|
15
|
+
const routeId = process.env.ZENITH_SERVER_ROUTE_ID || routePattern || '';
|
|
16
|
+
const routeKind = process.env.ZENITH_SERVER_ROUTE_KIND || 'page';
|
|
17
|
+
const guardOnly = process.env.ZENITH_SERVER_GUARD_ONLY === '1';
|
|
18
|
+
|
|
19
|
+
if (!source.trim()) {
|
|
20
|
+
process.stdout.write('null');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let cachedTypeScript = undefined;
|
|
25
|
+
async function loadTypeScript() {
|
|
26
|
+
if (cachedTypeScript !== undefined) {
|
|
27
|
+
return cachedTypeScript;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const mod = await import('typescript');
|
|
31
|
+
cachedTypeScript = mod.default || mod;
|
|
32
|
+
} catch {
|
|
33
|
+
cachedTypeScript = null;
|
|
34
|
+
}
|
|
35
|
+
return cachedTypeScript;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function transpileIfNeeded(filename, code) {
|
|
39
|
+
const lower = String(filename || '').toLowerCase();
|
|
40
|
+
const isTs =
|
|
41
|
+
lower.endsWith('.ts') ||
|
|
42
|
+
lower.endsWith('.tsx') ||
|
|
43
|
+
lower.endsWith('.mts') ||
|
|
44
|
+
lower.endsWith('.cts');
|
|
45
|
+
if (!isTs) {
|
|
46
|
+
return code;
|
|
47
|
+
}
|
|
48
|
+
const ts = await loadTypeScript();
|
|
49
|
+
if (!ts || typeof ts.transpileModule !== 'function') {
|
|
50
|
+
throw new Error('[zenith-preview] TypeScript is required to execute server modules that import .ts files');
|
|
51
|
+
}
|
|
52
|
+
const output = ts.transpileModule(code, {
|
|
53
|
+
fileName: filename || 'server-script.ts',
|
|
54
|
+
compilerOptions: {
|
|
55
|
+
target: ts.ScriptTarget.ES2022,
|
|
56
|
+
module: ts.ModuleKind.ESNext,
|
|
57
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext
|
|
58
|
+
},
|
|
59
|
+
reportDiagnostics: false
|
|
60
|
+
});
|
|
61
|
+
return output.outputText;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function exists(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveRelativeSpecifier(specifier, parentIdentifier) {
|
|
74
|
+
let basePath = sourcePath;
|
|
75
|
+
if (parentIdentifier && parentIdentifier.startsWith('file:')) {
|
|
76
|
+
basePath = fileURLToPath(parentIdentifier);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const baseDir = basePath ? path.dirname(basePath) : process.cwd();
|
|
80
|
+
const candidateBase = specifier.startsWith('file:')
|
|
81
|
+
? fileURLToPath(specifier)
|
|
82
|
+
: path.resolve(baseDir, specifier);
|
|
83
|
+
|
|
84
|
+
const candidates = [];
|
|
85
|
+
if (path.extname(candidateBase)) {
|
|
86
|
+
candidates.push(candidateBase);
|
|
87
|
+
} else {
|
|
88
|
+
candidates.push(candidateBase);
|
|
89
|
+
candidates.push(candidateBase + '.ts');
|
|
90
|
+
candidates.push(candidateBase + '.tsx');
|
|
91
|
+
candidates.push(candidateBase + '.mts');
|
|
92
|
+
candidates.push(candidateBase + '.cts');
|
|
93
|
+
candidates.push(candidateBase + '.js');
|
|
94
|
+
candidates.push(candidateBase + '.mjs');
|
|
95
|
+
candidates.push(candidateBase + '.cjs');
|
|
96
|
+
candidates.push(path.join(candidateBase, 'index.ts'));
|
|
97
|
+
candidates.push(path.join(candidateBase, 'index.tsx'));
|
|
98
|
+
candidates.push(path.join(candidateBase, 'index.mts'));
|
|
99
|
+
candidates.push(path.join(candidateBase, 'index.cts'));
|
|
100
|
+
candidates.push(path.join(candidateBase, 'index.js'));
|
|
101
|
+
candidates.push(path.join(candidateBase, 'index.mjs'));
|
|
102
|
+
candidates.push(path.join(candidateBase, 'index.cjs'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const candidate of candidates) {
|
|
106
|
+
if (await exists(candidate)) {
|
|
107
|
+
return pathToFileURL(candidate).href;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error(
|
|
112
|
+
'[zenith-preview] Cannot resolve server import "' + specifier + '" from "' + (basePath || '<inline>') + '"'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isRelativeSpecifier(specifier) {
|
|
117
|
+
return (
|
|
118
|
+
specifier.startsWith('./') ||
|
|
119
|
+
specifier.startsWith('../') ||
|
|
120
|
+
specifier.startsWith('/') ||
|
|
121
|
+
specifier.startsWith('file:')
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const safeRequestHeaders =
|
|
126
|
+
requestHeaders && typeof requestHeaders === 'object'
|
|
127
|
+
? { ...requestHeaders }
|
|
128
|
+
: {};
|
|
129
|
+
function parseCookies(rawCookieHeader) {
|
|
130
|
+
const out = Object.create(null);
|
|
131
|
+
const raw = String(rawCookieHeader || '');
|
|
132
|
+
if (!raw) return out;
|
|
133
|
+
const pairs = raw.split(';');
|
|
134
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
135
|
+
const part = pairs[i];
|
|
136
|
+
const eq = part.indexOf('=');
|
|
137
|
+
if (eq <= 0) continue;
|
|
138
|
+
const key = part.slice(0, eq).trim();
|
|
139
|
+
if (!key) continue;
|
|
140
|
+
const value = part.slice(eq + 1).trim();
|
|
141
|
+
try {
|
|
142
|
+
out[key] = decodeURIComponent(value);
|
|
143
|
+
} catch {
|
|
144
|
+
out[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
const cookieHeader = typeof safeRequestHeaders.cookie === 'string'
|
|
150
|
+
? safeRequestHeaders.cookie
|
|
151
|
+
: '';
|
|
152
|
+
const requestCookies = parseCookies(cookieHeader);
|
|
153
|
+
|
|
154
|
+
function ctxAllow() {
|
|
155
|
+
return { kind: 'allow' };
|
|
156
|
+
}
|
|
157
|
+
function ctxRedirect(location, status = 302) {
|
|
158
|
+
return {
|
|
159
|
+
kind: 'redirect',
|
|
160
|
+
location: String(location || ''),
|
|
161
|
+
status: Number.isInteger(status) ? status : 302
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function ctxDeny(status = 403, message = undefined) {
|
|
165
|
+
return {
|
|
166
|
+
kind: 'deny',
|
|
167
|
+
status: Number.isInteger(status) ? status : 403,
|
|
168
|
+
message: typeof message === 'string' ? message : undefined
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function ctxInvalid(payload, status = 400) {
|
|
172
|
+
return {
|
|
173
|
+
kind: 'invalid',
|
|
174
|
+
data: payload,
|
|
175
|
+
status: Number.isInteger(status) ? status : 400
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function ctxData(payload) {
|
|
179
|
+
return {
|
|
180
|
+
kind: 'data',
|
|
181
|
+
data: payload
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function ctxJson(payload, status = 200) {
|
|
185
|
+
return {
|
|
186
|
+
kind: 'json',
|
|
187
|
+
data: payload,
|
|
188
|
+
status: Number.isInteger(status) ? status : 200
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function ctxText(body, status = 200) {
|
|
192
|
+
return {
|
|
193
|
+
kind: 'text',
|
|
194
|
+
body: typeof body === 'string' ? body : String(body ?? ''),
|
|
195
|
+
status: Number.isInteger(status) ? status : 200
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function readStdinBuffer() {
|
|
200
|
+
const chunks = [];
|
|
201
|
+
for await (const chunk of process.stdin) {
|
|
202
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
203
|
+
}
|
|
204
|
+
return Buffer.concat(chunks);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const requestInit = {
|
|
208
|
+
method: requestMethod,
|
|
209
|
+
headers: new Headers(safeRequestHeaders)
|
|
210
|
+
};
|
|
211
|
+
const requestBodyBuffer =
|
|
212
|
+
requestMethod !== 'GET' && requestMethod !== 'HEAD'
|
|
213
|
+
? await readStdinBuffer()
|
|
214
|
+
: Buffer.alloc(0);
|
|
215
|
+
if (requestMethod !== 'GET' && requestMethod !== 'HEAD' && requestBodyBuffer.length > 0) {
|
|
216
|
+
requestInit.body = requestBodyBuffer;
|
|
217
|
+
requestInit.duplex = 'half';
|
|
218
|
+
}
|
|
219
|
+
const requestSnapshot = new Request(requestUrl, requestInit);
|
|
220
|
+
const routeParams = { ...params };
|
|
221
|
+
const routeMeta = {
|
|
222
|
+
id: routeId,
|
|
223
|
+
pattern: routePattern,
|
|
224
|
+
file: routeFile ? path.relative(process.cwd(), routeFile) : ''
|
|
225
|
+
};
|
|
226
|
+
const routeContext = {
|
|
227
|
+
params: routeParams,
|
|
228
|
+
url: new URL(requestUrl),
|
|
229
|
+
headers: { ...safeRequestHeaders },
|
|
230
|
+
cookies: requestCookies,
|
|
231
|
+
request: requestSnapshot,
|
|
232
|
+
method: requestMethod,
|
|
233
|
+
route: routeMeta,
|
|
234
|
+
env: {},
|
|
235
|
+
action: null,
|
|
236
|
+
allow: ctxAllow,
|
|
237
|
+
redirect: ctxRedirect,
|
|
238
|
+
deny: ctxDeny,
|
|
239
|
+
invalid: ctxInvalid,
|
|
240
|
+
data: ctxData,
|
|
241
|
+
json: ctxJson,
|
|
242
|
+
text: ctxText
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const context = vm.createContext({
|
|
246
|
+
params: routeParams,
|
|
247
|
+
ctx: routeContext,
|
|
248
|
+
fetch: globalThis.fetch,
|
|
249
|
+
Blob: globalThis.Blob,
|
|
250
|
+
File: globalThis.File,
|
|
251
|
+
FormData: globalThis.FormData,
|
|
252
|
+
Headers: globalThis.Headers,
|
|
253
|
+
Request: globalThis.Request,
|
|
254
|
+
Response: globalThis.Response,
|
|
255
|
+
TextEncoder: globalThis.TextEncoder,
|
|
256
|
+
TextDecoder: globalThis.TextDecoder,
|
|
257
|
+
URL,
|
|
258
|
+
URLSearchParams,
|
|
259
|
+
Buffer,
|
|
260
|
+
console,
|
|
261
|
+
process,
|
|
262
|
+
setTimeout,
|
|
263
|
+
clearTimeout,
|
|
264
|
+
setInterval,
|
|
265
|
+
clearInterval
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const moduleCache = new Map();
|
|
269
|
+
const syntheticModuleCache = new Map();
|
|
270
|
+
|
|
271
|
+
async function createSyntheticModule(specifier) {
|
|
272
|
+
if (syntheticModuleCache.has(specifier)) {
|
|
273
|
+
return syntheticModuleCache.get(specifier);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const ns = await import(specifier);
|
|
277
|
+
const exportNames = Object.keys(ns);
|
|
278
|
+
const module = new vm.SyntheticModule(
|
|
279
|
+
exportNames,
|
|
280
|
+
function() {
|
|
281
|
+
for (const key of exportNames) {
|
|
282
|
+
this.setExport(key, ns[key]);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{ context }
|
|
286
|
+
);
|
|
287
|
+
await module.link(() => {
|
|
288
|
+
throw new Error(
|
|
289
|
+
'[zenith-preview] synthetic modules cannot contain nested imports: ' + specifier
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
syntheticModuleCache.set(specifier, module);
|
|
293
|
+
return module;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function loadFileModule(moduleUrl) {
|
|
297
|
+
if (moduleCache.has(moduleUrl)) {
|
|
298
|
+
return moduleCache.get(moduleUrl);
|
|
299
|
+
}
|
|
300
|
+
const modulePromise = (async () => {
|
|
301
|
+
const filename = fileURLToPath(moduleUrl);
|
|
302
|
+
let code = await fs.readFile(filename, 'utf8');
|
|
303
|
+
code = await transpileIfNeeded(filename, code);
|
|
304
|
+
const module = new vm.SourceTextModule(code, {
|
|
305
|
+
context,
|
|
306
|
+
identifier: moduleUrl,
|
|
307
|
+
initializeImportMeta(meta) {
|
|
308
|
+
meta.url = moduleUrl;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await module.link((specifier, referencingModule) => {
|
|
313
|
+
return linkModule(specifier, referencingModule.identifier);
|
|
314
|
+
});
|
|
315
|
+
return module;
|
|
316
|
+
})();
|
|
317
|
+
|
|
318
|
+
moduleCache.set(moduleUrl, modulePromise);
|
|
319
|
+
try {
|
|
320
|
+
return await modulePromise;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
moduleCache.delete(moduleUrl);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function linkModule(specifier, parentIdentifier) {
|
|
328
|
+
if (!isRelativeSpecifier(specifier)) {
|
|
329
|
+
return createSyntheticModule(specifier);
|
|
330
|
+
}
|
|
331
|
+
const resolvedUrl = await resolveRelativeSpecifier(specifier, parentIdentifier);
|
|
332
|
+
return loadFileModule(resolvedUrl);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender', 'exportPaths']);
|
|
336
|
+
const prelude = "const params = globalThis.params;\n" +
|
|
337
|
+
"const ctx = globalThis.ctx;\n" +
|
|
338
|
+
"import { download, resolveRouteResult } from 'zenith:server-contract';\n" +
|
|
339
|
+
"import { attachRouteAuth } from 'zenith:route-auth';\n" +
|
|
340
|
+
"ctx.download = download;\n" +
|
|
341
|
+
"globalThis.resolveRouteResult = resolveRouteResult;\n" +
|
|
342
|
+
"globalThis.attachRouteAuth = attachRouteAuth;\n";
|
|
343
|
+
const entryIdentifier = sourcePath
|
|
344
|
+
? pathToFileURL(sourcePath).href
|
|
345
|
+
: 'zenith:server-script';
|
|
346
|
+
const entryTranspileFilename = sourcePath && sourcePath.toLowerCase().endsWith('.zen')
|
|
347
|
+
? sourcePath.replace(/\.zen$/i, '.ts')
|
|
348
|
+
: (sourcePath || 'server-script.ts');
|
|
349
|
+
|
|
350
|
+
const entryCode = await transpileIfNeeded(entryTranspileFilename, prelude + source);
|
|
351
|
+
const entryModule = new vm.SourceTextModule(entryCode, {
|
|
352
|
+
context,
|
|
353
|
+
identifier: entryIdentifier,
|
|
354
|
+
initializeImportMeta(meta) {
|
|
355
|
+
meta.url = entryIdentifier;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
moduleCache.set(entryIdentifier, entryModule);
|
|
360
|
+
await entryModule.link((specifier, referencingModule) => {
|
|
361
|
+
if (specifier === 'zenith:server-contract') {
|
|
362
|
+
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-contract.js');
|
|
363
|
+
const configuredPath = process.env.ZENITH_SERVER_CONTRACT_PATH || '';
|
|
364
|
+
const contractUrl = pathToFileURL(configuredPath || defaultPath).href;
|
|
365
|
+
if (configuredPath) {
|
|
366
|
+
return loadFileModule(contractUrl);
|
|
367
|
+
}
|
|
368
|
+
return loadFileModule(contractUrl).catch(() =>
|
|
369
|
+
loadFileModule(pathToFileURL(defaultPath).href)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (specifier === 'zenith:route-auth') {
|
|
373
|
+
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'auth', 'route-auth.js');
|
|
374
|
+
const configuredPath = process.env.ZENITH_SERVER_ROUTE_AUTH_PATH || '';
|
|
375
|
+
const authUrl = pathToFileURL(configuredPath || defaultPath).href;
|
|
376
|
+
if (configuredPath) {
|
|
377
|
+
return loadFileModule(authUrl);
|
|
378
|
+
}
|
|
379
|
+
return loadFileModule(authUrl).catch(() =>
|
|
380
|
+
loadFileModule(pathToFileURL(defaultPath).href)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return linkModule(specifier, referencingModule.identifier);
|
|
384
|
+
});
|
|
385
|
+
await entryModule.evaluate();
|
|
386
|
+
context.attachRouteAuth(routeContext, {
|
|
387
|
+
requestUrl: routeContext.url,
|
|
388
|
+
guardOnly,
|
|
389
|
+
redirect: ctxRedirect,
|
|
390
|
+
deny: ctxDeny
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const namespaceKeys = Object.keys(entryModule.namespace);
|
|
394
|
+
for (const key of namespaceKeys) {
|
|
395
|
+
if (!allowed.has(key)) {
|
|
396
|
+
throw new Error('[zenith-preview] unsupported server export "' + key + '"');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const exported = entryModule.namespace;
|
|
401
|
+
try {
|
|
402
|
+
const resolved = await context.resolveRouteResult({
|
|
403
|
+
exports: exported,
|
|
404
|
+
ctx: context.ctx,
|
|
405
|
+
filePath: sourcePath || 'server_script',
|
|
406
|
+
guardOnly: guardOnly,
|
|
407
|
+
routeKind: routeKind
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
process.stdout.write(JSON.stringify(resolved || null));
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const message = error instanceof Error
|
|
413
|
+
? (typeof error.stack === 'string' && error.stack.length > 0 ? error.stack : error.message)
|
|
414
|
+
: String(error);
|
|
415
|
+
process.stderr.write('[Zenith:Server] preview route execution failed\\n' + message + '\\n');
|
|
416
|
+
process.stdout.write(
|
|
417
|
+
JSON.stringify({
|
|
418
|
+
__zenith_error: {
|
|
419
|
+
status: 500,
|
|
420
|
+
code: 'LOAD_FAILED'
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
`;
|