@zenithbuild/cli 0.7.3 → 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.
- package/README.md +18 -13
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +56 -13
- package/dist/adapters/adapter-node.js +8 -0
- package/dist/adapters/adapter-static-export.d.ts +5 -0
- package/dist/adapters/adapter-static-export.js +115 -0
- package/dist/adapters/adapter-types.d.ts +3 -1
- package/dist/adapters/adapter-types.js +5 -2
- package/dist/adapters/adapter-vercel.d.ts +1 -1
- package/dist/adapters/adapter-vercel.js +70 -13
- package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
- package/dist/adapters/copy-hosted-page-runtime.js +49 -0
- package/dist/adapters/resolve-adapter.js +4 -0
- package/dist/adapters/route-rules.d.ts +5 -0
- package/dist/adapters/route-rules.js +9 -0
- package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
- package/dist/adapters/validate-hosted-resource-routes.js +13 -0
- package/dist/auth/route-auth.d.ts +6 -0
- package/dist/auth/route-auth.js +236 -0
- package/dist/build/compiler-runtime.d.ts +10 -9
- package/dist/build/compiler-runtime.js +58 -2
- package/dist/build/compiler-signal-expression.d.ts +1 -0
- package/dist/build/compiler-signal-expression.js +155 -0
- package/dist/build/expression-rewrites.d.ts +1 -6
- package/dist/build/expression-rewrites.js +61 -65
- package/dist/build/page-component-loop.d.ts +3 -13
- package/dist/build/page-component-loop.js +21 -46
- package/dist/build/page-ir-normalization.d.ts +0 -8
- package/dist/build/page-ir-normalization.js +13 -234
- package/dist/build/page-loop-state.d.ts +6 -9
- package/dist/build/page-loop-state.js +9 -8
- package/dist/build/page-loop.js +27 -22
- package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
- package/dist/build/scoped-identifier-rewrite.js +28 -128
- package/dist/build/server-script.d.ts +3 -1
- package/dist/build/server-script.js +35 -5
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +32 -18
- package/dist/component-instance-ir.js +158 -52
- package/dist/dev-build-session.js +20 -6
- package/dist/dev-server.js +152 -55
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/framework-components/Image.zen +1 -1
- package/dist/images/materialization-plan.d.ts +1 -0
- package/dist/images/materialization-plan.js +6 -0
- package/dist/images/materialize.d.ts +5 -3
- package/dist/images/materialize.js +24 -109
- package/dist/images/router-manifest.d.ts +1 -0
- package/dist/images/router-manifest.js +49 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/index.js +8 -2
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +27 -7
- package/dist/preview.d.ts +13 -4
- package/dist/preview.js +261 -101
- package/dist/request-body.d.ts +1 -0
- package/dist/request-body.js +7 -0
- package/dist/request-origin.d.ts +2 -0
- package/dist/request-origin.js +45 -0
- package/dist/resource-manifest.d.ts +16 -0
- package/dist/resource-manifest.js +53 -0
- package/dist/resource-response.d.ts +34 -0
- package/dist/resource-response.js +71 -0
- package/dist/resource-route-module.d.ts +15 -0
- package/dist/resource-route-module.js +129 -0
- package/dist/route-check-support.d.ts +1 -0
- package/dist/route-check-support.js +4 -0
- package/dist/server-contract.d.ts +29 -6
- package/dist/server-contract.js +304 -42
- package/dist/server-error.d.ts +4 -0
- package/dist/server-error.js +36 -0
- package/dist/server-output.d.ts +4 -1
- package/dist/server-output.js +71 -10
- package/dist/server-runtime/node-server.js +67 -31
- package/dist/server-runtime/route-render.d.ts +27 -3
- package/dist/server-runtime/route-render.js +94 -53
- package/dist/server-script-composition.d.ts +13 -5
- package/dist/server-script-composition.js +29 -11
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -2,26 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
> **⚠️ Internal API:** This package is an internal implementation detail of the Zenith framework. It is not intended for public use and its API may break without warning. Please use `@zenithbuild/core` instead.
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
The command-line interface for developing and building Zenith applications.
|
|
7
6
|
|
|
8
7
|
## Canonical Docs
|
|
9
8
|
|
|
10
|
-
- CLI contract:
|
|
11
|
-
- Deployment targets guide:
|
|
12
|
-
-
|
|
9
|
+
- CLI contract: `../../docs/documentation/cli-contract.md`
|
|
10
|
+
- Deployment targets guide: `../../docs/documentation/guides/deployment-targets.md`
|
|
11
|
+
- Route protection: `../../docs/documentation/routing/route-protection.md`
|
|
13
12
|
- Server output contract: `./SERVER_OUTPUT_CONTRACT.md`
|
|
14
13
|
|
|
15
14
|
## Overview
|
|
16
15
|
|
|
17
|
-
`@zenithbuild/cli`
|
|
16
|
+
`@zenithbuild/cli` is Zenith's deterministic project orchestrator. It owns the daily development loop:
|
|
17
|
+
|
|
18
|
+
- `zenith dev`
|
|
19
|
+
- `zenith build`
|
|
20
|
+
- `zenith preview`
|
|
21
|
+
|
|
22
|
+
It does not ship a public plugin-management surface.
|
|
18
23
|
|
|
19
24
|
## Features
|
|
20
25
|
|
|
21
26
|
- **Dev Server**: Instant HMR (Hot Module Replacement) powered by Bun.
|
|
22
|
-
- **Build System**:
|
|
23
|
-
- **
|
|
24
|
-
- **Preview**: Test your production builds locally.
|
|
27
|
+
- **Build System**: deterministic build output and adapter packaging.
|
|
28
|
+
- **Preview**: target-aware verification of built output.
|
|
25
29
|
|
|
26
30
|
## Config Baseline
|
|
27
31
|
|
|
@@ -49,8 +53,8 @@ There is no separate `assetPrefix` config. Public framework asset URLs follow `b
|
|
|
49
53
|
|
|
50
54
|
- `basePath` defaults to `/`.
|
|
51
55
|
- Canonical route paths stay base-path free in manifests and route classification.
|
|
52
|
-
- Public app URLs, bundled asset URLs, router URLs,
|
|
53
|
-
-
|
|
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.
|
|
54
58
|
|
|
55
59
|
`router` behavior:
|
|
56
60
|
|
|
@@ -62,6 +66,7 @@ There is no separate `assetPrefix` config. Public framework asset URLs follow `b
|
|
|
62
66
|
|
|
63
67
|
- `target` is the shorthand deployment target. Phase 1 defaults loaded config to `target: 'static'`.
|
|
64
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.
|
|
65
70
|
- `vercel-static` emits a Vercel Build Output API layout rooted at `outDir`.
|
|
66
71
|
- `netlify-static` emits a Netlify publish directory rooted at `outDir`, including generated `_redirects` rewrites for dynamic prerendered routes.
|
|
67
72
|
- `vercel` emits a Vercel Build Output API layout with packaged route functions for server-classified routes and static rewrites for prerendered dynamic routes.
|
|
@@ -77,7 +82,10 @@ Server-capable target contract:
|
|
|
77
82
|
Current limitations:
|
|
78
83
|
|
|
79
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.
|
|
80
86
|
- `vercel` and `netlify` do not yet emit a deployed `/_zenith/image` endpoint. The `node` target does.
|
|
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.
|
|
88
|
+
- There is no shipped plugin install/remove command surface in this CLI.
|
|
81
89
|
|
|
82
90
|
## Commands
|
|
83
91
|
|
|
@@ -90,9 +98,6 @@ Compiles and bundles your application for production.
|
|
|
90
98
|
### `zenith preview`
|
|
91
99
|
Previews the locally built target contract for verification. Static targets serve built files; `target: 'node'` boots the built Node artifact.
|
|
92
100
|
|
|
93
|
-
### `zenith add <plugin>`
|
|
94
|
-
Installs and configures a Zenith plugin.
|
|
95
|
-
|
|
96
101
|
## Installation
|
|
97
102
|
|
|
98
103
|
Typically installed as a dev dependency in your Zenith project:
|
|
@@ -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 {
|
|
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,15 +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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
await cp(join(options.coreOutput, 'server', '
|
|
112
|
-
await
|
|
113
|
-
for (const route of serverRoutes) {
|
|
114
|
-
await cp(join(options.coreOutput, 'server', 'routes', route.name), join(functionsDir, '_zenith', 'routes', route.name), { recursive: true, force: true });
|
|
115
|
-
await writeFile(join(functionsDir, `__zenith_${route.name}.mjs`), createFunctionSource(route), 'utf8');
|
|
116
|
-
}
|
|
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');
|
|
117
160
|
}
|
|
118
161
|
await writeFile(join(publishDir, '_redirects'), buildRedirectsFile(options.manifest, serverRoutes), 'utf8');
|
|
119
162
|
await writeFile(join(options.outDir, 'netlify.toml'), [
|
|
@@ -12,6 +12,14 @@ const NODE_RUNTIME_FILES = [
|
|
|
12
12
|
from: new URL('../server/resolve-request-route.js', import.meta.url),
|
|
13
13
|
to: 'runtime/resolve-request-route.js'
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
from: new URL('../request-origin.js', import.meta.url),
|
|
17
|
+
to: 'request-origin.js'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
from: new URL('../server-error.js', import.meta.url),
|
|
21
|
+
to: 'server-error.js'
|
|
22
|
+
},
|
|
15
23
|
{
|
|
16
24
|
from: new URL('../images/service.js', import.meta.url),
|
|
17
25
|
to: 'images/service.js'
|
|
@@ -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,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 {
|
|
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,22 +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
|
|
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 });
|
|
148
|
+
await copyHostedPageRuntime(options.coreOutput, functionDir);
|
|
87
149
|
await cp(join(options.coreOutput, 'server', 'routes', route.name), functionDir, { recursive: true, force: true });
|
|
88
150
|
await writeFile(join(functionDir, 'package.json'), '{\n "type": "module"\n}\n', 'utf8');
|
|
89
151
|
await writeFile(join(functionDir, 'index.js'), createFunctionSource(route), 'utf8');
|
|
90
|
-
await writeFile(join(functionDir, '.vc-config.json'),
|
|
91
|
-
runtime: 'nodejs22.x',
|
|
92
|
-
handler: 'index.js',
|
|
93
|
-
launcherType: 'Nodejs',
|
|
94
|
-
shouldAddHelpers: true
|
|
95
|
-
}, null, 2)}\n`, 'utf8');
|
|
152
|
+
await writeFile(join(functionDir, '.vc-config.json'), vercelFunctionConfig(), 'utf8');
|
|
96
153
|
}
|
|
97
154
|
await writeFile(join(options.outDir, 'config.json'), `${JSON.stringify(buildVercelConfig(options.manifest, serverRoutes), null, 2)}\n`, 'utf8');
|
|
98
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;
|