@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.
Files changed (111) hide show
  1. package/README.md +14 -2
  2. package/dist/adapters/adapter-netlify-static.d.ts +2 -5
  3. package/dist/adapters/adapter-netlify.d.ts +2 -5
  4. package/dist/adapters/adapter-netlify.js +22 -5
  5. package/dist/adapters/adapter-types.d.ts +32 -13
  6. package/dist/adapters/adapter-types.js +0 -59
  7. package/dist/adapters/adapter-vercel-static.d.ts +2 -5
  8. package/dist/adapters/adapter-vercel.d.ts +2 -5
  9. package/dist/adapters/adapter-vercel.js +21 -6
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
  11. package/dist/adapters/copy-hosted-page-runtime.js +68 -3
  12. package/dist/adapters/resolve-adapter.d.ts +6 -4
  13. package/dist/build/compiler-runtime.js +3 -0
  14. package/dist/build/expression-rewrites.d.ts +3 -1
  15. package/dist/build/expression-rewrites.js +14 -2
  16. package/dist/build/page-component-loop.d.ts +1 -0
  17. package/dist/build/page-component-loop.js +66 -6
  18. package/dist/build/page-ir-normalization.js +7 -0
  19. package/dist/build/page-loop-state.d.ts +2 -4
  20. package/dist/build/page-loop-state.js +17 -9
  21. package/dist/build/page-loop.js +18 -8
  22. package/dist/build/scoped-expression-context.d.ts +5 -0
  23. package/dist/build/scoped-expression-context.js +133 -0
  24. package/dist/build/server-script.js +13 -36
  25. package/dist/build/type-declarations.d.ts +2 -1
  26. package/dist/build/type-declarations.js +29 -52
  27. package/dist/build-output-manifest.d.ts +10 -6
  28. package/dist/build-output-manifest.js +4 -1
  29. package/dist/build.js +11 -2
  30. package/dist/component-instance-ir.js +1 -0
  31. package/dist/component-occurrences.d.ts +9 -0
  32. package/dist/component-occurrences.js +18 -0
  33. package/dist/config-plugins.d.ts +12 -0
  34. package/dist/config-plugins.js +100 -0
  35. package/dist/config.d.ts +1 -0
  36. package/dist/config.js +56 -5
  37. package/dist/dev-build-session/helpers.js +27 -7
  38. package/dist/dev-build-session/session.js +19 -10
  39. package/dist/dev-server/build-error-response.d.ts +21 -0
  40. package/dist/dev-server/build-error-response.js +48 -0
  41. package/dist/dev-server/port-fallback.d.ts +15 -0
  42. package/dist/dev-server/port-fallback.js +61 -0
  43. package/dist/dev-server/request-handler.js +58 -5
  44. package/dist/dev-server/watcher.js +15 -0
  45. package/dist/dev-server.d.ts +5 -2
  46. package/dist/dev-server.js +129 -49
  47. package/dist/global-middleware-runtime-source.d.ts +15 -0
  48. package/dist/global-middleware-runtime-source.js +62 -0
  49. package/dist/global-middleware.d.ts +13 -0
  50. package/dist/global-middleware.js +252 -0
  51. package/dist/images/remote-fetch.d.ts +12 -0
  52. package/dist/images/remote-fetch.js +257 -0
  53. package/dist/images/service.d.ts +10 -0
  54. package/dist/images/service.js +9 -46
  55. package/dist/index.js +12 -2
  56. package/dist/manifest.d.ts +9 -1
  57. package/dist/manifest.js +70 -25
  58. package/dist/preview/request-handler.js +78 -5
  59. package/dist/preview/server-runner.d.ts +7 -2
  60. package/dist/preview/server-runner.js +19 -6
  61. package/dist/preview/server-script-runner-template.js +97 -29
  62. package/dist/resource-response.js +25 -8
  63. package/dist/resource-route-module.js +5 -22
  64. package/dist/route-classification.d.ts +11 -0
  65. package/dist/route-classification.js +21 -0
  66. package/dist/route-handler-export-analysis.d.ts +22 -0
  67. package/dist/route-handler-export-analysis.js +41 -0
  68. package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
  69. package/dist/scoped-server-data/analyze-owner-file.js +149 -0
  70. package/dist/scoped-server-data/diagnostics.d.ts +18 -0
  71. package/dist/scoped-server-data/diagnostics.js +32 -0
  72. package/dist/scoped-server-data/lowering.d.ts +27 -0
  73. package/dist/scoped-server-data/lowering.js +242 -0
  74. package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
  75. package/dist/scoped-server-data/manifest-integration.js +125 -0
  76. package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
  77. package/dist/scoped-server-data/owner-scanner.js +55 -0
  78. package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
  79. package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
  80. package/dist/scoped-server-data/runtime.d.ts +24 -0
  81. package/dist/scoped-server-data/runtime.js +121 -0
  82. package/dist/scoped-server-data/serialization-set.d.ts +2 -0
  83. package/dist/scoped-server-data/serialization-set.js +52 -0
  84. package/dist/scoped-server-data/static-props.d.ts +12 -0
  85. package/dist/scoped-server-data/static-props.js +307 -0
  86. package/dist/scoped-server-data/type-declarations.d.ts +10 -0
  87. package/dist/scoped-server-data/type-declarations.js +368 -0
  88. package/dist/scoped-server-data/types.d.ts +74 -0
  89. package/dist/scoped-server-data/types.js +1 -0
  90. package/dist/server-contract/auth-control-flow.d.ts +1 -0
  91. package/dist/server-contract/auth-control-flow.js +10 -0
  92. package/dist/server-contract/resolve.d.ts +19 -0
  93. package/dist/server-contract/resolve.js +85 -13
  94. package/dist/server-contract/resolved-envelope.d.ts +9 -0
  95. package/dist/server-contract/resolved-envelope.js +14 -0
  96. package/dist/server-contract/stage.js +1 -10
  97. package/dist/server-module-output.d.ts +9 -0
  98. package/dist/server-module-output.js +250 -0
  99. package/dist/server-output.d.ts +7 -1
  100. package/dist/server-output.js +144 -195
  101. package/dist/server-route-names.d.ts +2 -0
  102. package/dist/server-route-names.js +38 -0
  103. package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
  104. package/dist/server-runtime/matched-route-pipeline.js +1 -0
  105. package/dist/server-runtime/node-server.js +26 -3
  106. package/dist/server-runtime/route-render.d.ts +12 -3
  107. package/dist/server-runtime/route-render.js +67 -13
  108. package/dist/types/generate-env-dts.js +2 -44
  109. package/dist/types/zenith-env-dts.d.ts +4 -0
  110. package/dist/types/zenith-env-dts.js +96 -0
  111. 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
+ }
@@ -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';
@@ -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 { buildLocalImageKey, buildLocalVariantAssetPath, matchRemotePattern, normalizeImageConfig, normalizeImageFormat } from './shared.js';
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 fetch(remote, {
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
- logger.ok(`http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${dev.port}`);
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();
@@ -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
  };