@zenithbuild/cli 0.7.10 → 0.7.12
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 +14 -2
- package/dist/adapters/adapter-netlify-static.d.ts +2 -5
- package/dist/adapters/adapter-netlify.d.ts +2 -5
- package/dist/adapters/adapter-netlify.js +22 -5
- package/dist/adapters/adapter-types.d.ts +32 -13
- package/dist/adapters/adapter-types.js +0 -59
- package/dist/adapters/adapter-vercel-static.d.ts +2 -5
- package/dist/adapters/adapter-vercel.d.ts +2 -5
- package/dist/adapters/adapter-vercel.js +21 -6
- package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
- package/dist/adapters/copy-hosted-page-runtime.js +68 -3
- package/dist/adapters/resolve-adapter.d.ts +6 -4
- package/dist/build/compiler-runtime.js +3 -0
- package/dist/build/expression-rewrites.d.ts +3 -1
- package/dist/build/expression-rewrites.js +14 -2
- package/dist/build/page-component-loop.d.ts +1 -0
- package/dist/build/page-component-loop.js +66 -6
- package/dist/build/page-ir-normalization.js +7 -0
- package/dist/build/page-loop-state.d.ts +2 -4
- package/dist/build/page-loop-state.js +17 -9
- package/dist/build/page-loop.js +18 -8
- package/dist/build/scoped-expression-context.d.ts +5 -0
- package/dist/build/scoped-expression-context.js +133 -0
- package/dist/build/server-script.js +13 -36
- package/dist/build/type-declarations.d.ts +2 -1
- package/dist/build/type-declarations.js +29 -52
- package/dist/build-output-manifest.d.ts +10 -6
- package/dist/build-output-manifest.js +4 -1
- package/dist/build.js +11 -2
- package/dist/component-instance-ir.js +1 -0
- package/dist/component-occurrences.d.ts +9 -0
- package/dist/component-occurrences.js +18 -0
- package/dist/config-plugins.d.ts +12 -0
- package/dist/config-plugins.js +100 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +56 -5
- package/dist/dev-build-session/helpers.js +27 -7
- package/dist/dev-build-session/session.js +19 -10
- package/dist/dev-server/build-error-response.d.ts +21 -0
- package/dist/dev-server/build-error-response.js +48 -0
- package/dist/dev-server/port-fallback.d.ts +15 -0
- package/dist/dev-server/port-fallback.js +61 -0
- package/dist/dev-server/request-handler.js +58 -5
- package/dist/dev-server/watcher.js +15 -0
- package/dist/dev-server.d.ts +5 -2
- package/dist/dev-server.js +129 -49
- package/dist/global-middleware-runtime-source.d.ts +15 -0
- package/dist/global-middleware-runtime-source.js +62 -0
- package/dist/global-middleware.d.ts +13 -0
- package/dist/global-middleware.js +252 -0
- package/dist/images/remote-fetch.d.ts +12 -0
- package/dist/images/remote-fetch.js +257 -0
- package/dist/images/service.d.ts +10 -0
- package/dist/images/service.js +9 -46
- package/dist/index.js +12 -2
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +70 -25
- package/dist/preview/request-handler.js +78 -5
- package/dist/preview/server-runner.d.ts +7 -2
- package/dist/preview/server-runner.js +19 -6
- package/dist/preview/server-script-runner-template.js +97 -29
- package/dist/resource-response.js +25 -8
- package/dist/resource-route-module.js +5 -22
- package/dist/route-classification.d.ts +11 -0
- package/dist/route-classification.js +21 -0
- package/dist/route-handler-export-analysis.d.ts +22 -0
- package/dist/route-handler-export-analysis.js +41 -0
- package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
- package/dist/scoped-server-data/analyze-owner-file.js +149 -0
- package/dist/scoped-server-data/diagnostics.d.ts +18 -0
- package/dist/scoped-server-data/diagnostics.js +32 -0
- package/dist/scoped-server-data/lowering.d.ts +27 -0
- package/dist/scoped-server-data/lowering.js +242 -0
- package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
- package/dist/scoped-server-data/manifest-integration.js +125 -0
- package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
- package/dist/scoped-server-data/owner-scanner.js +55 -0
- package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
- package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
- package/dist/scoped-server-data/runtime.d.ts +24 -0
- package/dist/scoped-server-data/runtime.js +121 -0
- package/dist/scoped-server-data/serialization-set.d.ts +2 -0
- package/dist/scoped-server-data/serialization-set.js +52 -0
- package/dist/scoped-server-data/static-props.d.ts +12 -0
- package/dist/scoped-server-data/static-props.js +307 -0
- package/dist/scoped-server-data/type-declarations.d.ts +10 -0
- package/dist/scoped-server-data/type-declarations.js +368 -0
- package/dist/scoped-server-data/types.d.ts +74 -0
- package/dist/scoped-server-data/types.js +1 -0
- package/dist/server-contract/auth-control-flow.d.ts +1 -0
- package/dist/server-contract/auth-control-flow.js +10 -0
- package/dist/server-contract/resolve.d.ts +19 -0
- package/dist/server-contract/resolve.js +85 -13
- package/dist/server-contract/resolved-envelope.d.ts +9 -0
- package/dist/server-contract/resolved-envelope.js +14 -0
- package/dist/server-contract/stage.js +1 -10
- package/dist/server-module-output.d.ts +9 -0
- package/dist/server-module-output.js +250 -0
- package/dist/server-output.d.ts +7 -1
- package/dist/server-output.js +144 -195
- package/dist/server-route-names.d.ts +2 -0
- package/dist/server-route-names.js +38 -0
- package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
- package/dist/server-runtime/matched-route-pipeline.js +1 -0
- package/dist/server-runtime/node-server.js +26 -3
- package/dist/server-runtime/route-render.d.ts +12 -3
- package/dist/server-runtime/route-render.js +67 -13
- package/dist/types/generate-env-dts.js +2 -44
- package/dist/types/zenith-env-dts.d.ts +4 -0
- package/dist/types/zenith-env-dts.js +96 -0
- package/package.json +3 -6
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
const PACKAGE_REQUIRE = createRequire(import.meta.url);
|
|
6
|
+
const STATIC_MIDDLEWARE_TARGETS = new Set([
|
|
7
|
+
'static',
|
|
8
|
+
'static-export',
|
|
9
|
+
'vercel-static',
|
|
10
|
+
'netlify-static'
|
|
11
|
+
]);
|
|
12
|
+
function toPosixRelative(from, to) {
|
|
13
|
+
const relativePath = relative(from, to).replaceAll('\\', '/');
|
|
14
|
+
return relativePath || '.';
|
|
15
|
+
}
|
|
16
|
+
function middlewareError(sourceFile, message) {
|
|
17
|
+
return new Error(`[Zenith:Middleware] Invalid global middleware in ${sourceFile}: ${message}`);
|
|
18
|
+
}
|
|
19
|
+
function resolveTypeScriptApi(projectRoot) {
|
|
20
|
+
try {
|
|
21
|
+
const projectRequire = createRequire(join(projectRoot, '__zenith_middleware_parser__.js'));
|
|
22
|
+
return projectRequire('typescript');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
try {
|
|
26
|
+
return PACKAGE_REQUIRE('typescript');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error('[Zenith:Middleware] Global middleware validation requires the `typescript` package to be installed.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function hasModifier(ts, node, kind) {
|
|
34
|
+
return Boolean(node?.modifiers?.some((modifier) => modifier.kind === kind));
|
|
35
|
+
}
|
|
36
|
+
function isAllowedTypeOnlyNamedExport(ts, node) {
|
|
37
|
+
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
|
|
38
|
+
return hasModifier(ts, node, ts.SyntaxKind.ExportKeyword)
|
|
39
|
+
&& !hasModifier(ts, node, ts.SyntaxKind.DefaultKeyword);
|
|
40
|
+
}
|
|
41
|
+
if (ts.isExportDeclaration(node)) {
|
|
42
|
+
if (node.isTypeOnly) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const elements = node.exportClause && ts.isNamedExports(node.exportClause)
|
|
46
|
+
? node.exportClause.elements
|
|
47
|
+
: [];
|
|
48
|
+
return elements.length > 0 && elements.every((specifier) => specifier.isTypeOnly === true);
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function unwrapExpression(ts, expression) {
|
|
53
|
+
let current = expression;
|
|
54
|
+
while (current && ts.isParenthesizedExpression(current)) {
|
|
55
|
+
current = current.expression;
|
|
56
|
+
}
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
function isFunctionLikeDefault(ts, expression) {
|
|
60
|
+
const unwrapped = unwrapExpression(ts, expression);
|
|
61
|
+
return ts.isFunctionExpression(unwrapped) || ts.isArrowFunction(unwrapped)
|
|
62
|
+
? unwrapped
|
|
63
|
+
: null;
|
|
64
|
+
}
|
|
65
|
+
function propertyAccessPath(ts, node) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
let current = node;
|
|
68
|
+
while (current && ts.isPropertyAccessExpression(current)) {
|
|
69
|
+
parts.unshift(current.name.text);
|
|
70
|
+
current = current.expression;
|
|
71
|
+
}
|
|
72
|
+
if (current && ts.isIdentifier(current)) {
|
|
73
|
+
parts.unshift(current.text);
|
|
74
|
+
}
|
|
75
|
+
return parts;
|
|
76
|
+
}
|
|
77
|
+
function hasCommonJsExport(ts, node) {
|
|
78
|
+
let found = false;
|
|
79
|
+
function visit(current) {
|
|
80
|
+
if (found) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (ts.isBinaryExpression(current)
|
|
84
|
+
&& current.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
85
|
+
&& ts.isPropertyAccessExpression(current.left)) {
|
|
86
|
+
const parts = propertyAccessPath(ts, current.left);
|
|
87
|
+
if (parts[0] === 'module' && parts[1] === 'exports') {
|
|
88
|
+
found = true;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (parts[0] === 'exports') {
|
|
92
|
+
found = true;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
ts.forEachChild(current, visit);
|
|
97
|
+
}
|
|
98
|
+
ts.forEachChild(node, visit);
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
function assertTwoNonRestParams(fn, sourceFile) {
|
|
102
|
+
const params = Array.isArray(fn?.parameters) ? fn.parameters : [];
|
|
103
|
+
const hasRest = params.some((param) => param.dotDotDotToken);
|
|
104
|
+
if (params.length !== 2 || hasRest) {
|
|
105
|
+
throw middlewareError(sourceFile, 'default function must accept exactly two arguments: ctx and next.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function validateGlobalMiddlewareSource(source, sourceFile, projectRoot = process.cwd()) {
|
|
109
|
+
const ts = resolveTypeScriptApi(projectRoot);
|
|
110
|
+
const parsed = ts.createSourceFile(sourceFile, String(source || ''), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
111
|
+
if (parsed.parseDiagnostics.length > 0) {
|
|
112
|
+
throw middlewareError(sourceFile, 'unable to parse middleware module.');
|
|
113
|
+
}
|
|
114
|
+
if (hasCommonJsExport(ts, parsed)) {
|
|
115
|
+
throw middlewareError(sourceFile, 'CommonJS middleware exports are not supported. Use `export default function middleware(ctx, next) { ... }`.');
|
|
116
|
+
}
|
|
117
|
+
let defaultExportCount = 0;
|
|
118
|
+
let defaultFunction = null;
|
|
119
|
+
let defaultExportWasNonFunction = false;
|
|
120
|
+
let hasNamedRuntimeExport = false;
|
|
121
|
+
for (const statement of parsed.statements) {
|
|
122
|
+
if (ts.isExportDeclaration(statement)) {
|
|
123
|
+
if (!isAllowedTypeOnlyNamedExport(ts, statement)) {
|
|
124
|
+
hasNamedRuntimeExport = true;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ts.isExportAssignment(statement)) {
|
|
129
|
+
if (statement.isExportEquals) {
|
|
130
|
+
throw middlewareError(sourceFile, 'CommonJS middleware exports are not supported. Use `export default function middleware(ctx, next) { ... }`.');
|
|
131
|
+
}
|
|
132
|
+
defaultExportCount += 1;
|
|
133
|
+
const fn = isFunctionLikeDefault(ts, statement.expression);
|
|
134
|
+
if (fn) {
|
|
135
|
+
defaultFunction = fn;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
defaultExportWasNonFunction = true;
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const hasExport = hasModifier(ts, statement, ts.SyntaxKind.ExportKeyword);
|
|
143
|
+
if (!hasExport) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const hasDefault = hasModifier(ts, statement, ts.SyntaxKind.DefaultKeyword);
|
|
147
|
+
if (hasDefault) {
|
|
148
|
+
defaultExportCount += 1;
|
|
149
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
150
|
+
defaultFunction = statement;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
defaultExportWasNonFunction = true;
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!isAllowedTypeOnlyNamedExport(ts, statement)) {
|
|
158
|
+
hasNamedRuntimeExport = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (hasNamedRuntimeExport) {
|
|
162
|
+
throw middlewareError(sourceFile, 'named runtime exports are not supported. Export only `default function middleware(ctx, next)`.');
|
|
163
|
+
}
|
|
164
|
+
if (defaultExportCount !== 1) {
|
|
165
|
+
throw middlewareError(sourceFile, 'expected exactly one default export function.');
|
|
166
|
+
}
|
|
167
|
+
if (!defaultFunction || defaultExportWasNonFunction) {
|
|
168
|
+
throw middlewareError(sourceFile, 'default export must be a function. Use `export default function middleware(ctx, next) { ... }`.');
|
|
169
|
+
}
|
|
170
|
+
assertTwoNonRestParams(defaultFunction, sourceFile);
|
|
171
|
+
}
|
|
172
|
+
async function findNestedMiddlewareFiles(dir, projectRoot) {
|
|
173
|
+
const matches = [];
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return matches;
|
|
180
|
+
}
|
|
181
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
182
|
+
for (const entry of entries) {
|
|
183
|
+
const fullPath = join(dir, entry.name);
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
if (entry.name === 'middleware' && existsSync(join(fullPath, 'index.ts'))) {
|
|
186
|
+
matches.push(join(fullPath, 'index.ts'));
|
|
187
|
+
}
|
|
188
|
+
matches.push(...await findNestedMiddlewareFiles(fullPath, projectRoot));
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (entry.isFile() && entry.name === 'middleware.ts') {
|
|
192
|
+
matches.push(fullPath);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return matches.sort((left, right) => (toPosixRelative(projectRoot, left).localeCompare(toPosixRelative(projectRoot, right))));
|
|
196
|
+
}
|
|
197
|
+
function createMetadata(sourceFile) {
|
|
198
|
+
return { source_file: sourceFile };
|
|
199
|
+
}
|
|
200
|
+
export function normalizeGlobalMiddlewareMetadata(globalMiddleware) {
|
|
201
|
+
const sourceFile = typeof globalMiddleware?.source_file === 'string'
|
|
202
|
+
? globalMiddleware.source_file
|
|
203
|
+
: typeof globalMiddleware?.sourceFile === 'string'
|
|
204
|
+
? globalMiddleware.sourceFile
|
|
205
|
+
: null;
|
|
206
|
+
return sourceFile ? createMetadata(sourceFile) : null;
|
|
207
|
+
}
|
|
208
|
+
export function assertGlobalMiddlewareTargetSupported(target, globalMiddleware) {
|
|
209
|
+
if (!globalMiddleware || !STATIC_MIDDLEWARE_TARGETS.has(target)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`[Zenith:Middleware] target "${target}" cannot use global middleware. ` +
|
|
213
|
+
'Global middleware requires a server-capable target ("node", "vercel", or "netlify"). ' +
|
|
214
|
+
`File: ${globalMiddleware.sourceFile}.`);
|
|
215
|
+
}
|
|
216
|
+
export async function resolveGlobalMiddleware({ projectRoot, pagesDir, target } = {}) {
|
|
217
|
+
const resolvedProjectRoot = resolve(projectRoot || process.cwd());
|
|
218
|
+
const resolvedPagesDir = resolve(resolvedProjectRoot, pagesDir || 'pages');
|
|
219
|
+
const middlewareRoot = dirname(resolvedPagesDir);
|
|
220
|
+
const rootCandidates = [
|
|
221
|
+
join(middlewareRoot, 'middleware.ts'),
|
|
222
|
+
join(middlewareRoot, 'middleware', 'index.ts')
|
|
223
|
+
].filter((candidate) => existsSync(candidate));
|
|
224
|
+
if (rootCandidates.length > 1) {
|
|
225
|
+
throw new Error(`[Zenith:Middleware] Multiple global middleware files found in "${middlewareRoot}". ` +
|
|
226
|
+
'Keep exactly one of: middleware.ts, middleware/index.ts.');
|
|
227
|
+
}
|
|
228
|
+
const nestedMatches = await findNestedMiddlewareFiles(resolvedPagesDir, resolvedProjectRoot);
|
|
229
|
+
if (nestedMatches.length > 0) {
|
|
230
|
+
const relativePath = toPosixRelative(resolvedProjectRoot, nestedMatches[0]);
|
|
231
|
+
const middlewareRootRelative = toPosixRelative(resolvedProjectRoot, middlewareRoot);
|
|
232
|
+
const targetPath = middlewareRootRelative === '.'
|
|
233
|
+
? 'middleware.ts'
|
|
234
|
+
: `${middlewareRootRelative}/middleware.ts`;
|
|
235
|
+
throw new Error('[Zenith:Middleware] Nested middleware files are not supported in V1. ' +
|
|
236
|
+
`Move "${relativePath}" to "${targetPath}" or remove it.`);
|
|
237
|
+
}
|
|
238
|
+
if (rootCandidates.length === 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const sourcePath = rootCandidates[0];
|
|
242
|
+
const sourceFile = toPosixRelative(resolvedProjectRoot, sourcePath);
|
|
243
|
+
const globalMiddleware = {
|
|
244
|
+
sourcePath,
|
|
245
|
+
sourceFile,
|
|
246
|
+
root: middlewareRoot,
|
|
247
|
+
metadata: createMetadata(sourceFile)
|
|
248
|
+
};
|
|
249
|
+
assertGlobalMiddlewareTargetSupported(target, globalMiddleware);
|
|
250
|
+
validateGlobalMiddlewareSource(await readFile(sourcePath, 'utf8'), sourceFile, resolvedProjectRoot);
|
|
251
|
+
return globalMiddleware;
|
|
252
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isLocalNetworkAddress(address: any): boolean;
|
|
2
|
+
export function resolveRemoteTarget(remoteUrl: any, config: any, lookupImpl?: typeof lookup): Promise<{
|
|
3
|
+
url: import("node:url").URL;
|
|
4
|
+
address: string;
|
|
5
|
+
family: number;
|
|
6
|
+
requestUrl: import("node:url").URL;
|
|
7
|
+
}>;
|
|
8
|
+
export function validateRemoteTarget(remoteUrl: any, config: any): Promise<import("node:url").URL>;
|
|
9
|
+
export function fetchRemoteImage(remote: any, config: any, fetchImpl?: typeof fetchPinnedRemoteUrl, lookupImpl?: typeof lookup): Promise<any>;
|
|
10
|
+
import { lookup } from 'node:dns/promises';
|
|
11
|
+
declare function fetchPinnedRemoteUrl(requestUrl: any, options?: {}): Promise<any>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import { isIP } from 'node:net';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { matchRemotePattern } from './shared.js';
|
|
7
|
+
const MAX_REMOTE_REDIRECTS = 5;
|
|
8
|
+
const PINNED_REMOTE_TARGET = Symbol('zenithPinnedRemoteTarget');
|
|
9
|
+
function parseIpv4(address) {
|
|
10
|
+
if (!address) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const parts = String(address).split('.');
|
|
14
|
+
if (parts.length !== 4) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const octets = parts.map((part) => {
|
|
18
|
+
if (!/^\d+$/.test(part)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const value = Number.parseInt(part, 10);
|
|
22
|
+
return value >= 0 && value <= 255 ? value : null;
|
|
23
|
+
});
|
|
24
|
+
return octets.every((part) => part !== null) ? octets : null;
|
|
25
|
+
}
|
|
26
|
+
function isBlockedIpv4(address) {
|
|
27
|
+
const octets = parseIpv4(address);
|
|
28
|
+
if (!octets) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const [a, b, c, d] = octets;
|
|
32
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (a === 100 && b >= 64 && b <= 127) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (a === 169 && b === 254) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (a === 192 && (b === 168 || (b === 0 && (c === 0 || c === 2)) || (b === 88 && c === 99))) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100))) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (a === 203 && b === 0 && c === 113) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return a === 255 && b === 255 && c === 255 && d === 255;
|
|
54
|
+
}
|
|
55
|
+
function leadingIpv6Hextet(address) {
|
|
56
|
+
const first = String(address).toLowerCase().replace(/^\[|\]$/g, '').split('%')[0].split(':')[0];
|
|
57
|
+
return Number.parseInt(first || '0', 16);
|
|
58
|
+
}
|
|
59
|
+
function mappedIpv4Address(address) {
|
|
60
|
+
const normalized = String(address || '').toLowerCase().replace(/^\[|\]$/g, '').split('%')[0];
|
|
61
|
+
if (normalized.includes('.')) {
|
|
62
|
+
const candidate = normalized.slice(normalized.lastIndexOf(':') + 1);
|
|
63
|
+
return parseIpv4(candidate) ? candidate : null;
|
|
64
|
+
}
|
|
65
|
+
const mappedHex = normalized.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
66
|
+
if (mappedHex) {
|
|
67
|
+
const high = Number.parseInt(mappedHex[1], 16);
|
|
68
|
+
const low = Number.parseInt(mappedHex[2], 16);
|
|
69
|
+
if (Number.isFinite(high) && Number.isFinite(low)) {
|
|
70
|
+
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function isBlockedIpv6(address) {
|
|
76
|
+
const normalized = String(address || '').toLowerCase().replace(/^\[|\]$/g, '').split('%')[0];
|
|
77
|
+
if (!normalized) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const mapped = mappedIpv4Address(normalized);
|
|
81
|
+
if (mapped) {
|
|
82
|
+
return isBlockedIpv4(mapped);
|
|
83
|
+
}
|
|
84
|
+
if (normalized === '::' || normalized === '::1') {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const first = leadingIpv6Hextet(normalized);
|
|
88
|
+
if (!Number.isFinite(first)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return (first & 0xfe00) === 0xfc00
|
|
92
|
+
|| (first & 0xffc0) === 0xfe80
|
|
93
|
+
|| (first & 0xff00) === 0xff00;
|
|
94
|
+
}
|
|
95
|
+
function normalizeHostnameAddress(hostname) {
|
|
96
|
+
return String(hostname || '').replace(/^\[|\]$/g, '').split('%')[0];
|
|
97
|
+
}
|
|
98
|
+
export function isLocalNetworkAddress(address) {
|
|
99
|
+
const normalized = normalizeHostnameAddress(address);
|
|
100
|
+
if (!normalized) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (isBlockedIpv4(normalized)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return isBlockedIpv6(normalized);
|
|
107
|
+
}
|
|
108
|
+
function isLoopbackHostname(hostname) {
|
|
109
|
+
const normalized = String(hostname || '').toLowerCase();
|
|
110
|
+
return normalized === 'localhost' || normalized.endsWith('.localhost');
|
|
111
|
+
}
|
|
112
|
+
async function resolveRemoteAddress(url, config, lookupImpl = lookup) {
|
|
113
|
+
const hostname = normalizeHostnameAddress(url.hostname);
|
|
114
|
+
const allowLocalNetwork = Boolean(config.dangerouslyAllowLocalNetwork);
|
|
115
|
+
if (!allowLocalNetwork && (isLoopbackHostname(hostname) || isLocalNetworkAddress(hostname))) {
|
|
116
|
+
throw new Error('[Zenith:Image] Loopback and local network image fetches are blocked');
|
|
117
|
+
}
|
|
118
|
+
const literalFamily = isIP(hostname);
|
|
119
|
+
if (literalFamily) {
|
|
120
|
+
return {
|
|
121
|
+
address: hostname,
|
|
122
|
+
family: literalFamily
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const resolved = await lookupImpl(hostname, { all: true });
|
|
126
|
+
if (!Array.isArray(resolved) || resolved.length === 0) {
|
|
127
|
+
throw new Error('[Zenith:Image] Remote image hostname did not resolve');
|
|
128
|
+
}
|
|
129
|
+
if (!allowLocalNetwork && resolved.some((entry) => isLocalNetworkAddress(entry.address))) {
|
|
130
|
+
throw new Error('[Zenith:Image] Private network image fetches are blocked');
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
address: resolved[0].address,
|
|
134
|
+
family: resolved[0].family || isIP(resolved[0].address)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function buildPinnedUrl(url, address, family) {
|
|
138
|
+
const pinned = new URL(url.toString());
|
|
139
|
+
pinned.hostname = family === 6 ? `[${address}]` : address;
|
|
140
|
+
return pinned;
|
|
141
|
+
}
|
|
142
|
+
export async function resolveRemoteTarget(remoteUrl, config, lookupImpl = lookup) {
|
|
143
|
+
const url = new URL(remoteUrl);
|
|
144
|
+
if (!matchRemotePattern(url, config.remotePatterns)) {
|
|
145
|
+
throw new Error('[Zenith:Image] Remote URL is not allowed by images.remotePatterns');
|
|
146
|
+
}
|
|
147
|
+
const resolved = await resolveRemoteAddress(url, config, lookupImpl);
|
|
148
|
+
return {
|
|
149
|
+
url,
|
|
150
|
+
address: resolved.address,
|
|
151
|
+
family: resolved.family,
|
|
152
|
+
requestUrl: buildPinnedUrl(url, resolved.address, resolved.family)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function remoteFetchHeaders(target) {
|
|
156
|
+
return {
|
|
157
|
+
'Accept': 'image/avif,image/webp,image/png,image/jpeg,image/*;q=0.8,*/*;q=0.1',
|
|
158
|
+
'Host': target.url.host
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function createRemoteFetchOptions(target) {
|
|
162
|
+
return {
|
|
163
|
+
headers: remoteFetchHeaders(target),
|
|
164
|
+
redirect: 'manual',
|
|
165
|
+
[PINNED_REMOTE_TARGET]: target
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function normalizeRequestHeaders(headers = {}) {
|
|
169
|
+
if (headers instanceof Headers) {
|
|
170
|
+
return Object.fromEntries(headers.entries());
|
|
171
|
+
}
|
|
172
|
+
return { ...headers };
|
|
173
|
+
}
|
|
174
|
+
function responseHeadersFromNode(headers) {
|
|
175
|
+
const out = new Headers();
|
|
176
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
for (const item of value) {
|
|
179
|
+
out.append(key, String(item));
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (value !== undefined) {
|
|
184
|
+
out.set(key, String(value));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
function nodeRequestOptions(target, options = {}) {
|
|
190
|
+
const url = target.url;
|
|
191
|
+
const protocol = url.protocol;
|
|
192
|
+
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
193
|
+
throw new Error('[Zenith:Image] Remote image protocol must be http or https');
|
|
194
|
+
}
|
|
195
|
+
const headers = normalizeRequestHeaders(options.headers);
|
|
196
|
+
const requestOptions = {
|
|
197
|
+
protocol,
|
|
198
|
+
hostname: target.address,
|
|
199
|
+
port: url.port || (protocol === 'https:' ? 443 : 80),
|
|
200
|
+
method: 'GET',
|
|
201
|
+
path: `${url.pathname}${url.search}`,
|
|
202
|
+
headers
|
|
203
|
+
};
|
|
204
|
+
const originalHostname = normalizeHostnameAddress(url.hostname);
|
|
205
|
+
if (protocol === 'https:' && !isIP(originalHostname)) {
|
|
206
|
+
requestOptions.servername = originalHostname;
|
|
207
|
+
}
|
|
208
|
+
return requestOptions;
|
|
209
|
+
}
|
|
210
|
+
async function fetchPinnedRemoteUrl(requestUrl, options = {}) {
|
|
211
|
+
const target = options[PINNED_REMOTE_TARGET];
|
|
212
|
+
if (!target) {
|
|
213
|
+
return fetch(requestUrl, options);
|
|
214
|
+
}
|
|
215
|
+
const transport = target.url.protocol === 'https:' ? https : http;
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
const request = transport.request(nodeRequestOptions(target, options), (response) => {
|
|
218
|
+
const status = response.statusCode || 502;
|
|
219
|
+
const body = status === 204 || status === 205 || status === 304
|
|
220
|
+
? null
|
|
221
|
+
: Readable.toWeb(response);
|
|
222
|
+
resolve(new Response(body, {
|
|
223
|
+
status,
|
|
224
|
+
statusText: response.statusMessage || '',
|
|
225
|
+
headers: responseHeadersFromNode(response.headers)
|
|
226
|
+
}));
|
|
227
|
+
});
|
|
228
|
+
request.on('error', reject);
|
|
229
|
+
request.end();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
export async function validateRemoteTarget(remoteUrl, config) {
|
|
233
|
+
return (await resolveRemoteTarget(remoteUrl, config)).url;
|
|
234
|
+
}
|
|
235
|
+
export async function fetchRemoteImage(remote, config, fetchImpl = fetchPinnedRemoteUrl, lookupImpl = lookup) {
|
|
236
|
+
let current = remote instanceof URL ? remote : new URL(String(remote));
|
|
237
|
+
for (let redirectCount = 0; redirectCount <= MAX_REMOTE_REDIRECTS; redirectCount += 1) {
|
|
238
|
+
const target = await resolveRemoteTarget(current.toString(), config, lookupImpl);
|
|
239
|
+
current = target.url;
|
|
240
|
+
const response = await fetchImpl(target.requestUrl, createRemoteFetchOptions(target));
|
|
241
|
+
if (response.status < 300 || response.status >= 400) {
|
|
242
|
+
return response;
|
|
243
|
+
}
|
|
244
|
+
const location = response.headers.get('location');
|
|
245
|
+
if (!location) {
|
|
246
|
+
throw new Error('[Zenith:Image] Remote image redirect is missing a Location header');
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await response.body?.cancel?.();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Ignore body cancellation errors while redirecting.
|
|
253
|
+
}
|
|
254
|
+
current = new URL(location, current);
|
|
255
|
+
}
|
|
256
|
+
throw new Error('[Zenith:Image] Remote image redirected too many times');
|
|
257
|
+
}
|
package/dist/images/service.d.ts
CHANGED
|
@@ -14,3 +14,13 @@ export function handleImageFetchRequest(request: Request | {
|
|
|
14
14
|
config?: Record<string, unknown>;
|
|
15
15
|
}): Promise<Response>;
|
|
16
16
|
export function handleImageRequest(_req: any, res: any, options: any): Promise<boolean>;
|
|
17
|
+
export namespace __imageServiceTestHooks {
|
|
18
|
+
export { fetchRemoteImage };
|
|
19
|
+
export { isLocalNetworkAddress };
|
|
20
|
+
export { resolveRemoteTarget };
|
|
21
|
+
export { validateRemoteTarget };
|
|
22
|
+
}
|
|
23
|
+
import { fetchRemoteImage } from './remote-fetch.js';
|
|
24
|
+
import { isLocalNetworkAddress } from './remote-fetch.js';
|
|
25
|
+
import { resolveRemoteTarget } from './remote-fetch.js';
|
|
26
|
+
import { validateRemoteTarget } from './remote-fetch.js';
|
package/dist/images/service.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { lookup } from 'node:dns/promises';
|
|
2
1
|
import { existsSync } from 'node:fs';
|
|
3
2
|
import { mkdir, readFile, stat, writeFile, readdir } from 'node:fs/promises';
|
|
4
3
|
import { dirname, extname, join, relative, resolve } from 'node:path';
|
|
5
4
|
import sharp from 'sharp';
|
|
6
|
-
import {
|
|
5
|
+
import { fetchRemoteImage, isLocalNetworkAddress, resolveRemoteTarget, validateRemoteTarget } from './remote-fetch.js';
|
|
6
|
+
import { buildLocalImageKey, buildLocalVariantAssetPath, normalizeImageConfig, normalizeImageFormat } from './shared.js';
|
|
7
7
|
const RASTER_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif']);
|
|
8
8
|
const MIME_BY_FORMAT = {
|
|
9
9
|
avif: 'image/avif',
|
|
@@ -12,28 +12,6 @@ const MIME_BY_FORMAT = {
|
|
|
12
12
|
jpg: 'image/jpeg',
|
|
13
13
|
jpeg: 'image/jpeg'
|
|
14
14
|
};
|
|
15
|
-
function isPrivateIp(address) {
|
|
16
|
-
if (!address) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
if (address === '::1' || address === '127.0.0.1') {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
if (address.startsWith('10.') || address.startsWith('192.168.')) {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(address)) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
if (/^(fc|fd)/i.test(address.replace(/:/g, ''))) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
function isLoopbackHostname(hostname) {
|
|
34
|
-
const normalized = String(hostname || '').toLowerCase();
|
|
35
|
-
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
|
|
36
|
-
}
|
|
37
15
|
function mimeTypeForFormat(format) {
|
|
38
16
|
return MIME_BY_FORMAT[normalizeImageFormat(format)] || 'application/octet-stream';
|
|
39
17
|
}
|
|
@@ -167,22 +145,6 @@ export async function buildImageArtifacts(options) {
|
|
|
167
145
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
168
146
|
return { manifest };
|
|
169
147
|
}
|
|
170
|
-
async function validateRemoteTarget(remoteUrl, config) {
|
|
171
|
-
const url = new URL(remoteUrl);
|
|
172
|
-
if (!matchRemotePattern(url, config.remotePatterns)) {
|
|
173
|
-
throw new Error('[Zenith:Image] Remote URL is not allowed by images.remotePatterns');
|
|
174
|
-
}
|
|
175
|
-
if (!config.dangerouslyAllowLocalNetwork) {
|
|
176
|
-
if (isLoopbackHostname(url.hostname)) {
|
|
177
|
-
throw new Error('[Zenith:Image] Loopback and local network image fetches are blocked');
|
|
178
|
-
}
|
|
179
|
-
const resolved = await lookup(url.hostname, { all: true });
|
|
180
|
-
if (resolved.some((entry) => isPrivateIp(entry.address))) {
|
|
181
|
-
throw new Error('[Zenith:Image] Private network image fetches are blocked');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return url;
|
|
185
|
-
}
|
|
186
148
|
async function readRemoteBuffer(response, maxBytes) {
|
|
187
149
|
const reader = response.body?.getReader?.();
|
|
188
150
|
if (!reader) {
|
|
@@ -273,12 +235,7 @@ async function createImageResponse(options) {
|
|
|
273
235
|
: mimeTypeForFormat(format || 'jpg');
|
|
274
236
|
return createBufferResponse(200, contentType, cached, config.minimumCacheTTL);
|
|
275
237
|
}
|
|
276
|
-
const response = await
|
|
277
|
-
headers: {
|
|
278
|
-
'Accept': 'image/avif,image/webp,image/png,image/jpeg,image/*;q=0.8,*/*;q=0.1'
|
|
279
|
-
},
|
|
280
|
-
redirect: 'follow'
|
|
281
|
-
});
|
|
238
|
+
const response = await fetchRemoteImage(remote, config);
|
|
282
239
|
if (!response.ok) {
|
|
283
240
|
throw new Error(`[Zenith:Image] Remote image fetch failed with status ${response.status}`);
|
|
284
241
|
}
|
|
@@ -330,3 +287,9 @@ export async function handleImageRequest(_req, res, options) {
|
|
|
330
287
|
await sendResponse(res, response);
|
|
331
288
|
return true;
|
|
332
289
|
}
|
|
290
|
+
export const __imageServiceTestHooks = {
|
|
291
|
+
fetchRemoteImage,
|
|
292
|
+
isLocalNetworkAddress,
|
|
293
|
+
resolveRemoteTarget,
|
|
294
|
+
validateRemoteTarget
|
|
295
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -124,8 +124,18 @@ export async function cli(args, cwd) {
|
|
|
124
124
|
: resolvePort(args.slice(1), 3000);
|
|
125
125
|
const host = process.env.ZENITH_DEV_HOST || '127.0.0.1';
|
|
126
126
|
logger.dev('Starting dev server…');
|
|
127
|
-
const dev = await createDevServer({ pagesDir, outDir, port, host, config, logger });
|
|
128
|
-
|
|
127
|
+
const dev = await createDevServer({ pagesDir, outDir, projectRoot, port, host, config, logger });
|
|
128
|
+
const displayHost = host === '0.0.0.0' ? '127.0.0.1' : host;
|
|
129
|
+
const servingUrl = `http://${displayHost}:${dev.port}`;
|
|
130
|
+
if (dev.portFallback) {
|
|
131
|
+
const occupied = Array.isArray(dev.portFallback.occupiedPorts)
|
|
132
|
+
? dev.portFallback.occupiedPorts.join(', ')
|
|
133
|
+
: String(dev.requestedPort);
|
|
134
|
+
logger.warn(`Requested port ${dev.requestedPort} is occupied; using ${dev.port}.`, {
|
|
135
|
+
hint: `Occupied port(s): ${occupied}; serving at ${servingUrl}`
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
logger.ok(servingUrl);
|
|
129
139
|
// Graceful shutdown
|
|
130
140
|
process.on('SIGINT', () => {
|
|
131
141
|
dev.close();
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export function analyzeRouteScopedServerMetadata(options: import("./scoped-server-data/types.js").AnalyzeRouteScopedServerMetadataOptions): import("./scoped-server-data/types.js").AnalyzeRouteScopedServerMetadataResult;
|
|
2
|
+
export function assertNoScopedServerBuildErrors(diagnostics: import("./scoped-server-data/types.js").ScopedServerDiagnostic[], contextFile: string): void;
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {{
|
|
3
5
|
* path: string,
|
|
@@ -11,6 +13,8 @@
|
|
|
11
13
|
* has_guard?: boolean,
|
|
12
14
|
* has_load?: boolean,
|
|
13
15
|
* has_action?: boolean,
|
|
16
|
+
* has_scoped_server_data?: boolean,
|
|
17
|
+
* scoped_server_data?: import('./scoped-server-data/types.js').ManifestScopedServerDataEntry[],
|
|
14
18
|
* export_paths?: string[]
|
|
15
19
|
* }} ManifestEntry
|
|
16
20
|
*/
|
|
@@ -19,11 +23,13 @@
|
|
|
19
23
|
*
|
|
20
24
|
* @param {string} pagesDir - Absolute path to /pages directory
|
|
21
25
|
* @param {string} [extension='.zen'] - File extension to scan for
|
|
22
|
-
* @param {{ compilerOpts?: object }} [options]
|
|
26
|
+
* @param {{ compilerOpts?: object, srcDir?: string, registry?: Map<string, string> }} [options]
|
|
23
27
|
* @returns {Promise<ManifestEntry[]>}
|
|
24
28
|
*/
|
|
25
29
|
export function generateManifest(pagesDir: string, extension?: string, options?: {
|
|
26
30
|
compilerOpts?: object;
|
|
31
|
+
srcDir?: string;
|
|
32
|
+
registry?: Map<string, string>;
|
|
27
33
|
}): Promise<ManifestEntry[]>;
|
|
28
34
|
/**
|
|
29
35
|
* Generate a JavaScript module string from manifest entries.
|
|
@@ -45,5 +51,7 @@ export type ManifestEntry = {
|
|
|
45
51
|
has_guard?: boolean;
|
|
46
52
|
has_load?: boolean;
|
|
47
53
|
has_action?: boolean;
|
|
54
|
+
has_scoped_server_data?: boolean;
|
|
55
|
+
scoped_server_data?: import("./scoped-server-data/types.js").ManifestScopedServerDataEntry[];
|
|
48
56
|
export_paths?: string[];
|
|
49
57
|
};
|