@zenithbuild/cli 0.7.0 → 0.7.2
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 +3 -2
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
const PACKAGE_REQUIRE = createRequire(import.meta.url);
|
|
7
|
+
const RELATIVE_SPECIFIER_RE = /((?:import|export)\s+(?:[^'"]*?\s+from\s+)?|import\s*\()\s*(['"])([^'"]+)\2/g;
|
|
8
|
+
const SERVER_RUNTIME_FILES = [
|
|
9
|
+
{
|
|
10
|
+
from: new URL('./server-runtime/route-render.js', import.meta.url),
|
|
11
|
+
to: 'runtime/route-render.js'
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
from: new URL('./server-contract.js', import.meta.url),
|
|
15
|
+
to: 'server-contract.js'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
from: new URL('./base-path.js', import.meta.url),
|
|
19
|
+
to: 'base-path.js'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
from: new URL('./images/materialize.js', import.meta.url),
|
|
23
|
+
to: 'images/materialize.js'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
from: new URL('./images/payload.js', import.meta.url),
|
|
27
|
+
to: 'images/payload.js'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
from: new URL('./images/shared.js', import.meta.url),
|
|
31
|
+
to: 'images/shared.js'
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
function normalizeRouteName(routePath) {
|
|
35
|
+
if (routePath === '/') {
|
|
36
|
+
return 'index';
|
|
37
|
+
}
|
|
38
|
+
return routePath
|
|
39
|
+
.replace(/^\//, '')
|
|
40
|
+
.replace(/\/+/g, '_')
|
|
41
|
+
.replace(/:/g, 'param_')
|
|
42
|
+
.replace(/\*/g, 'splat_')
|
|
43
|
+
.replace(/\?/g, 'opt')
|
|
44
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
45
|
+
.replace(/_+/g, '_')
|
|
46
|
+
.replace(/^_+|_+$/g, '');
|
|
47
|
+
}
|
|
48
|
+
function resolveTypeScriptApi(projectRoot) {
|
|
49
|
+
try {
|
|
50
|
+
const projectRequire = createRequire(join(projectRoot, '__zenith_server_output_loader__.js'));
|
|
51
|
+
return projectRequire('typescript');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
try {
|
|
55
|
+
return PACKAGE_REQUIRE('typescript');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error('[Zenith:Build] Server-capable targets require the `typescript` package to transpile route modules.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function withJsExtension(specifier) {
|
|
63
|
+
if (specifier.endsWith('.json')) {
|
|
64
|
+
return specifier;
|
|
65
|
+
}
|
|
66
|
+
return specifier.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
|
|
67
|
+
}
|
|
68
|
+
function replaceSpecifier(source, original, nextValue) {
|
|
69
|
+
return source.replace(new RegExp(`(['"])${escapeRegex(original)}\\1`, 'g'), (_, quote) => `${quote}${nextValue}${quote}`);
|
|
70
|
+
}
|
|
71
|
+
function escapeRegex(value) {
|
|
72
|
+
return String(value).replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
|
|
73
|
+
}
|
|
74
|
+
function isRelativeSpecifier(specifier) {
|
|
75
|
+
return (specifier.startsWith('./') ||
|
|
76
|
+
specifier.startsWith('../') ||
|
|
77
|
+
specifier.startsWith('/') ||
|
|
78
|
+
specifier.startsWith('file:'));
|
|
79
|
+
}
|
|
80
|
+
function resolveModuleCandidates(basePath) {
|
|
81
|
+
if (extname(basePath)) {
|
|
82
|
+
return [basePath];
|
|
83
|
+
}
|
|
84
|
+
return [
|
|
85
|
+
basePath,
|
|
86
|
+
`${basePath}.ts`,
|
|
87
|
+
`${basePath}.tsx`,
|
|
88
|
+
`${basePath}.mts`,
|
|
89
|
+
`${basePath}.cts`,
|
|
90
|
+
`${basePath}.js`,
|
|
91
|
+
`${basePath}.mjs`,
|
|
92
|
+
`${basePath}.cjs`,
|
|
93
|
+
`${basePath}.json`,
|
|
94
|
+
join(basePath, 'index.ts'),
|
|
95
|
+
join(basePath, 'index.tsx'),
|
|
96
|
+
join(basePath, 'index.mts'),
|
|
97
|
+
join(basePath, 'index.cts'),
|
|
98
|
+
join(basePath, 'index.js'),
|
|
99
|
+
join(basePath, 'index.mjs'),
|
|
100
|
+
join(basePath, 'index.cjs'),
|
|
101
|
+
join(basePath, 'index.json')
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
function resolveImportedModule(specifier, sourceFile) {
|
|
105
|
+
if (!isRelativeSpecifier(specifier)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const baseDir = dirname(sourceFile);
|
|
109
|
+
const basePath = specifier.startsWith('file:')
|
|
110
|
+
? new URL(specifier)
|
|
111
|
+
: resolve(baseDir, specifier);
|
|
112
|
+
const filePath = basePath instanceof URL ? fileURLToPath(basePath) : basePath;
|
|
113
|
+
const candidates = resolveModuleCandidates(filePath);
|
|
114
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
115
|
+
if (!found) {
|
|
116
|
+
throw new Error(`[Zenith:Build] Cannot resolve server import "${specifier}" from "${sourceFile}"`);
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
function gatherSpecifiers(source) {
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const match of source.matchAll(RELATIVE_SPECIFIER_RE)) {
|
|
123
|
+
const specifier = String(match[3] || '');
|
|
124
|
+
results.push(specifier);
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
function transpileSource(ts, source, filePath) {
|
|
129
|
+
return ts.transpileModule(source, {
|
|
130
|
+
compilerOptions: {
|
|
131
|
+
module: ts.ModuleKind.ESNext,
|
|
132
|
+
target: ts.ScriptTarget.ES2022,
|
|
133
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
134
|
+
esModuleInterop: true,
|
|
135
|
+
allowSyntheticDefaultImports: true
|
|
136
|
+
},
|
|
137
|
+
fileName: filePath
|
|
138
|
+
}).outputText;
|
|
139
|
+
}
|
|
140
|
+
function outputPathForSource(projectRoot, modulesRoot, sourcePath) {
|
|
141
|
+
const relativePath = relative(projectRoot, sourcePath).replaceAll('\\', '/');
|
|
142
|
+
const nextRelative = extname(relativePath) === '.json'
|
|
143
|
+
? relativePath
|
|
144
|
+
: relativePath.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
|
|
145
|
+
return join(modulesRoot, nextRelative);
|
|
146
|
+
}
|
|
147
|
+
async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts, seen }) {
|
|
148
|
+
if (seen.has(sourcePath)) {
|
|
149
|
+
return outputPathForSource(projectRoot, modulesRoot, sourcePath);
|
|
150
|
+
}
|
|
151
|
+
seen.add(sourcePath);
|
|
152
|
+
const outPath = outputPathForSource(projectRoot, modulesRoot, sourcePath);
|
|
153
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
154
|
+
if (extname(sourcePath) === '.json') {
|
|
155
|
+
await cp(sourcePath, outPath, { force: true });
|
|
156
|
+
return outPath;
|
|
157
|
+
}
|
|
158
|
+
const source = await readFile(sourcePath, 'utf8');
|
|
159
|
+
let output = transpileSource(ts, source, sourcePath);
|
|
160
|
+
for (const specifier of gatherSpecifiers(output)) {
|
|
161
|
+
if (!isRelativeSpecifier(specifier)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const resolvedPath = resolveImportedModule(specifier, sourcePath);
|
|
165
|
+
if (!resolvedPath) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const compiledDependencyPath = await compileImportedModule({
|
|
169
|
+
projectRoot,
|
|
170
|
+
modulesRoot,
|
|
171
|
+
sourcePath: resolvedPath,
|
|
172
|
+
ts,
|
|
173
|
+
seen
|
|
174
|
+
});
|
|
175
|
+
const nextSpecifier = relative(dirname(outPath), compiledDependencyPath).replaceAll('\\', '/');
|
|
176
|
+
output = replaceSpecifier(output, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
|
|
177
|
+
}
|
|
178
|
+
await writeFile(outPath, output, 'utf8');
|
|
179
|
+
return outPath;
|
|
180
|
+
}
|
|
181
|
+
async function writeRouteModulePackage({ projectRoot, routeDir, route }) {
|
|
182
|
+
const ts = resolveTypeScriptApi(projectRoot);
|
|
183
|
+
const modulesRoot = join(routeDir, 'modules');
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
let entryOutput = transpileSource(ts, route.server_script || '', route.server_script_path || 'route-entry.ts');
|
|
186
|
+
for (const specifier of gatherSpecifiers(entryOutput)) {
|
|
187
|
+
if (!isRelativeSpecifier(specifier)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const resolvedPath = resolveImportedModule(specifier, route.server_script_path || projectRoot);
|
|
191
|
+
if (!resolvedPath) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const compiledDependencyPath = await compileImportedModule({
|
|
195
|
+
projectRoot,
|
|
196
|
+
modulesRoot,
|
|
197
|
+
sourcePath: resolvedPath,
|
|
198
|
+
ts,
|
|
199
|
+
seen
|
|
200
|
+
});
|
|
201
|
+
const nextSpecifier = relative(join(routeDir, 'route'), compiledDependencyPath).replaceAll('\\', '/');
|
|
202
|
+
entryOutput = replaceSpecifier(entryOutput, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
|
|
203
|
+
}
|
|
204
|
+
const routeModulePath = join(routeDir, 'route', 'entry.js');
|
|
205
|
+
await mkdir(dirname(routeModulePath), { recursive: true });
|
|
206
|
+
await writeFile(routeModulePath, entryOutput, 'utf8');
|
|
207
|
+
}
|
|
208
|
+
async function copyRuntimeFiles(serverDir) {
|
|
209
|
+
for (const file of SERVER_RUNTIME_FILES) {
|
|
210
|
+
const targetPath = join(serverDir, file.to);
|
|
211
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
212
|
+
await cp(file.from, targetPath, { force: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function copyOptionalFile(sourcePath, targetPath) {
|
|
216
|
+
if (!sourcePath || !existsSync(sourcePath)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
220
|
+
await cp(sourcePath, targetPath, { force: true });
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot, config, basePath = '/' }) {
|
|
224
|
+
const serverDir = join(coreOutputDir, 'server');
|
|
225
|
+
await rm(serverDir, { recursive: true, force: true });
|
|
226
|
+
let routerManifest = { routes: [] };
|
|
227
|
+
try {
|
|
228
|
+
routerManifest = JSON.parse(await readFile(join(staticDir, 'assets', 'router-manifest.json'), 'utf8'));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
routerManifest = { routes: [] };
|
|
232
|
+
}
|
|
233
|
+
const routes = Array.isArray(routerManifest.routes) ? routerManifest.routes : [];
|
|
234
|
+
const serverRoutes = routes.filter((route) => route.server_script && route.prerender !== true);
|
|
235
|
+
await mkdir(serverDir, { recursive: true });
|
|
236
|
+
await copyRuntimeFiles(serverDir);
|
|
237
|
+
const imageManifestSource = join(staticDir, '_zenith', 'image', 'manifest.json');
|
|
238
|
+
const emittedRoutes = [];
|
|
239
|
+
for (const route of serverRoutes) {
|
|
240
|
+
const name = normalizeRouteName(route.path);
|
|
241
|
+
const routeDir = join(serverDir, 'routes', name);
|
|
242
|
+
await mkdir(routeDir, { recursive: true });
|
|
243
|
+
const htmlSourcePath = join(staticDir, String(route.output || '').replace(/^\//, ''));
|
|
244
|
+
await copyOptionalFile(htmlSourcePath, join(routeDir, 'route', 'page.html'));
|
|
245
|
+
let pageAssetFile = null;
|
|
246
|
+
if (typeof route.page_asset === 'string' && route.page_asset.length > 0) {
|
|
247
|
+
const assetSourcePath = join(staticDir, route.page_asset.replace(/^\//, ''));
|
|
248
|
+
const assetFileName = basename(assetSourcePath);
|
|
249
|
+
if (await copyOptionalFile(assetSourcePath, join(routeDir, 'route', assetFileName))) {
|
|
250
|
+
pageAssetFile = assetFileName;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let imageManifestFile = null;
|
|
254
|
+
if (await copyOptionalFile(imageManifestSource, join(routeDir, 'route', 'image-manifest.json'))) {
|
|
255
|
+
imageManifestFile = 'image-manifest.json';
|
|
256
|
+
}
|
|
257
|
+
await writeRouteModulePackage({
|
|
258
|
+
projectRoot,
|
|
259
|
+
routeDir,
|
|
260
|
+
route
|
|
261
|
+
});
|
|
262
|
+
const meta = {
|
|
263
|
+
name,
|
|
264
|
+
path: route.path,
|
|
265
|
+
output: route.output,
|
|
266
|
+
base_path: basePath,
|
|
267
|
+
page_asset: route.page_asset || null,
|
|
268
|
+
page_asset_file: pageAssetFile,
|
|
269
|
+
route_id: route.route_id || null,
|
|
270
|
+
server_script_path: route.server_script_path || null,
|
|
271
|
+
guard_module_ref: route.guard_module_ref || null,
|
|
272
|
+
load_module_ref: route.load_module_ref || null,
|
|
273
|
+
has_guard: route.has_guard === true,
|
|
274
|
+
has_load: route.has_load === true,
|
|
275
|
+
params: extractRouteParams(route.path),
|
|
276
|
+
image_manifest_file: imageManifestFile,
|
|
277
|
+
image_config: config?.images || {}
|
|
278
|
+
};
|
|
279
|
+
await writeFile(join(routeDir, 'route.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
280
|
+
emittedRoutes.push(meta);
|
|
281
|
+
}
|
|
282
|
+
await writeFile(join(serverDir, 'manifest.json'), `${JSON.stringify({ base_path: basePath, routes: emittedRoutes }, null, 2)}\n`, 'utf8');
|
|
283
|
+
return {
|
|
284
|
+
serverDir,
|
|
285
|
+
routes: emittedRoutes
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function extractRouteParams(routePath) {
|
|
289
|
+
return String(routePath || '')
|
|
290
|
+
.split('/')
|
|
291
|
+
.filter(Boolean)
|
|
292
|
+
.filter((segment) => segment.startsWith(':') || segment.startsWith('*'))
|
|
293
|
+
.map((segment) => {
|
|
294
|
+
const raw = segment.slice(1);
|
|
295
|
+
return raw.endsWith('?') ? raw.slice(0, -1) : raw;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { access, readFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, extname, join, normalize, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from '../base-path.js';
|
|
7
|
+
import { handleImageRequest } from '../images/service.js';
|
|
8
|
+
import { executeRouteRequest, renderRouteRequest } from './route-render.js';
|
|
9
|
+
import { resolveRequestRoute } from './resolve-request-route.js';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const MIME_TYPES = {
|
|
13
|
+
'.html': 'text/html; charset=utf-8',
|
|
14
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
15
|
+
'.css': 'text/css; charset=utf-8',
|
|
16
|
+
'.json': 'application/json; charset=utf-8',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.jpeg': 'image/jpeg',
|
|
19
|
+
'.jpg': 'image/jpeg',
|
|
20
|
+
'.svg': 'image/svg+xml',
|
|
21
|
+
'.webp': 'image/webp',
|
|
22
|
+
'.avif': 'image/avif',
|
|
23
|
+
'.gif': 'image/gif',
|
|
24
|
+
'.txt': 'text/plain; charset=utf-8'
|
|
25
|
+
};
|
|
26
|
+
let runtimeContextPromise = null;
|
|
27
|
+
async function fileExists(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
await access(filePath);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function readJson(filePath, fallback) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveWithinRoot(rootDir, requestPath) {
|
|
45
|
+
let decoded = requestPath;
|
|
46
|
+
try {
|
|
47
|
+
decoded = decodeURIComponent(requestPath);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const normalized = normalize(decoded).replace(/\\/g, '/');
|
|
53
|
+
const relativePath = normalized.replace(/^\/+/, '');
|
|
54
|
+
const root = resolve(rootDir);
|
|
55
|
+
const candidate = resolve(root, relativePath);
|
|
56
|
+
if (candidate === root || candidate.startsWith(`${root}${sep}`)) {
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function toStaticFilePath(staticDir, pathname) {
|
|
62
|
+
let resolvedPath = pathname;
|
|
63
|
+
if (resolvedPath === '/') {
|
|
64
|
+
resolvedPath = '/index.html';
|
|
65
|
+
}
|
|
66
|
+
else if (!extname(resolvedPath)) {
|
|
67
|
+
resolvedPath += '/index.html';
|
|
68
|
+
}
|
|
69
|
+
return resolveWithinRoot(staticDir, resolvedPath);
|
|
70
|
+
}
|
|
71
|
+
function publicHost(host) {
|
|
72
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
73
|
+
return '127.0.0.1';
|
|
74
|
+
}
|
|
75
|
+
return host;
|
|
76
|
+
}
|
|
77
|
+
function createRequestBase(req, fallbackOrigin) {
|
|
78
|
+
if (typeof req.headers.host === 'string' && req.headers.host.length > 0) {
|
|
79
|
+
return `http://${req.headers.host}`;
|
|
80
|
+
}
|
|
81
|
+
return fallbackOrigin;
|
|
82
|
+
}
|
|
83
|
+
async function createWebRequest(req, url) {
|
|
84
|
+
const init = {
|
|
85
|
+
method: req.method || 'GET',
|
|
86
|
+
headers: new Headers()
|
|
87
|
+
};
|
|
88
|
+
for (const [key, rawValue] of Object.entries(req.headers || {})) {
|
|
89
|
+
if (Array.isArray(rawValue)) {
|
|
90
|
+
for (const value of rawValue) {
|
|
91
|
+
init.headers.append(key, String(value));
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (rawValue !== undefined) {
|
|
96
|
+
init.headers.set(key, String(rawValue));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const method = String(init.method || 'GET').toUpperCase();
|
|
100
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
101
|
+
const bodyChunks = [];
|
|
102
|
+
for await (const chunk of req) {
|
|
103
|
+
bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
104
|
+
}
|
|
105
|
+
init.body = Readable.toWeb(Readable.from(bodyChunks));
|
|
106
|
+
init.duplex = 'half';
|
|
107
|
+
}
|
|
108
|
+
return new Request(url.toString(), init);
|
|
109
|
+
}
|
|
110
|
+
async function sendFetchResponse(res, response, method) {
|
|
111
|
+
res.statusCode = response.status;
|
|
112
|
+
for (const [key, value] of response.headers.entries()) {
|
|
113
|
+
res.setHeader(key, value);
|
|
114
|
+
}
|
|
115
|
+
if (String(method || 'GET').toUpperCase() === 'HEAD') {
|
|
116
|
+
res.end();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const body = await response.arrayBuffer();
|
|
120
|
+
res.end(Buffer.from(body));
|
|
121
|
+
}
|
|
122
|
+
async function sendStaticFile(res, filePath, method) {
|
|
123
|
+
const body = await readFile(filePath);
|
|
124
|
+
const contentType = MIME_TYPES[extname(filePath)] || 'application/octet-stream';
|
|
125
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
126
|
+
if (String(method || 'GET').toUpperCase() === 'HEAD') {
|
|
127
|
+
res.end();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
res.end(body);
|
|
131
|
+
}
|
|
132
|
+
function normalizeRouteCheckResult(result, targetUrl, basePath) {
|
|
133
|
+
if (!result || result.kind !== 'redirect') {
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
const location = appLocalRedirectLocation(result.location, basePath);
|
|
137
|
+
if (location.includes('://') || location.startsWith('//')) {
|
|
138
|
+
try {
|
|
139
|
+
const redirectUrl = new URL(location);
|
|
140
|
+
if (redirectUrl.origin !== targetUrl.origin) {
|
|
141
|
+
return { ...result, location: appLocalRedirectLocation('/', basePath) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return { ...result, location: appLocalRedirectLocation('/', basePath) };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { ...result, location };
|
|
149
|
+
}
|
|
150
|
+
async function loadRuntimeContext(options = {}) {
|
|
151
|
+
if (!options.distDir && runtimeContextPromise) {
|
|
152
|
+
return runtimeContextPromise;
|
|
153
|
+
}
|
|
154
|
+
const load = async () => {
|
|
155
|
+
const distDir = options.distDir ? resolve(options.distDir) : resolve(__dirname, '..', '..');
|
|
156
|
+
const serverDir = join(distDir, 'server');
|
|
157
|
+
const config = await readJson(join(serverDir, 'config.json'), {
|
|
158
|
+
base_path: '/',
|
|
159
|
+
static_dir: '../static',
|
|
160
|
+
build_manifest: '../manifest.json',
|
|
161
|
+
images: {}
|
|
162
|
+
});
|
|
163
|
+
const buildManifest = await readJson(join(serverDir, config.build_manifest || '../manifest.json'), {
|
|
164
|
+
routes: [],
|
|
165
|
+
base_path: '/'
|
|
166
|
+
});
|
|
167
|
+
const serverManifest = await readJson(join(serverDir, 'manifest.json'), { routes: [] });
|
|
168
|
+
return {
|
|
169
|
+
distDir,
|
|
170
|
+
serverDir,
|
|
171
|
+
staticDir: resolve(serverDir, config.static_dir || '../static'),
|
|
172
|
+
buildManifest,
|
|
173
|
+
buildRoutes: Array.isArray(buildManifest.routes) ? buildManifest.routes : [],
|
|
174
|
+
serverRoutes: Array.isArray(serverManifest.routes) ? serverManifest.routes : [],
|
|
175
|
+
images: config.images || {},
|
|
176
|
+
basePath: normalizeBasePath(config.base_path || '/')
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
const promise = load();
|
|
180
|
+
if (!options.distDir) {
|
|
181
|
+
runtimeContextPromise = promise;
|
|
182
|
+
}
|
|
183
|
+
return promise;
|
|
184
|
+
}
|
|
185
|
+
async function handleRouteCheck(req, res, url, context) {
|
|
186
|
+
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
187
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ error: 'forbidden', message: 'invalid request context' }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const targetPath = String(url.searchParams.get('path') || '/');
|
|
192
|
+
if (targetPath.includes('://') || targetPath.startsWith('//') || /[\r\n]/.test(targetPath)) {
|
|
193
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
194
|
+
res.end(JSON.stringify({ error: 'invalid_path_format' }));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const targetUrl = new URL(targetPath, url.origin);
|
|
198
|
+
if (targetUrl.origin !== url.origin) {
|
|
199
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
200
|
+
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const canonicalPath = stripBasePath(targetUrl.pathname, context.basePath);
|
|
204
|
+
if (canonicalPath === null) {
|
|
205
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
206
|
+
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
210
|
+
canonicalTargetUrl.pathname = canonicalPath;
|
|
211
|
+
const buildResolved = resolveRequestRoute(canonicalTargetUrl, context.buildRoutes);
|
|
212
|
+
if (!buildResolved.matched || !buildResolved.route) {
|
|
213
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
214
|
+
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let result = { kind: 'allow' };
|
|
218
|
+
let routeId = buildResolved.route.path || '';
|
|
219
|
+
const serverResolved = resolveRequestRoute(canonicalTargetUrl, context.serverRoutes);
|
|
220
|
+
if (serverResolved.matched && serverResolved.route) {
|
|
221
|
+
routeId = serverResolved.route.route_id || serverResolved.route.name || serverResolved.route.path || routeId;
|
|
222
|
+
try {
|
|
223
|
+
const request = await createWebRequest(req, targetUrl);
|
|
224
|
+
const routeDir = join(context.serverDir, 'routes', serverResolved.route.name);
|
|
225
|
+
const execution = await executeRouteRequest({
|
|
226
|
+
request,
|
|
227
|
+
route: serverResolved.route,
|
|
228
|
+
params: serverResolved.params,
|
|
229
|
+
routeModulePath: join(routeDir, 'route', 'entry.js'),
|
|
230
|
+
guardOnly: true
|
|
231
|
+
});
|
|
232
|
+
result = normalizeRouteCheckResult(execution.result, targetUrl, context.basePath);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
result = {
|
|
236
|
+
kind: 'deny',
|
|
237
|
+
status: 500,
|
|
238
|
+
message: String(error)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
res.writeHead(200, {
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
245
|
+
Pragma: 'no-cache',
|
|
246
|
+
Expires: '0',
|
|
247
|
+
Vary: 'Cookie'
|
|
248
|
+
});
|
|
249
|
+
res.end(JSON.stringify({
|
|
250
|
+
result,
|
|
251
|
+
routeId,
|
|
252
|
+
to: targetUrl.toString()
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
async function handleNodeRequest(req, res, context, serverOrigin) {
|
|
256
|
+
const url = new URL(req.url || '/', createRequestBase(req, serverOrigin));
|
|
257
|
+
const canonicalPath = stripBasePath(url.pathname, context.basePath);
|
|
258
|
+
if (url.pathname === routeCheckPath(context.basePath)) {
|
|
259
|
+
await handleRouteCheck(req, res, url, context);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (url.pathname === imageEndpointPath(context.basePath)) {
|
|
263
|
+
await handleImageRequest(req, res, {
|
|
264
|
+
requestUrl: url,
|
|
265
|
+
projectRoot: context.distDir,
|
|
266
|
+
config: context.images
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (canonicalPath === null) {
|
|
271
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
272
|
+
res.end('404 Not Found');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const canonicalUrl = new URL(url.toString());
|
|
276
|
+
canonicalUrl.pathname = canonicalPath;
|
|
277
|
+
if (extname(canonicalPath) && extname(canonicalPath) !== '.html') {
|
|
278
|
+
const assetPath = resolveWithinRoot(context.staticDir, canonicalPath);
|
|
279
|
+
if (!assetPath || !(await fileExists(assetPath))) {
|
|
280
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
281
|
+
res.end('404 Not Found');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await sendStaticFile(res, assetPath, req.method);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const serverResolved = resolveRequestRoute(canonicalUrl, context.serverRoutes);
|
|
288
|
+
if (serverResolved.matched && serverResolved.route) {
|
|
289
|
+
const routeDir = join(context.serverDir, 'routes', serverResolved.route.name);
|
|
290
|
+
const request = await createWebRequest(req, url);
|
|
291
|
+
const response = await renderRouteRequest({
|
|
292
|
+
request,
|
|
293
|
+
route: serverResolved.route,
|
|
294
|
+
params: serverResolved.params,
|
|
295
|
+
routeModulePath: join(routeDir, 'route', 'entry.js'),
|
|
296
|
+
shellHtmlPath: join(routeDir, 'route', 'page.html'),
|
|
297
|
+
pageAssetPath: serverResolved.route.page_asset_file
|
|
298
|
+
? join(routeDir, 'route', serverResolved.route.page_asset_file)
|
|
299
|
+
: null,
|
|
300
|
+
imageManifestPath: serverResolved.route.image_manifest_file
|
|
301
|
+
? join(routeDir, 'route', serverResolved.route.image_manifest_file)
|
|
302
|
+
: null,
|
|
303
|
+
imageConfig: serverResolved.route.image_config || context.images
|
|
304
|
+
});
|
|
305
|
+
await sendFetchResponse(res, response, req.method);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const buildResolved = resolveRequestRoute(canonicalUrl, context.buildRoutes);
|
|
309
|
+
if (buildResolved.matched && buildResolved.route) {
|
|
310
|
+
const htmlPath = resolveWithinRoot(context.staticDir, buildResolved.route.html);
|
|
311
|
+
if (htmlPath && await fileExists(htmlPath)) {
|
|
312
|
+
await sendStaticFile(res, htmlPath, req.method);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const fallbackHtmlPath = toStaticFilePath(context.staticDir, canonicalPath);
|
|
317
|
+
if (fallbackHtmlPath && await fileExists(fallbackHtmlPath)) {
|
|
318
|
+
await sendStaticFile(res, fallbackHtmlPath, req.method);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
322
|
+
res.end('404 Not Found');
|
|
323
|
+
}
|
|
324
|
+
export async function createRequestHandler(options = {}) {
|
|
325
|
+
const context = await loadRuntimeContext(options);
|
|
326
|
+
const host = publicHost(options.host || '127.0.0.1');
|
|
327
|
+
const port = Number.isInteger(options.port) ? options.port : 3000;
|
|
328
|
+
const serverOrigin = `http://${host}:${port}`;
|
|
329
|
+
return async (req, res) => {
|
|
330
|
+
try {
|
|
331
|
+
await handleNodeRequest(req, res, context, serverOrigin);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
335
|
+
res.end(String(error));
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
export async function createNodeServer(options = {}) {
|
|
340
|
+
const { port = 3000, host = '127.0.0.1' } = options;
|
|
341
|
+
const handler = await createRequestHandler({ ...options, port, host });
|
|
342
|
+
const server = createServer((req, res) => {
|
|
343
|
+
void handler(req, res);
|
|
344
|
+
});
|
|
345
|
+
return new Promise((resolveServer) => {
|
|
346
|
+
server.listen(port, host, () => {
|
|
347
|
+
resolveServer({
|
|
348
|
+
server,
|
|
349
|
+
port: server.address().port,
|
|
350
|
+
close: () => server.close()
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|