@zenithbuild/cli 0.7.4 → 0.7.5

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 (58) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/adapter-netlify.d.ts +1 -1
  3. package/dist/adapters/adapter-netlify.js +56 -14
  4. package/dist/adapters/adapter-static-export.d.ts +5 -0
  5. package/dist/adapters/adapter-static-export.js +115 -0
  6. package/dist/adapters/adapter-types.d.ts +3 -1
  7. package/dist/adapters/adapter-types.js +5 -2
  8. package/dist/adapters/adapter-vercel.d.ts +1 -1
  9. package/dist/adapters/adapter-vercel.js +70 -14
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  11. package/dist/adapters/copy-hosted-page-runtime.js +49 -0
  12. package/dist/adapters/resolve-adapter.js +4 -0
  13. package/dist/adapters/route-rules.d.ts +5 -0
  14. package/dist/adapters/route-rules.js +9 -0
  15. package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
  16. package/dist/adapters/validate-hosted-resource-routes.js +13 -0
  17. package/dist/auth/route-auth.d.ts +6 -0
  18. package/dist/auth/route-auth.js +236 -0
  19. package/dist/build/compiler-runtime.d.ts +1 -1
  20. package/dist/build/compiler-runtime.js +8 -2
  21. package/dist/build/page-loop-state.js +1 -1
  22. package/dist/build/server-script.d.ts +2 -1
  23. package/dist/build/server-script.js +7 -3
  24. package/dist/build-output-manifest.d.ts +3 -2
  25. package/dist/build-output-manifest.js +3 -0
  26. package/dist/build.js +29 -17
  27. package/dist/dev-server.js +79 -25
  28. package/dist/download-result.d.ts +14 -0
  29. package/dist/download-result.js +148 -0
  30. package/dist/images/service.d.ts +13 -1
  31. package/dist/images/service.js +45 -15
  32. package/dist/manifest.d.ts +15 -1
  33. package/dist/manifest.js +24 -5
  34. package/dist/preview.d.ts +11 -3
  35. package/dist/preview.js +188 -62
  36. package/dist/request-body.d.ts +0 -1
  37. package/dist/request-body.js +0 -6
  38. package/dist/resource-manifest.d.ts +16 -0
  39. package/dist/resource-manifest.js +53 -0
  40. package/dist/resource-response.d.ts +34 -0
  41. package/dist/resource-response.js +71 -0
  42. package/dist/resource-route-module.d.ts +15 -0
  43. package/dist/resource-route-module.js +129 -0
  44. package/dist/route-check-support.js +1 -1
  45. package/dist/server-contract.d.ts +24 -16
  46. package/dist/server-contract.js +217 -25
  47. package/dist/server-error.d.ts +1 -1
  48. package/dist/server-error.js +2 -0
  49. package/dist/server-output.d.ts +2 -1
  50. package/dist/server-output.js +59 -11
  51. package/dist/server-runtime/node-server.js +34 -4
  52. package/dist/server-runtime/route-render.d.ts +25 -1
  53. package/dist/server-runtime/route-render.js +81 -29
  54. package/dist/server-script-composition.d.ts +4 -2
  55. package/dist/server-script-composition.js +6 -3
  56. package/dist/static-export-paths.d.ts +3 -0
  57. package/dist/static-export-paths.js +160 -0
  58. package/package.json +3 -3
package/README.md CHANGED
@@ -53,8 +53,8 @@ There is no separate `assetPrefix` config. Public framework asset URLs follow `b
53
53
 
54
54
  - `basePath` defaults to `/`.
55
55
  - Canonical route paths stay base-path free in manifests and route classification.
56
- - Public app URLs, bundled asset URLs, router URLs, `/_zenith/image`, and `/__zenith/route-check` are prefixed with `basePath`.
57
- - Static emitted files stay adapter-neutral; adapters map the public base path to those canonical outputs.
56
+ - Public app URLs, bundled asset URLs, router URLs, and any framework endpoints exposed by the selected target are prefixed with `basePath`.
57
+ - Canonical `.zenith-output` files stay adapter-neutral; final adapter output may still nest public files under `basePath` when direct-file serving requires it.
58
58
 
59
59
  `router` behavior:
60
60
 
@@ -66,6 +66,7 @@ There is no separate `assetPrefix` config. Public framework asset URLs follow `b
66
66
 
67
67
  - `target` is the shorthand deployment target. Phase 1 defaults loaded config to `target: 'static'`.
68
68
  - `adapter` is the explicit adapter object form and is mutually exclusive with `target`.
69
+ - `static-export` emits rewrite-free concrete public files rooted at `outDir` and requires `exportPaths` for dynamic prerender routes.
69
70
  - `vercel-static` emits a Vercel Build Output API layout rooted at `outDir`.
