@zenithbuild/cli 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/dist/adapters/adapter-netlify-static.d.ts +5 -0
- package/dist/adapters/adapter-netlify-static.js +39 -0
- package/dist/adapters/adapter-netlify.d.ts +5 -0
- package/dist/adapters/adapter-netlify.js +129 -0
- package/dist/adapters/adapter-node.d.ts +5 -0
- package/dist/adapters/adapter-node.js +121 -0
- package/dist/adapters/adapter-static.d.ts +5 -0
- package/dist/adapters/adapter-static.js +20 -0
- package/dist/adapters/adapter-types.d.ts +44 -0
- package/dist/adapters/adapter-types.js +65 -0
- package/dist/adapters/adapter-vercel-static.d.ts +5 -0
- package/dist/adapters/adapter-vercel-static.js +36 -0
- package/dist/adapters/adapter-vercel.d.ts +5 -0
- package/dist/adapters/adapter-vercel.js +99 -0
- package/dist/adapters/resolve-adapter.d.ts +5 -0
- package/dist/adapters/resolve-adapter.js +84 -0
- package/dist/adapters/route-rules.d.ts +7 -0
- package/dist/adapters/route-rules.js +88 -0
- package/dist/base-path-html.d.ts +2 -0
- package/dist/base-path-html.js +42 -0
- package/dist/base-path.d.ts +8 -0
- package/dist/base-path.js +74 -0
- package/dist/build/compiler-runtime.d.ts +2 -1
- package/dist/build/compiler-runtime.js +4 -1
- package/dist/build/page-loop.d.ts +2 -2
- package/dist/build/page-loop.js +3 -3
- package/dist/build-output-manifest.d.ts +28 -0
- package/dist/build-output-manifest.js +100 -0
- package/dist/build.js +42 -11
- package/dist/config.d.ts +10 -46
- package/dist/config.js +162 -28
- package/dist/dev-build-session.d.ts +1 -0
- package/dist/dev-build-session.js +4 -5
- package/dist/framework-components/Image.zen +31 -9
- package/dist/images/payload.d.ts +2 -1
- package/dist/images/payload.js +3 -2
- package/dist/images/runtime.js +6 -5
- package/dist/images/service.js +2 -2
- package/dist/images/shared.d.ts +4 -2
- package/dist/images/shared.js +8 -3
- package/dist/index.js +36 -15
- package/dist/manifest.d.ts +14 -2
- package/dist/manifest.js +49 -6
- package/dist/preview.js +61 -25
- package/dist/server-output.d.ts +26 -0
- package/dist/server-output.js +297 -0
- package/dist/server-runtime/node-server.d.ts +2 -0
- package/dist/server-runtime/node-server.js +354 -0
- package/dist/server-runtime/route-render.d.ts +64 -0
- package/dist/server-runtime/route-render.js +273 -0
- package/package.json +3 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { compareRouteSpecificity } from '../server/resolve-request-route.js';
|
|
4
|
+
import { createVercelBasePathAssetRoutes, createVercelRouteSource } from './route-rules.js';
|
|
5
|
+
function buildVercelServerDest(route) {
|
|
6
|
+
const base = `/__zenith/${route.name}`;
|
|
7
|
+
if (!Array.isArray(route.params) || route.params.length === 0) {
|
|
8
|
+
return base;
|
|
9
|
+
}
|
|
10
|
+
const query = route.params.map((param, index) => `__zenith_param_${param}=$${index + 1}`).join('&');
|
|
11
|
+
return `${base}?${query}`;
|
|
12
|
+
}
|
|
13
|
+
function buildVercelConfig(buildManifest, serverRoutes) {
|
|
14
|
+
const routes = [...createVercelBasePathAssetRoutes(buildManifest.base_path)];
|
|
15
|
+
for (const route of [...serverRoutes].sort((left, right) => compareRouteSpecificity(left.path, right.path))) {
|
|
16
|
+
routes.push({
|
|
17
|
+
src: createVercelRouteSource(route.path, buildManifest.base_path),
|
|
18
|
+
dest: buildVercelServerDest(route)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
routes.push({ handle: 'filesystem' });
|
|
22
|
+
for (const route of buildManifest.routes.filter((entry) => entry.render_mode === 'prerender' && entry.path_kind === 'dynamic')) {
|
|
23
|
+
routes.push({
|
|
24
|
+
src: createVercelRouteSource(route.path, buildManifest.base_path),
|
|
25
|
+
dest: route.html
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
version: 3,
|
|
30
|
+
routes
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createFunctionSource(route) {
|
|
34
|
+
return [
|
|
35
|
+
"import { fileURLToPath } from 'node:url';",
|
|
36
|
+
"import { dirname, join } from 'node:path';",
|
|
37
|
+
"import { renderRouteRequest, extractInternalParams } from './runtime/route-render.js';",
|
|
38
|
+
'',
|
|
39
|
+
'const __dirname = dirname(fileURLToPath(import.meta.url));',
|
|
40
|
+
`const route = ${JSON.stringify(route, null, 2)};`,
|
|
41
|
+
'',
|
|
42
|
+
'export default {',
|
|
43
|
+
' async fetch(request) {',
|
|
44
|
+
' const params = extractInternalParams(request.url, route);',
|
|
45
|
+
' return renderRouteRequest({',
|
|
46
|
+
' request,',
|
|
47
|
+
' route,',
|
|
48
|
+
' params,',
|
|
49
|
+
" routeModulePath: join(__dirname, 'route', 'entry.js'),",
|
|
50
|
+
" shellHtmlPath: join(__dirname, 'route', 'page.html'),",
|
|
51
|
+
` pageAssetPath: ${route.page_asset_file ? "join(__dirname, 'route', " + JSON.stringify(route.page_asset_file) + ')' : 'null'},`,
|
|
52
|
+
` imageManifestPath: ${route.image_manifest_file ? "join(__dirname, 'route', " + JSON.stringify(route.image_manifest_file) + ')' : 'null'},`,
|
|
53
|
+
` imageConfig: ${JSON.stringify(route.image_config || {}, null, 2)}`,
|
|
54
|
+
' });',
|
|
55
|
+
' }',
|
|
56
|
+
'};',
|
|
57
|
+
''
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
async function loadServerManifest(coreOutput) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(await readFile(join(coreOutput, 'server', 'manifest.json'), 'utf8'));
|
|
63
|
+
return Array.isArray(parsed.routes) ? parsed.routes : [];
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export const vercelAdapter = {
|
|
70
|
+
name: 'vercel',
|
|
71
|
+
validateRoutes() { },
|
|
72
|
+
async adapt(options) {
|
|
73
|
+
const staticDir = join(options.coreOutput, 'static');
|
|
74
|
+
// Route meaning is fixed upstream in the manifest/server package.
|
|
75
|
+
// The adapter only maps already-classified output into Vercel's layout.
|
|
76
|
+
const serverRoutes = await loadServerManifest(options.coreOutput);
|
|
77
|
+
await rm(options.outDir, { recursive: true, force: true });
|
|
78
|
+
await mkdir(join(options.outDir, 'static'), { recursive: true });
|
|
79
|
+
await cp(staticDir, join(options.outDir, 'static'), { recursive: true, force: true });
|
|
80
|
+
for (const route of serverRoutes) {
|
|
81
|
+
const functionDir = join(options.outDir, 'functions', '__zenith', `${route.name}.func`);
|
|
82
|
+
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', 'routes', route.name), functionDir, { recursive: true, force: true });
|
|
88
|
+
await writeFile(join(functionDir, 'package.json'), '{\n "type": "module"\n}\n', 'utf8');
|
|
89
|
+
await writeFile(join(functionDir, 'index.js'), createFunctionSource(route), 'utf8');
|
|
90
|
+
await writeFile(join(functionDir, '.vc-config.json'), `${JSON.stringify({
|
|
91
|
+
runtime: 'nodejs22.x',
|
|
92
|
+
handler: 'index.js',
|
|
93
|
+
launcherType: 'Nodejs',
|
|
94
|
+
shouldAddHelpers: true
|
|
95
|
+
}, null, 2)}\n`, 'utf8');
|
|
96
|
+
}
|
|
97
|
+
await writeFile(join(options.outDir, 'config.json'), `${JSON.stringify(buildVercelConfig(options.manifest, serverRoutes), null, 2)}\n`, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isConfigKeyExplicit, isLoadedConfig } from '../config.js';
|
|
2
|
+
import { netlifyAdapter } from './adapter-netlify.js';
|
|
3
|
+
import { nodeAdapter } from './adapter-node.js';
|
|
4
|
+
import { netlifyStaticAdapter } from './adapter-netlify-static.js';
|
|
5
|
+
import { staticAdapter } from './adapter-static.js';
|
|
6
|
+
import { KNOWN_TARGETS } from './adapter-types.js';
|
|
7
|
+
import { vercelAdapter } from './adapter-vercel.js';
|
|
8
|
+
import { vercelStaticAdapter } from './adapter-vercel-static.js';
|
|
9
|
+
const LEGACY_ADAPTER = {
|
|
10
|
+
name: 'legacy',
|
|
11
|
+
validateRoutes() {
|
|
12
|
+
// Internal build() callers without loaded config stay on the pre-target contract.
|
|
13
|
+
},
|
|
14
|
+
adapt: staticAdapter.adapt
|
|
15
|
+
};
|
|
16
|
+
function validateAdapterShape(adapter) {
|
|
17
|
+
if (!adapter || typeof adapter !== 'object' || Array.isArray(adapter)) {
|
|
18
|
+
throw new Error('[Zenith:Config] Key "adapter" must be a plain object');
|
|
19
|
+
}
|
|
20
|
+
if (typeof adapter.name !== 'string' || adapter.name.trim().length === 0) {
|
|
21
|
+
throw new Error('[Zenith:Config] Key "adapter.name" must be a non-empty string');
|
|
22
|
+
}
|
|
23
|
+
if (typeof adapter.validateRoutes !== 'function') {
|
|
24
|
+
throw new Error('[Zenith:Config] Key "adapter.validateRoutes" must be a function');
|
|
25
|
+
}
|
|
26
|
+
if (typeof adapter.adapt !== 'function') {
|
|
27
|
+
throw new Error('[Zenith:Config] Key "adapter.adapt" must be a function');
|
|
28
|
+
}
|
|
29
|
+
return adapter;
|
|
30
|
+
}
|
|
31
|
+
function resolveTargetAdapter(target) {
|
|
32
|
+
if (target === 'static') {
|
|
33
|
+
return staticAdapter;
|
|
34
|
+
}
|
|
35
|
+
if (target === 'vercel-static') {
|
|
36
|
+
return vercelStaticAdapter;
|
|
37
|
+
}
|
|
38
|
+
if (target === 'netlify-static') {
|
|
39
|
+
return netlifyStaticAdapter;
|
|
40
|
+
}
|
|
41
|
+
if (target === 'vercel') {
|
|
42
|
+
return vercelAdapter;
|
|
43
|
+
}
|
|
44
|
+
if (target === 'netlify') {
|
|
45
|
+
return netlifyAdapter;
|
|
46
|
+
}
|
|
47
|
+
if (target === 'node') {
|
|
48
|
+
return nodeAdapter;
|
|
49
|
+
}
|
|
50
|
+
if (KNOWN_TARGETS.includes(target)) {
|
|
51
|
+
throw new Error(`[Zenith:Build] Target "${target}" is not supported yet.`);
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`[Zenith:Config] Unsupported target: "${target}"`);
|
|
54
|
+
}
|
|
55
|
+
export function resolveBuildAdapter(config = {}) {
|
|
56
|
+
const targetExplicit = isConfigKeyExplicit(config, 'target');
|
|
57
|
+
const adapterExplicit = isConfigKeyExplicit(config, 'adapter') && config.adapter !== null && config.adapter !== undefined;
|
|
58
|
+
if (targetExplicit && adapterExplicit) {
|
|
59
|
+
throw new Error('[Zenith:Config] Keys "target" and "adapter" are mutually exclusive');
|
|
60
|
+
}
|
|
61
|
+
if (adapterExplicit) {
|
|
62
|
+
const adapter = validateAdapterShape(config.adapter);
|
|
63
|
+
return {
|
|
64
|
+
target: adapter.name,
|
|
65
|
+
adapter,
|
|
66
|
+
mode: 'adapter'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (targetExplicit || isLoadedConfig(config)) {
|
|
70
|
+
const target = typeof config.target === 'string' && config.target.trim().length > 0
|
|
71
|
+
? config.target.trim()
|
|
72
|
+
: 'static';
|
|
73
|
+
return {
|
|
74
|
+
target,
|
|
75
|
+
adapter: resolveTargetAdapter(target),
|
|
76
|
+
mode: 'target'
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
target: 'legacy',
|
|
81
|
+
adapter: LEGACY_ADAPTER,
|
|
82
|
+
mode: 'legacy'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function createNetlifyBasePathAssetRules(basePath: any): string[];
|
|
2
|
+
export function createNetlifyRewriteRules(route: any, basePath?: string): string[];
|
|
3
|
+
export function createVercelBasePathAssetRoutes(basePath: any): {
|
|
4
|
+
src: string;
|
|
5
|
+
dest: string;
|
|
6
|
+
}[];
|
|
7
|
+
export function createVercelRouteSource(routePath: any, basePath?: string): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { normalizeBasePath, prependBasePath } from '../base-path.js';
|
|
2
|
+
function splitRouteSegments(routePath) {
|
|
3
|
+
return String(routePath || '').split('/').filter(Boolean);
|
|
4
|
+
}
|
|
5
|
+
function escapeRegex(value) {
|
|
6
|
+
return String(value).replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
|
|
7
|
+
}
|
|
8
|
+
function buildNetlifyPatternSegment(segment) {
|
|
9
|
+
if (segment.startsWith(':')) {
|
|
10
|
+
return segment;
|
|
11
|
+
}
|
|
12
|
+
return segment;
|
|
13
|
+
}
|
|
14
|
+
export function createNetlifyBasePathAssetRules(basePath) {
|
|
15
|
+
const normalizedBasePath = normalizeBasePath(basePath);
|
|
16
|
+
if (normalizedBasePath === '/') {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return [
|
|
20
|
+
`${prependBasePath(normalizedBasePath, '/assets/*')} /assets/:splat 200`,
|
|
21
|
+
`${prependBasePath(normalizedBasePath, '/_zenith/image/local/*')} /_zenith/image/local/:splat 200`
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
export function createNetlifyRewriteRules(route, basePath = '/') {
|
|
25
|
+
const segments = splitRouteSegments(route.path);
|
|
26
|
+
if (segments.length === 0) {
|
|
27
|
+
return [`${prependBasePath(basePath, '/')} ${route.html} 200`];
|
|
28
|
+
}
|
|
29
|
+
const terminal = segments[segments.length - 1];
|
|
30
|
+
const prefixSegments = segments.slice(0, -1).map(buildNetlifyPatternSegment);
|
|
31
|
+
const prefix = prefixSegments.length > 0 ? `/${prefixSegments.join('/')}` : '';
|
|
32
|
+
if (terminal.startsWith('*') && terminal.endsWith('?')) {
|
|
33
|
+
const exactPath = prefix || '/';
|
|
34
|
+
const splatPath = prefix ? `${prefix}/*` : '/*';
|
|
35
|
+
return [
|
|
36
|
+
`${prependBasePath(basePath, exactPath)} ${route.html} 200`,
|
|
37
|
+
`${prependBasePath(basePath, splatPath)} ${route.html} 200`
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
if (terminal.startsWith('*')) {
|
|
41
|
+
const splatPath = prefix ? `${prefix}/*` : '/*';
|
|
42
|
+
return [`${prependBasePath(basePath, splatPath)} ${route.html} 200`];
|
|
43
|
+
}
|
|
44
|
+
const path = `/${segments.map(buildNetlifyPatternSegment).join('/')}`;
|
|
45
|
+
return [`${prependBasePath(basePath, path)} ${route.html} 200`];
|
|
46
|
+
}
|
|
47
|
+
export function createVercelBasePathAssetRoutes(basePath) {
|
|
48
|
+
const normalizedBasePath = normalizeBasePath(basePath);
|
|
49
|
+
if (normalizedBasePath === '/') {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const escaped = escapeRegex(normalizedBasePath);
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
src: `^${escaped}/assets/(.+)$`,
|
|
56
|
+
dest: '/assets/$1'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
src: `^${escaped}/_zenith/image/local/(.+)$`,
|
|
60
|
+
dest: '/_zenith/image/local/$1'
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
export function createVercelRouteSource(routePath, basePath = '/') {
|
|
65
|
+
const segments = splitRouteSegments(routePath);
|
|
66
|
+
if (segments.length === 0) {
|
|
67
|
+
const rootPath = prependBasePath(basePath, '/');
|
|
68
|
+
return rootPath === '/' ? '^/?$' : `^${escapeRegex(rootPath)}/?$`;
|
|
69
|
+
}
|
|
70
|
+
let pattern = `^${escapeRegex(normalizeBasePath(basePath) === '/' ? '' : normalizeBasePath(basePath))}`;
|
|
71
|
+
for (const segment of segments) {
|
|
72
|
+
if (segment.startsWith(':')) {
|
|
73
|
+
pattern += '/([^/]+)';
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (segment.startsWith('*') && segment.endsWith('?')) {
|
|
77
|
+
pattern += '(?:/(.*))?';
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (segment.startsWith('*')) {
|
|
81
|
+
pattern += '/(.+)';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
pattern += `/${escapeRegex(segment)}`;
|
|
85
|
+
}
|
|
86
|
+
pattern += '/?$';
|
|
87
|
+
return pattern;
|
|
88
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { prependBasePath } from './base-path.js';
|
|
4
|
+
const SOFT_LINK_RE = /<a\b([^>]*\bdata-zen-link(?:=(["']).*?\2)?[^>]*)\bhref=(["'])(\/(?!\/)[^"']*)\3/gi;
|
|
5
|
+
export function rewriteSoftNavigationHrefBasePath(html, basePath) {
|
|
6
|
+
return String(html || '').replace(SOFT_LINK_RE, (match, beforeHref, _attrQuote, hrefQuote, hrefValue) => {
|
|
7
|
+
const nextHref = prependBasePath(basePath, hrefValue);
|
|
8
|
+
if (nextHref === hrefValue) {
|
|
9
|
+
return match;
|
|
10
|
+
}
|
|
11
|
+
return `<a${beforeHref}href=${hrefQuote}${nextHref}${hrefQuote}`;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function rewriteSoftNavigationHrefBasePathInHtmlFiles(rootDir, basePath) {
|
|
15
|
+
async function walk(dir) {
|
|
16
|
+
let entries = [];
|
|
17
|
+
try {
|
|
18
|
+
entries = await readdir(dir);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
entries.sort((left, right) => left.localeCompare(right));
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const fullPath = join(dir, entry);
|
|
26
|
+
const info = await stat(fullPath);
|
|
27
|
+
if (info.isDirectory()) {
|
|
28
|
+
await walk(fullPath);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!entry.endsWith('.html')) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const html = await readFile(fullPath, 'utf8');
|
|
35
|
+
const next = rewriteSoftNavigationHrefBasePath(html, basePath);
|
|
36
|
+
if (next !== html) {
|
|
37
|
+
await writeFile(fullPath, next, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await walk(rootDir);
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function normalizeBasePath(value: any): string;
|
|
2
|
+
export function normalizePublicPath(pathname: any): string;
|
|
3
|
+
export function prependBasePath(basePath: any, pathname?: string): string;
|
|
4
|
+
export function stripBasePath(pathname: any, basePath: any): string | null;
|
|
5
|
+
export function isWithinBasePath(pathname: any, basePath: any): boolean;
|
|
6
|
+
export function appLocalRedirectLocation(location: any, basePath: any): string;
|
|
7
|
+
export function routeCheckPath(basePath: any): string;
|
|
8
|
+
export function imageEndpointPath(basePath: any): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const ROOT_BASE_PATH = '/';
|
|
2
|
+
function normalizeLeadingSlash(value) {
|
|
3
|
+
const raw = String(value || '').trim();
|
|
4
|
+
if (!raw) {
|
|
5
|
+
return ROOT_BASE_PATH;
|
|
6
|
+
}
|
|
7
|
+
const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
|
|
8
|
+
return withLeadingSlash.replace(/\/{2,}/g, '/');
|
|
9
|
+
}
|
|
10
|
+
export function normalizeBasePath(value) {
|
|
11
|
+
const raw = String(value ?? ROOT_BASE_PATH).trim();
|
|
12
|
+
if (!raw || raw === ROOT_BASE_PATH) {
|
|
13
|
+
return ROOT_BASE_PATH;
|
|
14
|
+
}
|
|
15
|
+
if (raw.includes('?') || raw.includes('#')) {
|
|
16
|
+
throw new Error('[Zenith:Config] Key "basePath" must not include query or hash fragments');
|
|
17
|
+
}
|
|
18
|
+
if (!raw.startsWith('/')) {
|
|
19
|
+
throw new Error('[Zenith:Config] Key "basePath" must start with "/"');
|
|
20
|
+
}
|
|
21
|
+
const normalized = normalizeLeadingSlash(raw).replace(/\/+$/g, '');
|
|
22
|
+
return normalized || ROOT_BASE_PATH;
|
|
23
|
+
}
|
|
24
|
+
export function normalizePublicPath(pathname) {
|
|
25
|
+
const normalized = normalizeLeadingSlash(pathname);
|
|
26
|
+
if (normalized.length > 1) {
|
|
27
|
+
return normalized.replace(/\/+$/g, '');
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
export function prependBasePath(basePath, pathname = ROOT_BASE_PATH) {
|
|
32
|
+
const normalizedBasePath = normalizeBasePath(basePath);
|
|
33
|
+
const normalizedPath = normalizePublicPath(pathname);
|
|
34
|
+
if (normalizedBasePath === ROOT_BASE_PATH) {
|
|
35
|
+
return normalizedPath;
|
|
36
|
+
}
|
|
37
|
+
if (normalizedPath === ROOT_BASE_PATH) {
|
|
38
|
+
return normalizedBasePath;
|
|
39
|
+
}
|
|
40
|
+
if (normalizedPath === normalizedBasePath || normalizedPath.startsWith(`${normalizedBasePath}/`)) {
|
|
41
|
+
return normalizedPath;
|
|
42
|
+
}
|
|
43
|
+
return `${normalizedBasePath}${normalizedPath}`;
|
|
44
|
+
}
|
|
45
|
+
export function stripBasePath(pathname, basePath) {
|
|
46
|
+
const normalizedBasePath = normalizeBasePath(basePath);
|
|
47
|
+
const normalizedPath = normalizePublicPath(pathname);
|
|
48
|
+
if (normalizedBasePath === ROOT_BASE_PATH) {
|
|
49
|
+
return normalizedPath;
|
|
50
|
+
}
|
|
51
|
+
if (normalizedPath === normalizedBasePath) {
|
|
52
|
+
return ROOT_BASE_PATH;
|
|
53
|
+
}
|
|
54
|
+
if (normalizedPath.startsWith(`${normalizedBasePath}/`)) {
|
|
55
|
+
return normalizedPath.slice(normalizedBasePath.length) || ROOT_BASE_PATH;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
export function isWithinBasePath(pathname, basePath) {
|
|
60
|
+
return stripBasePath(pathname, basePath) !== null;
|
|
61
|
+
}
|
|
62
|
+
export function appLocalRedirectLocation(location, basePath) {
|
|
63
|
+
const value = typeof location === 'string' ? location.trim() : '';
|
|
64
|
+
if (!value || !value.startsWith('/') || value.startsWith('//')) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
return prependBasePath(basePath, value);
|
|
68
|
+
}
|
|
69
|
+
export function routeCheckPath(basePath) {
|
|
70
|
+
return prependBasePath(basePath, '/__zenith/route-check');
|
|
71
|
+
}
|
|
72
|
+
export function imageEndpointPath(basePath) {
|
|
73
|
+
return prependBasePath(basePath, '/_zenith/image');
|
|
74
|
+
}
|
|
@@ -42,7 +42,7 @@ export function createTimedCompilerRunner(startupProfile: ReturnType<typeof impo
|
|
|
42
42
|
* @param {object | null} [logger]
|
|
43
43
|
* @param {boolean} [showInfo]
|
|
44
44
|
* @param {string|object} [bundlerBin]
|
|
45
|
-
* @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string }} [bundlerOptions]
|
|
45
|
+
* @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string, basePath?: string }} [bundlerOptions]
|
|
46
46
|
* @returns {Promise<void>}
|
|
47
47
|
*/
|
|
48
48
|
export function runBundler(envelope: object | object[], outDir: string, projectRoot: string, logger?: object | null, showInfo?: boolean, bundlerBin?: string | object, bundlerOptions?: {
|
|
@@ -51,6 +51,7 @@ export function runBundler(envelope: object | object[], outDir: string, projectR
|
|
|
51
51
|
changedRoutes?: string[];
|
|
52
52
|
fastPath?: boolean;
|
|
53
53
|
globalGraphHash?: string;
|
|
54
|
+
basePath?: string;
|
|
54
55
|
}): Promise<void>;
|
|
55
56
|
/**
|
|
56
57
|
* @param {string} rootDir
|
|
@@ -171,7 +171,7 @@ export function createTimedCompilerRunner(startupProfile, compilerTotals) {
|
|
|
171
171
|
* @param {object | null} [logger]
|
|
172
172
|
* @param {boolean} [showInfo]
|
|
173
173
|
* @param {string|object} [bundlerBin]
|
|
174
|
-
* @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string }} [bundlerOptions]
|
|
174
|
+
* @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string, basePath?: string }} [bundlerOptions]
|
|
175
175
|
* @returns {Promise<void>}
|
|
176
176
|
*/
|
|
177
177
|
export function runBundler(envelope, outDir, projectRoot, logger = null, showInfo = true, bundlerBin = resolveBundlerBin(projectRoot), bundlerOptions = {}) {
|
|
@@ -189,6 +189,9 @@ export function runBundler(envelope, outDir, projectRoot, logger = null, showInf
|
|
|
189
189
|
'--out-dir',
|
|
190
190
|
outDir
|
|
191
191
|
];
|
|
192
|
+
if (typeof bundlerOptions.basePath === 'string' && bundlerOptions.basePath.length > 0) {
|
|
193
|
+
bundlerArgs.push('--base-path', bundlerOptions.basePath);
|
|
194
|
+
}
|
|
192
195
|
if (bundlerOptions.devStableAssets === true) {
|
|
193
196
|
bundlerArgs.push('--dev-stable-assets');
|
|
194
197
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* registry: Map<string, string>
|
|
7
7
|
* compilerOpts: object
|
|
8
8
|
* compilerBin: string|object
|
|
9
|
-
*
|
|
9
|
+
* routerEnabled: boolean
|
|
10
10
|
* startupProfile: ReturnType<import('../startup-profile.js').createStartupProfiler>
|
|
11
11
|
* compilerTotals: Record<string, number>
|
|
12
12
|
* emitCompilerWarning: (line: string) => void
|
|
@@ -23,7 +23,7 @@ export function buildPageEnvelopes(input: {
|
|
|
23
23
|
registry: Map<string, string>;
|
|
24
24
|
compilerOpts: object;
|
|
25
25
|
compilerBin: string | object;
|
|
26
|
-
|
|
26
|
+
routerEnabled: boolean;
|
|
27
27
|
startupProfile: ReturnType<typeof import("../startup-profile.js").createStartupProfiler>;
|
|
28
28
|
compilerTotals: Record<string, number>;
|
|
29
29
|
emitCompilerWarning: (line: string) => void;
|
package/dist/build/page-loop.js
CHANGED
|
@@ -22,7 +22,7 @@ import { extractServerScript } from './server-script.js';
|
|
|
22
22
|
* registry: Map<string, string>
|
|
23
23
|
* compilerOpts: object
|
|
24
24
|
* compilerBin: string|object
|
|
25
|
-
*
|
|
25
|
+
* routerEnabled: boolean
|
|
26
26
|
* startupProfile: ReturnType<import('../startup-profile.js').createStartupProfiler>
|
|
27
27
|
* compilerTotals: Record<string, number>
|
|
28
28
|
* emitCompilerWarning: (line: string) => void
|
|
@@ -30,7 +30,7 @@ import { extractServerScript } from './server-script.js';
|
|
|
30
30
|
* @returns {Promise<{ envelopes: object[], expressionRewriteMetrics: Record<string, number> }>}
|
|
31
31
|
*/
|
|
32
32
|
export async function buildPageEnvelopes(input) {
|
|
33
|
-
const { manifest, pagesDir, srcDir, registry, compilerOpts, compilerBin,
|
|
33
|
+
const { manifest, pagesDir, srcDir, registry, compilerOpts, compilerBin, routerEnabled, startupProfile, compilerTotals, emitCompilerWarning } = input;
|
|
34
34
|
const cacheState = input.pageLoopCaches || createPageLoopCaches();
|
|
35
35
|
const executionState = createPageLoopExecutionState();
|
|
36
36
|
const { componentIrCache, componentDocumentModeCache, componentExpressionRewriteCache, templateExpressionCache, hoistedCodeTransformCache } = cacheState;
|
|
@@ -182,7 +182,7 @@ export async function buildPageEnvelopes(input) {
|
|
|
182
182
|
route: entry.path,
|
|
183
183
|
file: sourceFile,
|
|
184
184
|
ir: pageIr,
|
|
185
|
-
router:
|
|
185
|
+
router: routerEnabled
|
|
186
186
|
});
|
|
187
187
|
recordPageProfile({
|
|
188
188
|
pageProfiles,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath }: {
|
|
2
|
+
coreOutputDir: any;
|
|
3
|
+
staticDir: any;
|
|
4
|
+
target: any;
|
|
5
|
+
routeManifest: any;
|
|
6
|
+
basePath?: string | undefined;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
schema_version: number;
|
|
9
|
+
zenith_version: any;
|
|
10
|
+
target: any;
|
|
11
|
+
base_path: string;
|
|
12
|
+
content_hash: any;
|
|
13
|
+
routes: {
|
|
14
|
+
path: any;
|
|
15
|
+
file: any;
|
|
16
|
+
path_kind: any;
|
|
17
|
+
render_mode: any;
|
|
18
|
+
requires_hydration: boolean;
|
|
19
|
+
params: any[];
|
|
20
|
+
html: any;
|
|
21
|
+
assets: any[];
|
|
22
|
+
}[];
|
|
23
|
+
assets: {
|
|
24
|
+
js: any[];
|
|
25
|
+
css: any[];
|
|
26
|
+
vendor: any;
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const CLI_VERSION = (() => {
|
|
5
|
+
try {
|
|
6
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return '0.0.0';
|
|
11
|
+
}
|
|
12
|
+
})();
|
|
13
|
+
async function readJson(filePath, fallback) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function extractAssetRefs(html) {
|
|
22
|
+
const refs = new Set();
|
|
23
|
+
for (const match of html.matchAll(/<script\b[^>]*\bsrc="([^"]+)"/gi)) {
|
|
24
|
+
refs.add(String(match[1] || ''));
|
|
25
|
+
}
|
|
26
|
+
for (const match of html.matchAll(/<link\b[^>]*\brel="stylesheet"[^>]*\bhref="([^"]+)"/gi)) {
|
|
27
|
+
refs.add(String(match[1] || ''));
|
|
28
|
+
}
|
|
29
|
+
return [...refs]
|
|
30
|
+
.map((value) => value.trim())
|
|
31
|
+
.filter((value) => value.length > 0)
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
34
|
+
async function readRouteHtml(staticDir, htmlPath) {
|
|
35
|
+
try {
|
|
36
|
+
return await readFile(join(staticDir, htmlPath.replace(/^\//, '')), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath = '/' }) {
|
|
43
|
+
const bundlerManifest = await readJson(join(staticDir, 'manifest.json'), {});
|
|
44
|
+
const routerManifest = await readJson(join(staticDir, 'assets', 'router-manifest.json'), { routes: [] });
|
|
45
|
+
const routeByPath = new Map((Array.isArray(routerManifest.routes) ? routerManifest.routes : []).map((entry) => [entry.path, entry]));
|
|
46
|
+
const routes = [];
|
|
47
|
+
const routeAssetJs = new Set();
|
|
48
|
+
const routeAssetCss = new Set();
|
|
49
|
+
for (const entry of routeManifest) {
|
|
50
|
+
const routeMeta = routeByPath.get(entry.path);
|
|
51
|
+
const htmlPath = typeof routeMeta?.output === 'string' ? routeMeta.output : '/index.html';
|
|
52
|
+
const html = await readRouteHtml(staticDir, htmlPath);
|
|
53
|
+
const assets = extractAssetRefs(html);
|
|
54
|
+
for (const asset of assets) {
|
|
55
|
+
if (asset.endsWith('.css')) {
|
|
56
|
+
routeAssetCss.add(asset);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (asset.endsWith('.js')) {
|
|
60
|
+
routeAssetJs.add(asset);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
routes.push({
|
|
64
|
+
path: entry.path,
|
|
65
|
+
file: entry.file,
|
|
66
|
+
path_kind: entry.path_kind,
|
|
67
|
+
render_mode: entry.render_mode,
|
|
68
|
+
requires_hydration: /<script\b[^>]*type="module"/i.test(html),
|
|
69
|
+
params: [...entry.params],
|
|
70
|
+
html: htmlPath,
|
|
71
|
+
assets
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const jsAssets = new Set([
|
|
75
|
+
bundlerManifest.entry,
|
|
76
|
+
bundlerManifest.core,
|
|
77
|
+
bundlerManifest.router,
|
|
78
|
+
...Object.values(bundlerManifest.chunks || {}),
|
|
79
|
+
...routeAssetJs
|
|
80
|
+
].filter((value) => typeof value === 'string' && value.endsWith('.js')));
|
|
81
|
+
const cssAssets = new Set([
|
|
82
|
+
bundlerManifest.css,
|
|
83
|
+
...routeAssetCss
|
|
84
|
+
].filter((value) => typeof value === 'string' && value.endsWith('.css')));
|
|
85
|
+
const buildManifest = {
|
|
86
|
+
schema_version: 1,
|
|
87
|
+
zenith_version: CLI_VERSION,
|
|
88
|
+
target,
|
|
89
|
+
base_path: basePath,
|
|
90
|
+
content_hash: typeof bundlerManifest.hash === 'string' ? bundlerManifest.hash : '',
|
|
91
|
+
routes,
|
|
92
|
+
assets: {
|
|
93
|
+
js: [...jsAssets].sort(),
|
|
94
|
+
css: [...cssAssets].sort(),
|
|
95
|
+
vendor: typeof bundlerManifest.vendor === 'string' ? bundlerManifest.vendor : null
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
await writeFile(join(coreOutputDir, 'manifest.json'), `${JSON.stringify(buildManifest, null, 2)}\n`, 'utf8');
|
|
99
|
+
return buildManifest;
|
|
100
|
+
}
|