70
71
  - `netlify-static` emits a Netlify publish directory rooted at `outDir`, including generated `_redirects` rewrites for dynamic prerendered routes.
71
72
  - `vercel` emits a Vercel Build Output API layout with packaged route functions for server-classified routes and static rewrites for prerendered dynamic routes.
@@ -81,8 +82,9 @@ Server-capable target contract:
81
82
  Current limitations:
82
83
 
83
84
  - There is no separate `assetPrefix` knob. Assets intentionally follow `basePath`.
85
+ - `static-export` does not expose deployed `/_zenith/image` or `/__zenith/route-check` endpoints. A plain static file server is the contract.
84
86
  - `vercel` and `netlify` do not yet emit a deployed `/_zenith/image` endpoint. The `node` target does.
85
- - Image materialization is route-artifact-driven. Build, preview, and server render consume structured `image_materialization` metadata instead of executing page assets, and dynamic image props are currently unsupported until the compiler emits a dedicated image-props artifact.
87
+ - Image materialization is route-artifact-driven. Bundler owns final build/static HTML image materialization, while preview and server render still materialize at runtime from structured `image_materialization` metadata. No path executes page assets, and dynamic image props are currently unsupported until the compiler emits a dedicated image-props artifact.
86
88
  - There is no shipped plugin install/remove command surface in this CLI.
87
89
 
88
90
  ## Commands
@@ -1,5 +1,5 @@
1
1
  export namespace netlifyAdapter {
2
2
  let name: string;
3
- function validateRoutes(): void;
3
+ function validateRoutes(manifest: any): void;
4
4
  function adapt(options: any): Promise<void>;
5
5
  }
@@ -2,7 +2,9 @@ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { prependBasePath } from '../base-path.js';
4
4
  import { compareRouteSpecificity } from '../server/resolve-request-route.js';
5
- import { createNetlifyBasePathAssetRules, createNetlifyRewriteRules } from './route-rules.js';
5
+ import { copyHostedPageRuntime } from './copy-hosted-page-runtime.js';
6
+ import { createNetlifyBasePathAssetRules, createNetlifyImageEndpointRule, createNetlifyRewriteRules } from './route-rules.js';
7
+ import { validateHostedResourceRoutes } from './validate-hosted-resource-routes.js';
6
8
  function buildNetlifyServerRules(route, basePath = '/') {
7
9
  const destination = `/.netlify/functions/__zenith_${route.name}`;
8
10
  if (!Array.isArray(route.params) || route.params.length === 0) {
@@ -34,13 +36,37 @@ function createFunctionSource(route) {
34
36
  return [
35
37
  "import { fileURLToPath } from 'node:url';",
36
38
  "import { dirname, join } from 'node:path';",
37
- "import { renderRouteRequest, extractInternalParams } from './_zenith/runtime/route-render.js';",
39
+ "import { renderResourceRouteRequest, renderRouteRequest, extractInternalParams } from './_zenith/runtime/route-render.js';",
38
40
  '',
39
41
  'const __dirname = dirname(fileURLToPath(import.meta.url));',
40
42
  `const route = ${JSON.stringify(route, null, 2)};`,
41
43
  '',
44
+ 'function createHostedUnsupportedResponse(message) {',
45
+ " return new Response(message, { status: 501, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });",
46
+ '}',
47
+ '',
48
+ 'function isMultipartFormData(request) {',
49
+ " const contentType = request.headers.get('content-type') || '';",
50
+ " return /^multipart\\/form-data(?:\\s*;|$)/i.test(contentType.trim());",
51
+ '}',
52
+ '',
42
53
  'export default async function(request) {',
43
54
  ' const params = extractInternalParams(request.url, route);',
55
+ " if (route.route_kind === 'resource') {",
56
+ ' if (isMultipartFormData(request)) {',
57
+ " return createHostedUnsupportedResponse('Hosted multipart resource routes are unsupported in this milestone');",
58
+ ' }',
59
+ ' const response = await renderResourceRouteRequest({',
60
+ ' request,',
61
+ ' route,',
62
+ ' params,',
63
+ ` routeModulePath: join(__dirname, '_zenith', 'routes', ${JSON.stringify(route.name)}, 'route', 'entry.js')`,
64
+ ' });',
65
+ " if (response.headers.has('content-disposition')) {",
66
+ " return createHostedUnsupportedResponse('Hosted resource downloads are unsupported in this milestone');",
67
+ ' }',
68
+ ' return response;',
69
+ ' }',
44
70
  ' return renderRouteRequest({',
45
71
  ' request,',
46
72
  ' route,',
@@ -55,6 +81,24 @@ function createFunctionSource(route) {
55
81
  ''
56
82
  ].join('\n');
57
83
  }
84
+ function createImageFunctionSource(imagesConfig) {
85
+ return [
86
+ "import { fileURLToPath } from 'node:url';",
87
+ "import { dirname } from 'node:path';",
88
+ "import { handleImageFetchRequest } from './_zenith/images/service.js';",
89
+ '',
90
+ 'const __dirname = dirname(fileURLToPath(import.meta.url));',
91
+ `const imageConfig = ${JSON.stringify(imagesConfig || {}, null, 2)};`,
92
+ '',
93
+ 'export default async function(request) {',
94
+ ' return handleImageFetchRequest(request, {',
95
+ ' projectRoot: __dirname,',
96
+ ' config: imageConfig',
97
+ ' });',
98
+ '}',
99
+ ''
100
+ ].join('\n');
101
+ }
58
102
  async function loadServerManifest(coreOutput) {
59
103
  try {
60
104
  const parsed = JSON.parse(await readFile(join(coreOutput, 'server', 'manifest.json'), 'utf8'));
@@ -67,7 +111,8 @@ async function loadServerManifest(coreOutput) {
67
111
  function buildRedirectsFile(buildManifest, serverRoutes) {
68
112
  const lines = [
69
113
  '# Generated by Zenith netlify adapter',
70
- ...createNetlifyBasePathAssetRules(buildManifest.base_path)
114
+ ...createNetlifyBasePathAssetRules(buildManifest.base_path),
115
+ createNetlifyImageEndpointRule(buildManifest.base_path)
71
116
  ];
72
117
  const seen = new Set();
73
118
  for (const route of [...serverRoutes].sort((left, right) => compareRouteSpecificity(left.path, right.path))) {
@@ -92,7 +137,9 @@ function buildRedirectsFile(buildManifest, serverRoutes) {
92
137
  }
93
138
  export const netlifyAdapter = {
94
139
  name: 'netlify',
95
- validateRoutes() { },
140
+ validateRoutes(manifest) {
141
+ validateHostedResourceRoutes(manifest, 'netlify');
142
+ },
96
143
  async adapt(options) {
97
144
  const publishDir = join(options.outDir, 'publish');
98
145
  const functionsDir = join(options.outDir, 'functions');
@@ -105,16 +152,11 @@ export const netlifyAdapter = {
105
152
  await mkdir(functionsDir, { recursive: true });
106
153
  await cp(staticDir, publishDir, { recursive: true, force: true });
107
154
  await writeFile(join(functionsDir, 'package.json'), '{\n "type": "module"\n}\n', 'utf8');
108
- if (serverRoutes.length > 0) {
109
- await cp(join(options.coreOutput, 'server', 'runtime'), join(functionsDir, '_zenith', 'runtime'), { recursive: true, force: true });
110
- await cp(join(options.coreOutput, 'server', 'images'), join(functionsDir, '_zenith', 'images'), { recursive: true, force: true });
111
- await cp(join(options.coreOutput, 'server', 'base-path.js'), join(functionsDir, '_zenith', 'base-path.js'), { force: true });
112
- await cp(join(options.coreOutput, 'server', 'server-contract.js'), join(functionsDir, '_zenith', 'server-contract.js'), { force: true });
113
- await cp(join(options.coreOutput, 'server', 'server-error.js'), join(functionsDir, '_zenith', 'server-error.js'), { force: true });
114
- for (const route of serverRoutes) {
115
- await cp(join(options.coreOutput, 'server', 'routes', route.name), join(functionsDir, '_zenith', 'routes', route.name), { recursive: true, force: true });
116
- await writeFile(join(functionsDir, `__zenith_${route.name}.mjs`), createFunctionSource(route), 'utf8');
117
- }
155
+ await copyHostedPageRuntime(options.coreOutput, join(functionsDir, '_zenith'));
156
+ await writeFile(join(functionsDir, '__zenith_image.mjs'), createImageFunctionSource(options.config?.images || {}), 'utf8');
157
+ for (const route of serverRoutes) {
158
+ await cp(join(options.coreOutput, 'server', 'routes', route.name), join(functionsDir, '_zenith', 'routes', route.name), { recursive: true, force: true });
159
+ await writeFile(join(functionsDir, `__zenith_${route.name}.mjs`), createFunctionSource(route), 'utf8');
118
160
  }
119
161
  await writeFile(join(publishDir, '_redirects'), buildRedirectsFile(options.manifest, serverRoutes), 'utf8');
120
162
  await writeFile(join(options.outDir, 'netlify.toml'), [
@@ -0,0 +1,5 @@
1
+ export namespace staticExportAdapter {
2
+ let name: string;
3
+ function validateRoutes(manifest: any): void;
4
+ function adapt(options: any): Promise<void>;
5
+ }
@@ -0,0 +1,115 @@
1
+ import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, relative } from 'node:path';
3
+ import { prependBasePath } from '../base-path.js';
4
+ import { toStaticHtmlFilePath } from '../static-export-paths.js';
5
+ function stripLeadingSlash(value) {
6
+ return String(value || '').replace(/^\/+/, '');
7
+ }
8
+ function collectConcretePaths(route) {
9
+ if (route.path_kind === 'dynamic') {
10
+ return Array.isArray(route.export_paths) ? route.export_paths : [];
11
+ }
12
+ return [route.path];
13
+ }
14
+ async function copySupportFiles({ staticDir, outDir, publicRoot, skippedFiles }) {
15
+ async function walk(currentDir) {
16
+ let entries = [];
17
+ try {
18
+ entries = await readdir(currentDir);
19
+ }
20
+ catch {
21
+ return;
22
+ }
23
+ entries.sort((left, right) => left.localeCompare(right));
24
+ for (const name of entries) {
25
+ const sourcePath = join(currentDir, name);
26
+ const info = await stat(sourcePath);
27
+ if (info.isDirectory()) {
28
+ await walk(sourcePath);
29
+ continue;
30
+ }
31
+ const relativePath = relative(staticDir, sourcePath).replaceAll('\\', '/');
32
+ if (relativePath === 'manifest.json') {
33
+ await cp(sourcePath, join(outDir, 'manifest.json'), { force: true });
34
+ continue;
35
+ }
36
+ if (skippedFiles.has(relativePath)) {
37
+ continue;
38
+ }
39
+ const targetPath = join(publicRoot, relativePath);
40
+ await mkdir(dirname(targetPath), { recursive: true });
41
+ await cp(sourcePath, targetPath, { force: true });
42
+ }
43
+ }
44
+ await walk(staticDir);
45
+ }
46
+ async function writeConcreteHtmlFiles({ staticDir, outDir, routes, basePath }) {
47
+ for (const route of routes) {
48
+ const sourceHtmlPath = join(staticDir, stripLeadingSlash(route.html));
49
+ const sourceHtml = await readFile(sourceHtmlPath, 'utf8');
50
+ for (const concretePath of collectConcretePaths(route)) {
51
+ const publicPath = prependBasePath(basePath, concretePath);
52
+ const outputPath = join(outDir, toStaticHtmlFilePath(publicPath));
53
+ await mkdir(dirname(outputPath), { recursive: true });
54
+ await writeFile(outputPath, sourceHtml, 'utf8');
55
+ }
56
+ }
57
+ }
58
+ export const staticExportAdapter = {
59
+ name: 'static-export',
60
+ validateRoutes(manifest) {
61
+ const concretePathOwners = new Map();
62
+ for (const route of manifest) {
63
+ if (route.render_mode === 'server') {
64
+ throw new Error(`[Zenith:Build] target "static-export" cannot emit server-rendered routes. ` +
65
+ `Route "${route.path}" (${route.file}) requires render_mode="server".`);
66
+ }
67
+ if (route.path_kind === 'static') {
68
+ if (Array.isArray(route.export_paths) && route.export_paths.length > 0) {
69
+ throw new Error(`[Zenith:Build] target "static-export" only accepts exportPaths on dynamic prerender routes. ` +
70
+ `Route "${route.path}" (${route.file}) is already concrete.`);
71
+ }
72
+ }
73
+ else {
74
+ if (!Array.isArray(route.export_paths) || route.export_paths.length === 0) {
75
+ throw new Error(`[Zenith:Build] target "static-export" requires explicit exportPaths for dynamic prerender routes. ` +
76
+ `Route "${route.path}" (${route.file}) has no concrete export-path contract.`);
77
+ }
78
+ }
79
+ for (const concretePath of collectConcretePaths(route)) {
80
+ const existing = concretePathOwners.get(concretePath);
81
+ if (existing) {
82
+ throw new Error(`[Zenith:Build] target "static-export" produced a duplicate concrete path "${concretePath}" ` +
83
+ `from "${existing.file}" and "${route.file}".`);
84
+ }
85
+ concretePathOwners.set(concretePath, route);
86
+ }
87
+ }
88
+ },
89
+ async adapt(options) {
90
+ const staticDir = join(options.coreOutput, 'static');
91
+ const routes = Array.isArray(options.manifest?.routes) ? options.manifest.routes : [];
92
+ const skippedFiles = new Set(routes
93
+ .map((route) => stripLeadingSlash(route.html))
94
+ .filter((value) => value.length > 0));
95
+ const basePath = options.manifest?.base_path || '/';
96
+ const publicRoot = basePath === '/'
97
+ ? options.outDir
98
+ : join(options.outDir, stripLeadingSlash(basePath));
99
+ await rm(options.outDir, { recursive: true, force: true });
100
+ await mkdir(options.outDir, { recursive: true });
101
+ await mkdir(publicRoot, { recursive: true });
102
+ await copySupportFiles({
103
+ staticDir,
104
+ outDir: options.outDir,
105
+ publicRoot,
106
+ skippedFiles
107
+ });
108
+ await writeConcreteHtmlFiles({
109
+ staticDir,
110
+ outDir: options.outDir,
111
+ routes,
112
+ basePath
113
+ });
114
+ }
115
+ };
@@ -1,5 +1,5 @@
1
1
  export const KNOWN_TARGETS: string[];
2
- export type ZenithTarget = "static" | "vercel-static" | "netlify-static" | "vercel" | "netlify" | "node";
2
+ export type ZenithTarget = "static" | "static-export" | "vercel-static" | "netlify-static" | "vercel" | "netlify" | "node";
3
3
  export type ZenithRenderMode = "prerender" | "server";
4
4
  export type ZenithPathKind = "static" | "dynamic";
5
5
  export type RouteManifestEntry = {
@@ -8,6 +8,7 @@ export type RouteManifestEntry = {
8
8
  path_kind: ZenithPathKind;
9
9
  render_mode: ZenithRenderMode;
10
10
  params: string[];
11
+ export_paths?: string[];
11
12
  };
12
13
  export type BuildManifest = {
13
14
  schema_version: number;
@@ -22,6 +23,7 @@ export type BuildManifest = {
22
23
  render_mode: ZenithRenderMode;
23
24
  requires_hydration: boolean;
24
25
  params: string[];
26
+ export_paths?: string[];
25
27
  html: string;
26
28
  assets: string[];
27
29
  }>;
@@ -1,5 +1,6 @@
1
1
  export const KNOWN_TARGETS = [
2
2
  'static',
3
+ 'static-export',
3
4
  'vercel-static',
4
5
  'netlify-static',
5
6
  'vercel',
@@ -7,7 +8,7 @@ export const KNOWN_TARGETS = [
7
8
  'node'
8
9
  ];
9
10
  /**
10
- * @typedef {'static' | 'vercel-static' | 'netlify-static' | 'vercel' | 'netlify' | 'node'} ZenithTarget
11
+ * @typedef {'static' | 'static-export' | 'vercel-static' | 'netlify-static' | 'vercel' | 'netlify' | 'node'} ZenithTarget
11
12
  */
12
13
  /**
13
14
  * @typedef {'prerender' | 'server'} ZenithRenderMode
@@ -21,7 +22,8 @@ export const KNOWN_TARGETS = [
21
22
  * file: string,
22
23
  * path_kind: ZenithPathKind,
23
24
  * render_mode: ZenithRenderMode,
24
- * params: string[]
25
+ * params: string[],
26
+ * export_paths?: string[]
25
27
  * }} RouteManifestEntry
26
28
  */
27
29
  /**
@@ -38,6 +40,7 @@ export const KNOWN_TARGETS = [
38
40
  * render_mode: ZenithRenderMode,
39
41
  * requires_hydration: boolean,
40
42
  * params: string[],
43
+ * export_paths?: string[],
41
44
  * html: string,
42
45
  * assets: string[]
43
46
  * }>,
@@ -1,5 +1,5 @@
1
1
  export namespace vercelAdapter {
2
2
  let name: string;
3
- function validateRoutes(): void;
3
+ function validateRoutes(manifest: any): void;
4
4
  function adapt(options: any): Promise<void>;
5
5
  }
@@ -1,7 +1,9 @@
1
1
  import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
3
  import { compareRouteSpecificity } from '../server/resolve-request-route.js';
4
- import { createVercelBasePathAssetRoutes, createVercelRouteSource } from './route-rules.js';
4
+ import { copyHostedPageRuntime } from './copy-hosted-page-runtime.js';
5
+ import { createVercelBasePathAssetRoutes, createVercelImageEndpointRoute, createVercelRouteSource } from './route-rules.js';
6
+ import { validateHostedResourceRoutes } from './validate-hosted-resource-routes.js';
5
7
  function buildVercelServerDest(route) {
6
8
  const base = `/__zenith/${route.name}`;
7
9
  if (!Array.isArray(route.params) || route.params.length === 0) {
@@ -18,6 +20,7 @@ function buildVercelConfig(buildManifest, serverRoutes) {
18
20
  dest: buildVercelServerDest(route)
19
21
  });
20
22
  }
23
+ routes.push(createVercelImageEndpointRoute(buildManifest.base_path));
21
24
  routes.push({ handle: 'filesystem' });
22
25
  for (const route of buildManifest.routes.filter((entry) => entry.render_mode === 'prerender' && entry.path_kind === 'dynamic')) {
23
26
  routes.push({
@@ -30,18 +33,62 @@ function buildVercelConfig(buildManifest, serverRoutes) {
30
33
  routes
31
34
  };
32
35
  }
36
+ function createImageFunctionSource(imagesConfig) {
37
+ return [
38
+ "import { fileURLToPath } from 'node:url';",
39
+ "import { dirname } from 'node:path';",
40
+ "import { handleImageFetchRequest } from './images/service.js';",
41
+ '',
42
+ 'const __dirname = dirname(fileURLToPath(import.meta.url));',
43
+ `const imageConfig = ${JSON.stringify(imagesConfig || {}, null, 2)};`,
44
+ '',
45
+ 'export default {',
46
+ ' async fetch(request) {',
47
+ ' return handleImageFetchRequest(request, {',
48
+ ' projectRoot: __dirname,',
49
+ ' config: imageConfig',
50
+ ' });',
51
+ ' }',
52
+ '};',
53
+ ''
54
+ ].join('\n');
55
+ }
33
56
  function createFunctionSource(route) {
34
57
  return [
35
58
  "import { fileURLToPath } from 'node:url';",
36
59
  "import { dirname, join } from 'node:path';",
37
- "import { renderRouteRequest, extractInternalParams } from './runtime/route-render.js';",
60
+ "import { renderResourceRouteRequest, renderRouteRequest, extractInternalParams } from './runtime/route-render.js';",
38
61
  '',
39
62
  'const __dirname = dirname(fileURLToPath(import.meta.url));',
40
63
  `const route = ${JSON.stringify(route, null, 2)};`,
41
64
  '',
65
+ 'function createHostedUnsupportedResponse(message) {',
66
+ " return new Response(message, { status: 501, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });",
67
+ '}',
68
+ '',
69
+ 'function isMultipartFormData(request) {',
70
+ " const contentType = request.headers.get('content-type') || '';",
71
+ " return /^multipart\\/form-data(?:\\s*;|$)/i.test(contentType.trim());",
72
+ '}',
73
+ '',
42
74
  'export default {',
43
75
  ' async fetch(request) {',
44
76
  ' const params = extractInternalParams(request.url, route);',
77
+ " if (route.route_kind === 'resource') {",
78
+ ' if (isMultipartFormData(request)) {',
79
+ " return createHostedUnsupportedResponse('Hosted multipart resource routes are unsupported in this milestone');",
80
+ ' }',
81
+ ' const response = await renderResourceRouteRequest({',
82
+ ' request,',
83
+ ' route,',
84
+ ' params,',
85
+ " routeModulePath: join(__dirname, 'route', 'entry.js')",
86
+ ' });',
87
+ " if (response.headers.has('content-disposition')) {",
88
+ " return createHostedUnsupportedResponse('Hosted resource downloads are unsupported in this milestone');",
89
+ ' }',
90
+ ' return response;',
91
+ ' }',
45
92
  ' return renderRouteRequest({',
46
93
  ' request,',
47
94
  ' route,',
@@ -66,9 +113,26 @@ async function loadServerManifest(coreOutput) {
66
113
  return [];
67
114
  }
68
115
  }
116
+ function vercelFunctionConfig() {
117
+ return `${JSON.stringify({
118
+ runtime: 'nodejs22.x',
119
+ handler: 'index.js',
120
+ launcherType: 'Nodejs',
121
+ shouldAddHelpers: true
122
+ }, null, 2)}\n`;
123
+ }
124
+ async function writeHostedFunctionBundle(functionDir, coreOutput, source) {
125
+ await mkdir(functionDir, { recursive: true });
126
+ await copyHostedPageRuntime(coreOutput, functionDir);
127
+ await writeFile(join(functionDir, 'package.json'), '{\n "type": "module"\n}\n', 'utf8');
128
+ await writeFile(join(functionDir, 'index.js'), source, 'utf8');
129
+ await writeFile(join(functionDir, '.vc-config.json'), vercelFunctionConfig(), 'utf8');
130
+ }
69
131
  export const vercelAdapter = {
70
132
  name: 'vercel',
71
- validateRoutes() { },
133
+ validateRoutes(manifest) {
134
+ validateHostedResourceRoutes(manifest, 'vercel');
135
+ },
72
136
  async adapt(options) {
73
137
  const staticDir = join(options.coreOutput, 'static');
74
138
  // Route meaning is fixed upstream in the manifest/server package.
@@ -77,23 +141,15 @@ export const vercelAdapter = {
77
141
  await rm(options.outDir, { recursive: true, force: true });
78
142
  await mkdir(join(options.outDir, 'static'), { recursive: true });
79
143
  await cp(staticDir, join(options.outDir, 'static'), { recursive: true, force: true });
144
+ await writeHostedFunctionBundle(join(options.outDir, 'functions', '__zenith', 'image.func'), options.coreOutput, createImageFunctionSource(options.config?.images || {}));
80
145
  for (const route of serverRoutes) {
81
146
  const functionDir = join(options.outDir, 'functions', '__zenith', `${route.name}.func`);
82
147
  await mkdir(functionDir, { recursive: true });
83
- await cp(join(options.coreOutput, 'server', 'runtime'), join(functionDir, 'runtime'), { recursive: true, force: true });
84
- await cp(join(options.coreOutput, 'server', 'images'), join(functionDir, 'images'), { recursive: true, force: true });
85
- await cp(join(options.coreOutput, 'server', 'base-path.js'), join(functionDir, 'base-path.js'), { force: true });
86
- await cp(join(options.coreOutput, 'server', 'server-contract.js'), join(functionDir, 'server-contract.js'), { force: true });
87
- await cp(join(options.coreOutput, 'server', 'server-error.js'), join(functionDir, 'server-error.js'), { force: true });
148
+ await copyHostedPageRuntime(options.coreOutput, functionDir);
88
149
  await cp(join(options.coreOutput, 'server', 'routes', route.name), functionDir, { recursive: true, force: true });
89
150
  await writeFile(join(functionDir, 'package.json'), '{\n "type": "module"\n}\n', 'utf8');
90
151
  await writeFile(join(functionDir, 'index.js'), createFunctionSource(route), 'utf8');
91
- await writeFile(join(functionDir, '.vc-config.json'), `${JSON.stringify({
92
- runtime: 'nodejs22.x',
93
- handler: 'index.js',
94
- launcherType: 'Nodejs',
95
- shouldAddHelpers: true
96
- }, null, 2)}\n`, 'utf8');
152
+ await writeFile(join(functionDir, '.vc-config.json'), vercelFunctionConfig(), 'utf8');
97
153
  }
98
154
  await writeFile(join(options.outDir, 'config.json'), `${JSON.stringify(buildVercelConfig(options.manifest, serverRoutes), null, 2)}\n`, 'utf8');
99
155
  }
@@ -0,0 +1 @@
1
+ export function copyHostedPageRuntime(coreOutput: any, targetDir: any): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ const PACKAGE_REQUIRE = createRequire(import.meta.url);
6
+ const HOSTED_PAGE_RUNTIME_DIRS = ['runtime', 'images', 'auth'];
7
+ const HOSTED_PAGE_RUNTIME_FILES = [
8
+ 'base-path.js',
9
+ 'server-contract.js',
10
+ 'server-error.js',
11
+ 'resource-response.js',
12
+ 'download-result.js'
13
+ ];
14
+ function createSharpRuntimeSource() {
15
+ const sharpPath = PACKAGE_REQUIRE.resolve('sharp');
16
+ const fallbackUrl = pathToFileURL(sharpPath).href;
17
+ return [
18
+ 'async function loadSharp() {',
19
+ ' try {',
20
+ " const mod = await import('sharp');",
21
+ ' return mod.default || mod;',
22
+ ' } catch {',
23
+ ` const mod = await import(${JSON.stringify(fallbackUrl)});`,
24
+ ' return mod.default || mod;',
25
+ ' }',
26
+ '}',
27
+ '',
28
+ 'const sharp = await loadSharp();',
29
+ 'export default sharp;',
30
+ ''
31
+ ].join('\n');
32
+ }
33
+ export async function copyHostedPageRuntime(coreOutput, targetDir) {
34
+ const serverDir = join(coreOutput, 'server');
35
+ await mkdir(targetDir, { recursive: true });
36
+ for (const name of HOSTED_PAGE_RUNTIME_DIRS) {
37
+ await cp(join(serverDir, name), join(targetDir, name), {
38
+ recursive: true,
39
+ force: true
40
+ });
41
+ }
42
+ for (const name of HOSTED_PAGE_RUNTIME_FILES) {
43
+ await cp(join(serverDir, name), join(targetDir, name), { force: true });
44
+ }
45
+ const imageServicePath = join(targetDir, 'images', 'service.js');
46
+ const imageServiceSource = await readFile(imageServicePath, 'utf8');
47
+ await writeFile(imageServicePath, imageServiceSource.replace("import sharp from 'sharp';", "import sharp from './sharp-runtime.js';"), 'utf8');
48
+ await writeFile(join(targetDir, 'images', 'sharp-runtime.js'), createSharpRuntimeSource(), 'utf8');
49
+ }
@@ -2,6 +2,7 @@ import { isConfigKeyExplicit, isLoadedConfig } from '../config.js';
2
2
  import { netlifyAdapter } from './adapter-netlify.js';
3
3
  import { nodeAdapter } from './adapter-node.js';
4
4
  import { netlifyStaticAdapter } from './adapter-netlify-static.js';
5
+ import { staticExportAdapter } from './adapter-static-export.js';
5
6
  import { staticAdapter } from './adapter-static.js';
6
7
  import { KNOWN_TARGETS } from './adapter-types.js';
7
8
  import { vercelAdapter } from './adapter-vercel.js';
@@ -32,6 +33,9 @@ function resolveTargetAdapter(target) {
32
33
  if (target === 'static') {
33
34
  return staticAdapter;
34
35
  }
36
+ if (target === 'static-export') {
37
+ return staticExportAdapter;
38
+ }
35
39
  if (target === 'vercel-static') {
36
40
  return vercelStaticAdapter;
37
41
  }
@@ -1,7 +1,12 @@
1
1
  export function createNetlifyBasePathAssetRules(basePath: any): string[];
2
+ export function createNetlifyImageEndpointRule(basePath: any): string;
2
3
  export function createNetlifyRewriteRules(route: any, basePath?: string): string[];
3
4
  export function createVercelBasePathAssetRoutes(basePath: any): {
4
5
  src: string;
5
6
  dest: string;
6
7
  }[];
8
+ export function createVercelImageEndpointRoute(basePath: any): {
9
+ src: string;
10
+ dest: string;
11
+ };
7
12
  export function createVercelRouteSource(routePath: any, basePath?: string): string;
@@ -21,6 +21,9 @@ export function createNetlifyBasePathAssetRules(basePath) {
21
21
  `${prependBasePath(normalizedBasePath, '/_zenith/image/local/*')} /_zenith/image/local/:splat 200`
22
22
  ];
23
23
  }
24
+ export function createNetlifyImageEndpointRule(basePath) {
25
+ return `${prependBasePath(basePath, '/_zenith/image')} /.netlify/functions/__zenith_image 200!`;
26
+ }
24
27
  export function createNetlifyRewriteRules(route, basePath = '/') {
25
28
  const segments = splitRouteSegments(route.path);
26
29
  if (segments.length === 0) {
@@ -61,6 +64,12 @@ export function createVercelBasePathAssetRoutes(basePath) {
61
64
  }
62
65
  ];
63
66
  }
67
+ export function createVercelImageEndpointRoute(basePath) {
68
+ return {
69
+ src: createVercelRouteSource('/_zenith/image', basePath),
70
+ dest: '/__zenith/image'
71
+ };
72
+ }
64
73
  export function createVercelRouteSource(routePath, basePath = '/') {
65
74
  const segments = splitRouteSegments(routePath);
66
75
  if (segments.length === 0) {
@@ -0,0 +1 @@
1
+ export function validateHostedResourceRoutes(manifest: any, targetName: any): void;
@@ -0,0 +1,13 @@
1
+ const HOSTED_RESOURCE_DOWNLOAD_RE = /\b(?:ctx\.)?download\s*\(/;
2
+ export function validateHostedResourceRoutes(manifest, targetName) {
3
+ for (const route of Array.isArray(manifest) ? manifest : []) {
4
+ if (route?.route_kind !== 'resource') {
5
+ continue;
6
+ }
7
+ const source = typeof route.server_script === 'string' ? route.server_script : '';
8
+ if (HOSTED_RESOURCE_DOWNLOAD_RE.test(source)) {
9
+ throw new Error(`[Zenith:Build] target "${targetName}" does not support resource downloads in this milestone. ` +
10
+ `Route "${route.path}" (${route.file}) must run on dev, preview, or target "node".`);
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,6 @@
1
+ export function consumeStagedSetCookies(ctx: any): any[];
2
+ export function attachRouteAuth(ctx: any, options?: {}): any;
3
+ export const SESSION_COOKIE_NAME: "zenith_session";
4
+ export const SESSION_SECRET_ENV: "ZENITH_SESSION_SECRET";
5
+ export const STAGED_SET_COOKIES_KEY: "__zenith_staged_set_cookies";
6
+ export const AUTH_CONTROL_FLOW_FLAG: "__zenith_auth_control_flow";