@zenithbuild/cli 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/adapters/adapter-netlify-static.d.ts +2 -5
- package/dist/adapters/adapter-netlify.d.ts +2 -5
- package/dist/adapters/adapter-netlify.js +22 -5
- package/dist/adapters/adapter-types.d.ts +32 -13
- package/dist/adapters/adapter-types.js +0 -59
- package/dist/adapters/adapter-vercel-static.d.ts +2 -5
- package/dist/adapters/adapter-vercel.d.ts +2 -5
- package/dist/adapters/adapter-vercel.js +21 -6
- package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
- package/dist/adapters/copy-hosted-page-runtime.js +68 -3
- package/dist/adapters/resolve-adapter.d.ts +6 -4
- package/dist/build/compiler-runtime.js +3 -0
- package/dist/build/expression-rewrites.d.ts +3 -1
- package/dist/build/expression-rewrites.js +14 -2
- package/dist/build/page-component-loop.d.ts +1 -0
- package/dist/build/page-component-loop.js +66 -6
- package/dist/build/page-ir-normalization.js +7 -0
- package/dist/build/page-loop-state.d.ts +2 -4
- package/dist/build/page-loop-state.js +17 -9
- package/dist/build/page-loop.js +18 -8
- package/dist/build/scoped-expression-context.d.ts +5 -0
- package/dist/build/scoped-expression-context.js +133 -0
- package/dist/build/server-script.js +13 -36
- package/dist/build/type-declarations.d.ts +2 -1
- package/dist/build/type-declarations.js +29 -52
- package/dist/build-output-manifest.d.ts +10 -6
- package/dist/build-output-manifest.js +4 -1
- package/dist/build.js +11 -2
- package/dist/component-instance-ir.js +1 -0
- package/dist/component-occurrences.d.ts +9 -0
- package/dist/component-occurrences.js +18 -0
- package/dist/config-plugins.d.ts +12 -0
- package/dist/config-plugins.js +100 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +56 -5
- package/dist/dev-build-session/helpers.js +27 -7
- package/dist/dev-build-session/session.js +19 -10
- package/dist/dev-server/build-error-response.d.ts +21 -0
- package/dist/dev-server/build-error-response.js +48 -0
- package/dist/dev-server/port-fallback.d.ts +15 -0
- package/dist/dev-server/port-fallback.js +61 -0
- package/dist/dev-server/request-handler.js +58 -5
- package/dist/dev-server/watcher.js +15 -0
- package/dist/dev-server.d.ts +5 -2
- package/dist/dev-server.js +129 -49
- package/dist/global-middleware-runtime-source.d.ts +15 -0
- package/dist/global-middleware-runtime-source.js +62 -0
- package/dist/global-middleware.d.ts +13 -0
- package/dist/global-middleware.js +252 -0
- package/dist/images/remote-fetch.d.ts +12 -0
- package/dist/images/remote-fetch.js +257 -0
- package/dist/images/service.d.ts +10 -0
- package/dist/images/service.js +9 -46
- package/dist/index.js +12 -2
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +70 -25
- package/dist/preview/request-handler.js +78 -5
- package/dist/preview/server-runner.d.ts +7 -2
- package/dist/preview/server-runner.js +19 -6
- package/dist/preview/server-script-runner-template.js +97 -29
- package/dist/resource-response.js +25 -8
- package/dist/resource-route-module.js +5 -22
- package/dist/route-classification.d.ts +11 -0
- package/dist/route-classification.js +21 -0
- package/dist/route-handler-export-analysis.d.ts +22 -0
- package/dist/route-handler-export-analysis.js +41 -0
- package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
- package/dist/scoped-server-data/analyze-owner-file.js +149 -0
- package/dist/scoped-server-data/diagnostics.d.ts +18 -0
- package/dist/scoped-server-data/diagnostics.js +32 -0
- package/dist/scoped-server-data/lowering.d.ts +27 -0
- package/dist/scoped-server-data/lowering.js +242 -0
- package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
- package/dist/scoped-server-data/manifest-integration.js +125 -0
- package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
- package/dist/scoped-server-data/owner-scanner.js +55 -0
- package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
- package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
- package/dist/scoped-server-data/runtime.d.ts +24 -0
- package/dist/scoped-server-data/runtime.js +121 -0
- package/dist/scoped-server-data/serialization-set.d.ts +2 -0
- package/dist/scoped-server-data/serialization-set.js +52 -0
- package/dist/scoped-server-data/static-props.d.ts +12 -0
- package/dist/scoped-server-data/static-props.js +307 -0
- package/dist/scoped-server-data/type-declarations.d.ts +10 -0
- package/dist/scoped-server-data/type-declarations.js +368 -0
- package/dist/scoped-server-data/types.d.ts +74 -0
- package/dist/scoped-server-data/types.js +1 -0
- package/dist/server-contract/auth-control-flow.d.ts +1 -0
- package/dist/server-contract/auth-control-flow.js +10 -0
- package/dist/server-contract/resolve.d.ts +19 -0
- package/dist/server-contract/resolve.js +85 -13
- package/dist/server-contract/resolved-envelope.d.ts +9 -0
- package/dist/server-contract/resolved-envelope.js +14 -0
- package/dist/server-contract/stage.js +1 -10
- package/dist/server-module-output.d.ts +9 -0
- package/dist/server-module-output.js +250 -0
- package/dist/server-output.d.ts +7 -1
- package/dist/server-output.js +144 -195
- package/dist/server-route-names.d.ts +2 -0
- package/dist/server-route-names.js +38 -0
- package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
- package/dist/server-runtime/matched-route-pipeline.js +1 -0
- package/dist/server-runtime/node-server.js +26 -3
- package/dist/server-runtime/route-render.d.ts +12 -3
- package/dist/server-runtime/route-render.js +67 -13
- package/dist/types/generate-env-dts.js +2 -44
- package/dist/types/zenith-env-dts.d.ts +4 -0
- package/dist/types/zenith-env-dts.js +96 -0
- package/package.json +3 -6
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath }: {
|
|
1
|
+
export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath, globalMiddleware }: {
|
|
2
2
|
coreOutputDir: any;
|
|
3
3
|
staticDir: any;
|
|
4
4
|
target: any;
|
|
5
5
|
routeManifest: any;
|
|
6
6
|
basePath?: string | undefined;
|
|
7
|
+
globalMiddleware?: null | undefined;
|
|
7
8
|
}): Promise<{
|
|
8
|
-
schema_version: number;
|
|
9
|
-
zenith_version: any;
|
|
10
|
-
target: any;
|
|
11
|
-
base_path: string;
|
|
12
|
-
content_hash: any;
|
|
13
9
|
routes: {
|
|
14
10
|
html: any;
|
|
15
11
|
assets: any[];
|
|
@@ -26,4 +22,12 @@ export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, rou
|
|
|
26
22
|
css: any[];
|
|
27
23
|
vendor: any;
|
|
28
24
|
};
|
|
25
|
+
global_middleware?: {
|
|
26
|
+
source_file: any;
|
|
27
|
+
} | undefined;
|
|
28
|
+
schema_version: number;
|
|
29
|
+
zenith_version: any;
|
|
30
|
+
target: any;
|
|
31
|
+
base_path: string;
|
|
32
|
+
content_hash: any;
|
|
29
33
|
}>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { normalizeGlobalMiddlewareMetadata } from './global-middleware.js';
|
|
4
5
|
const CLI_VERSION = (() => {
|
|
5
6
|
try {
|
|
6
7
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -39,7 +40,7 @@ async function readRouteHtml(staticDir, htmlPath) {
|
|
|
39
40
|
return '';
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
export async function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath = '/' }) {
|
|
43
|
+
export async function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath = '/', globalMiddleware = null }) {
|
|
43
44
|
const bundlerManifest = await readJson(join(staticDir, 'manifest.json'), {});
|
|
44
45
|
const routerManifest = await readJson(join(staticDir, 'assets', 'router-manifest.json'), { routes: [] });
|
|
45
46
|
const routeByPath = new Map((Array.isArray(routerManifest.routes) ? routerManifest.routes : []).map((entry) => [entry.path, entry]));
|
|
@@ -85,12 +86,14 @@ export async function writeBuildOutputManifest({ coreOutputDir, staticDir, targe
|
|
|
85
86
|
bundlerManifest.css,
|
|
86
87
|
...routeAssetCss
|
|
87
88
|
].filter((value) => typeof value === 'string' && value.endsWith('.css')));
|
|
89
|
+
const globalMiddlewareMetadata = normalizeGlobalMiddlewareMetadata(globalMiddleware);
|
|
88
90
|
const buildManifest = {
|
|
89
91
|
schema_version: 1,
|
|
90
92
|
zenith_version: CLI_VERSION,
|
|
91
93
|
target,
|
|
92
94
|
base_path: basePath,
|
|
93
95
|
content_hash: typeof bundlerManifest.hash === 'string' ? bundlerManifest.hash : '',
|
|
96
|
+
...(globalMiddlewareMetadata ? { global_middleware: globalMiddlewareMetadata } : {}),
|
|
94
97
|
routes,
|
|
95
98
|
assets: {
|
|
96
99
|
js: [...jsAssets].sort(),
|
package/dist/build.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } fro
|
|
|
16
16
|
import { supportsTargetRouteCheck } from './route-check-support.js';
|
|
17
17
|
import { createStartupProfiler } from './startup-profile.js';
|
|
18
18
|
import { writeServerOutput } from './server-output.js';
|
|
19
|
+
import { resolveGlobalMiddleware } from './global-middleware.js';
|
|
19
20
|
import { resolveBundlerBin } from './toolchain-paths.js';
|
|
20
21
|
import { createBundlerToolchain, createCompilerToolchain, ensureToolchainCompatibility, getActiveToolchainCandidate } from './toolchain-runner.js';
|
|
21
22
|
import { maybeWarnAboutZenithVersionMismatch } from './version-check.js';
|
|
@@ -75,6 +76,7 @@ export async function build(options) {
|
|
|
75
76
|
}));
|
|
76
77
|
}
|
|
77
78
|
const registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
|
|
79
|
+
const globalMiddleware = await startupProfile.measureAsync('resolve_global_middleware', () => resolveGlobalMiddleware({ projectRoot, pagesDir, target }));
|
|
78
80
|
const manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(pagesDir, '.zen', { compilerOpts }));
|
|
79
81
|
const pageManifest = manifest.filter((entry) => entry?.route_kind !== 'resource');
|
|
80
82
|
if (mode !== 'legacy') {
|
|
@@ -136,14 +138,21 @@ export async function build(options) {
|
|
|
136
138
|
staticDir: staticOutputDir,
|
|
137
139
|
target,
|
|
138
140
|
routeManifest: pageManifest,
|
|
139
|
-
basePath
|
|
141
|
+
basePath,
|
|
142
|
+
globalMiddleware: globalMiddleware?.metadata || null
|
|
140
143
|
}));
|
|
141
144
|
await startupProfile.measureAsync('write_server_output', () => writeServerOutput({
|
|
142
145
|
coreOutputDir,
|
|
143
146
|
staticDir: staticOutputDir,
|
|
144
147
|
projectRoot,
|
|
145
148
|
config,
|
|
146
|
-
basePath
|
|
149
|
+
basePath,
|
|
150
|
+
globalMiddleware: globalMiddleware?.metadata || null,
|
|
151
|
+
pageManifest,
|
|
152
|
+
pagesDir,
|
|
153
|
+
srcDir,
|
|
154
|
+
registry,
|
|
155
|
+
compilerOpts
|
|
147
156
|
}));
|
|
148
157
|
await startupProfile.measureAsync('adapt_output', () => adapter.adapt({ coreOutput: coreOutputDir, outDir, manifest: buildManifest, config }));
|
|
149
158
|
const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
|
|
@@ -388,6 +388,7 @@ export function applyOccurrenceRewritePlans(pageIr, occurrencePlans, resolveBind
|
|
|
388
388
|
binding.state_index = resolved.state_index;
|
|
389
389
|
binding.component_instance = resolved.component_instance;
|
|
390
390
|
binding.component_binding = resolved.component_binding;
|
|
391
|
+
binding.scoped_data_key = resolved.scoped_data_key;
|
|
391
392
|
}
|
|
392
393
|
}
|
|
393
394
|
exprCursor = found + 1;
|
|
@@ -4,3 +4,12 @@ export function collectExpandedComponentOccurrences(source: any, registry: any,
|
|
|
4
4
|
ownerPath: string;
|
|
5
5
|
componentPath: string;
|
|
6
6
|
}[];
|
|
7
|
+
/**
|
|
8
|
+
* Unique layout/component `.zen` paths reachable from a page dependency graph.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} source
|
|
11
|
+
* @param {Map<string, string>} registry
|
|
12
|
+
* @param {string} sourceFile
|
|
13
|
+
* @returns {string[]}
|
|
14
|
+
*/
|
|
15
|
+
export function collectReachableOwnerPaths(source: string, registry: Map<string, string>, sourceFile: string): string[];
|
|
@@ -10,6 +10,24 @@ export function collectExpandedComponentOccurrences(source, registry, sourceFile
|
|
|
10
10
|
}, [], occurrences);
|
|
11
11
|
return occurrences;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Unique layout/component `.zen` paths reachable from a page dependency graph.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} source
|
|
17
|
+
* @param {Map<string, string>} registry
|
|
18
|
+
* @param {string} sourceFile
|
|
19
|
+
* @returns {string[]}
|
|
20
|
+
*/
|
|
21
|
+
export function collectReachableOwnerPaths(source, registry, sourceFile) {
|
|
22
|
+
const occurrences = collectExpandedComponentOccurrences(source, registry, sourceFile);
|
|
23
|
+
const paths = new Set();
|
|
24
|
+
for (const occurrence of occurrences) {
|
|
25
|
+
if (occurrence.componentPath) {
|
|
26
|
+
paths.add(occurrence.componentPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...paths];
|
|
30
|
+
}
|
|
13
31
|
function walkSource(source, registry, context, chain, occurrences) {
|
|
14
32
|
let cursor = 0;
|
|
15
33
|
while (cursor < source.length) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function normalizePlugins(value: any): ({
|
|
2
|
+
name: any;
|
|
3
|
+
config: any;
|
|
4
|
+
} | {
|
|
5
|
+
name: any;
|
|
6
|
+
config?: undefined;
|
|
7
|
+
})[];
|
|
8
|
+
export function assertPluginConfigPatch(value: any): void;
|
|
9
|
+
export function cloneConfigValue(value: any, seen?: Map<any, any>): any;
|
|
10
|
+
export function deepFreeze(value: any, seen?: Set<any>): any;
|
|
11
|
+
export function pluginHookError(pluginName: any, hookName: any, error: any): Error;
|
|
12
|
+
export const PLUGIN_CONFIG_PATCH_KEYS: Set<string>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const PLUGIN_OBJECT_KEYS = new Set(['name', 'config']);
|
|
2
|
+
const freezeObject = Object.freeze;
|
|
3
|
+
export const PLUGIN_CONFIG_PATCH_KEYS = new Set([
|
|
4
|
+
'router',
|
|
5
|
+
'embeddedMarkupExpressions',
|
|
6
|
+
'typescriptDefault',
|
|
7
|
+
'strictDomLints',
|
|
8
|
+
'images',
|
|
9
|
+
'basePath',
|
|
10
|
+
'outDir'
|
|
11
|
+
]);
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const proto = Object.getPrototypeOf(value);
|
|
17
|
+
return proto === Object.prototype || proto === null;
|
|
18
|
+
}
|
|
19
|
+
function describePlugin(index, plugin) {
|
|
20
|
+
if (plugin && typeof plugin === 'object' && typeof plugin.name === 'string' && plugin.name.trim()) {
|
|
21
|
+
return `"${plugin.name.trim()}"`;
|
|
22
|
+
}
|
|
23
|
+
return `at index ${index}`;
|
|
24
|
+
}
|
|
25
|
+
export function normalizePlugins(value) {
|
|
26
|
+
if (!Array.isArray(value)) {
|
|
27
|
+
throw new Error('[Zenith:Config] Key "plugins" must be an array');
|
|
28
|
+
}
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
return value.map((plugin, index) => {
|
|
31
|
+
if (!isPlainObject(plugin)) {
|
|
32
|
+
throw new Error(`[Zenith:Config] Plugin at index ${index} must be a plain object`);
|
|
33
|
+
}
|
|
34
|
+
for (const key of Object.keys(plugin)) {
|
|
35
|
+
if (!PLUGIN_OBJECT_KEYS.has(key)) {
|
|
36
|
+
throw new Error(`[Zenith:Config] Plugin ${describePlugin(index, plugin)} uses unsupported key "${key}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof plugin.name !== 'string' || plugin.name.trim().length === 0) {
|
|
40
|
+
throw new Error(`[Zenith:Config] Plugin at index ${index} must have a non-empty name`);
|
|
41
|
+
}
|
|
42
|
+
const name = plugin.name.trim();
|
|
43
|
+
if (seen.has(name)) {
|
|
44
|
+
throw new Error(`[Zenith:Config] Duplicate plugin name: "${name}"`);
|
|
45
|
+
}
|
|
46
|
+
seen.add(name);
|
|
47
|
+
if ('config' in plugin && typeof plugin.config !== 'function') {
|
|
48
|
+
throw new Error(`[Zenith:Config] Plugin "${name}" key "config" must be a function`);
|
|
49
|
+
}
|
|
50
|
+
return 'config' in plugin ? { name, config: plugin.config } : { name };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function assertPluginConfigPatch(value) {
|
|
54
|
+
if (!isPlainObject(value)) {
|
|
55
|
+
throw new Error('config hook must return a plain object patch');
|
|
56
|
+
}
|
|
57
|
+
for (const key of Object.keys(value)) {
|
|
58
|
+
if (!PLUGIN_CONFIG_PATCH_KEYS.has(key)) {
|
|
59
|
+
throw new Error(`${key} is not patchable`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function cloneConfigValue(value, seen = new Map()) {
|
|
64
|
+
if (!value || typeof value !== 'object') {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (seen.has(value)) {
|
|
68
|
+
return seen.get(value);
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
const out = [];
|
|
72
|
+
seen.set(value, out);
|
|
73
|
+
for (const item of value) {
|
|
74
|
+
out.push(cloneConfigValue(item, seen));
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
const out = {};
|
|
79
|
+
seen.set(value, out);
|
|
80
|
+
for (const [key, child] of Object.entries(value)) {
|
|
81
|
+
out[key] = cloneConfigValue(child, seen);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export function deepFreeze(value, seen = new Set()) {
|
|
86
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function') || seen.has(value)) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
seen.add(value);
|
|
90
|
+
for (const key of Object.keys(value)) {
|
|
91
|
+
deepFreeze(value[key], seen);
|
|
92
|
+
}
|
|
93
|
+
return freezeObject(value);
|
|
94
|
+
}
|
|
95
|
+
export function pluginHookError(pluginName, hookName, error) {
|
|
96
|
+
const message = error && typeof error.message === 'string'
|
|
97
|
+
? error.message
|
|
98
|
+
: String(error);
|
|
99
|
+
return new Error(`[Zenith plugin ${pluginName}] ${hookName} failed: ${message}`);
|
|
100
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join, resolve } from 'node:path';
|
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { KNOWN_TARGETS } from './adapters/adapter-types.js';
|
|
7
7
|
import { normalizeBasePath } from './base-path.js';
|
|
8
|
+
import { assertPluginConfigPatch, cloneConfigValue, deepFreeze, normalizePlugins, pluginHookError } from './config-plugins.js';
|
|
8
9
|
import { normalizeImageConfig } from './images/shared.js';
|
|
9
10
|
const PACKAGE_REQUIRE = createRequire(import.meta.url);
|
|
10
11
|
const CONFIG_FILES = ['zenith.config.ts', 'zenith.config.js'];
|
|
@@ -19,7 +20,8 @@ export const DEFAULT_CONFIG = {
|
|
|
19
20
|
target: 'static',
|
|
20
21
|
adapter: null,
|
|
21
22
|
strictDomLints: false,
|
|
22
|
-
images: normalizeImageConfig()
|
|
23
|
+
images: normalizeImageConfig(),
|
|
24
|
+
plugins: []
|
|
23
25
|
};
|
|
24
26
|
const TOP_LEVEL_SCHEMA = {
|
|
25
27
|
router: 'boolean',
|
|
@@ -31,7 +33,8 @@ const TOP_LEVEL_SCHEMA = {
|
|
|
31
33
|
target: 'string',
|
|
32
34
|
adapter: 'object',
|
|
33
35
|
strictDomLints: 'boolean',
|
|
34
|
-
images: 'object'
|
|
36
|
+
images: 'object',
|
|
37
|
+
plugins: 'array'
|
|
35
38
|
};
|
|
36
39
|
function attachConfigMeta(config, explicitKeys) {
|
|
37
40
|
Object.defineProperty(config, CONFIG_META, {
|
|
@@ -160,7 +163,7 @@ export function resolveConfigOutDir(projectRoot, config) {
|
|
|
160
163
|
}
|
|
161
164
|
export function validateConfig(config) {
|
|
162
165
|
if (config === null || config === undefined) {
|
|
163
|
-
return attachConfigMeta({ ...DEFAULT_CONFIG, images: normalizeImageConfig() }, []);
|
|
166
|
+
return attachConfigMeta({ ...DEFAULT_CONFIG, images: normalizeImageConfig(), plugins: [] }, []);
|
|
164
167
|
}
|
|
165
168
|
if (typeof config !== 'object' || Array.isArray(config)) {
|
|
166
169
|
throw new Error('[Zenith:Config] Config must be a plain object');
|
|
@@ -175,7 +178,8 @@ export function validateConfig(config) {
|
|
|
175
178
|
}
|
|
176
179
|
const result = {
|
|
177
180
|
...DEFAULT_CONFIG,
|
|
178
|
-
images: normalizeImageConfig(DEFAULT_CONFIG.images)
|
|
181
|
+
images: normalizeImageConfig(DEFAULT_CONFIG.images),
|
|
182
|
+
plugins: []
|
|
179
183
|
};
|
|
180
184
|
for (const [key, expectedType] of Object.entries(TOP_LEVEL_SCHEMA)) {
|
|
181
185
|
if (!(key in config)) {
|
|
@@ -190,6 +194,10 @@ export function validateConfig(config) {
|
|
|
190
194
|
result.adapter = validateAdapterValue(value);
|
|
191
195
|
continue;
|
|
192
196
|
}
|
|
197
|
+
if (key === 'plugins') {
|
|
198
|
+
result.plugins = normalizePlugins(value);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
193
201
|
if (typeof value !== expectedType) {
|
|
194
202
|
throw new Error(`[Zenith:Config] Key "${key}" must be ${expectedType}, got ${typeof value}`);
|
|
195
203
|
}
|
|
@@ -207,6 +215,48 @@ export function validateConfig(config) {
|
|
|
207
215
|
}
|
|
208
216
|
return attachConfigMeta(result, Object.keys(config));
|
|
209
217
|
}
|
|
218
|
+
function normalizeConfigPatch(patch) {
|
|
219
|
+
assertPluginConfigPatch(patch);
|
|
220
|
+
const keys = Object.keys(patch);
|
|
221
|
+
const normalized = validateConfig(patch);
|
|
222
|
+
const out = {};
|
|
223
|
+
for (const key of keys) {
|
|
224
|
+
out[key] = cloneConfigValue(normalized[key]);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
async function runPluginConfigHooks(config, projectRoot) {
|
|
229
|
+
let current = config;
|
|
230
|
+
for (const plugin of current.plugins) {
|
|
231
|
+
if (typeof plugin.config !== 'function') {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
let patch;
|
|
235
|
+
try {
|
|
236
|
+
const snapshot = deepFreeze(cloneConfigValue(current));
|
|
237
|
+
patch = await plugin.config(snapshot, { projectRoot });
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
throw pluginHookError(plugin.name, 'config', error);
|
|
241
|
+
}
|
|
242
|
+
if (patch === undefined || patch === null) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
let normalizedPatch;
|
|
246
|
+
try {
|
|
247
|
+
normalizedPatch = normalizeConfigPatch(patch);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
throw pluginHookError(plugin.name, 'config', error);
|
|
251
|
+
}
|
|
252
|
+
const explicitKeys = new Set(current?.[CONFIG_META]?.explicitKeys || []);
|
|
253
|
+
for (const key of Object.keys(normalizedPatch)) {
|
|
254
|
+
explicitKeys.add(key);
|
|
255
|
+
}
|
|
256
|
+
current = attachConfigMeta({ ...current, ...normalizedPatch, plugins: current.plugins }, explicitKeys);
|
|
257
|
+
}
|
|
258
|
+
return current;
|
|
259
|
+
}
|
|
210
260
|
export async function loadConfig(projectRoot) {
|
|
211
261
|
const resolvedProjectRoot = resolve(projectRoot);
|
|
212
262
|
const configPath = resolveConfigFile(resolvedProjectRoot);
|
|
@@ -216,5 +266,6 @@ export async function loadConfig(projectRoot) {
|
|
|
216
266
|
const mod = configPath.endsWith('.ts')
|
|
217
267
|
? await importTypescriptConfig(configPath, resolvedProjectRoot)
|
|
218
268
|
: await importJavascriptConfig(configPath, resolvedProjectRoot);
|
|
219
|
-
|
|
269
|
+
const config = validateConfig(mod.default || mod);
|
|
270
|
+
return markLoaded(await runPluginConfigHooks(config, resolvedProjectRoot));
|
|
220
271
|
}
|
|
@@ -143,18 +143,38 @@ function collectTemplateClassSignature(envelope) {
|
|
|
143
143
|
return [...classes].sort();
|
|
144
144
|
}
|
|
145
145
|
export function buildPageOnlyFastPathSignature(envelope) {
|
|
146
|
+
const ir = envelope.ir || {};
|
|
146
147
|
return stableJson({
|
|
147
148
|
route: envelope.route,
|
|
148
149
|
router: envelope.router === true,
|
|
150
|
+
interactivityShape: {
|
|
151
|
+
requiresJs: envelope.requires_js === true,
|
|
152
|
+
signals: Array.isArray(ir.signals) ? ir.signals.length : 0,
|
|
153
|
+
refs: Array.isArray(ir.ref_bindings) ? ir.ref_bindings.length : 0,
|
|
154
|
+
events: Array.isArray(ir.event_bindings)
|
|
155
|
+
? ir.event_bindings.map((entry) => [entry.index, entry.event]).sort()
|
|
156
|
+
: [],
|
|
157
|
+
markers: Array.isArray(ir.marker_bindings)
|
|
158
|
+
? ir.marker_bindings.map((entry) => [entry.index, entry.kind]).sort()
|
|
159
|
+
: [],
|
|
160
|
+
componentInstances: Array.isArray(ir.component_instances) ? ir.component_instances.length : 0,
|
|
161
|
+
hoistedCode: Array.isArray(ir.hoisted?.code)
|
|
162
|
+
? ir.hoisted.code.filter((entry) => String(entry || '').trim().length > 0).length
|
|
163
|
+
: 0,
|
|
164
|
+
hoistedState: Array.isArray(ir.hoisted?.state) ? ir.hoisted.state.length : 0,
|
|
165
|
+
hoistedSignals: Array.isArray(ir.hoisted?.signals) ? ir.hoisted.signals.length : 0
|
|
166
|
+
},
|
|
149
167
|
assetContract: collectEnvelopeAssetContract(envelope),
|
|
150
168
|
templateClassSignature: collectTemplateClassSignature(envelope),
|
|
151
|
-
styleBlocks:
|
|
152
|
-
serverScript:
|
|
153
|
-
prerender:
|
|
154
|
-
hasGuard:
|
|
155
|
-
hasLoad:
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
styleBlocks: ir.style_blocks || [],
|
|
170
|
+
serverScript: ir.server_script || null,
|
|
171
|
+
prerender: ir.prerender === true,
|
|
172
|
+
hasGuard: ir.has_guard === true,
|
|
173
|
+
hasLoad: ir.has_load === true,
|
|
174
|
+
hasAction: ir.has_action === true,
|
|
175
|
+
guardModuleRef: ir.guard_module_ref || null,
|
|
176
|
+
loadModuleRef: ir.load_module_ref || null,
|
|
177
|
+
actionModuleRef: ir.action_module_ref || null
|
|
158
178
|
});
|
|
159
179
|
}
|
|
160
180
|
export function buildGlobalGraphHash(envelopes) {
|
|
@@ -5,10 +5,12 @@ import { collectAssets, runBundler } from '../build/compiler-runtime.js';
|
|
|
5
5
|
import { buildPageEnvelopes } from '../build/page-loop.js';
|
|
6
6
|
import { createPageLoopCaches } from '../build/page-loop-state.js';
|
|
7
7
|
import { deriveProjectRootFromPagesDir, ensureZenithTypeDeclarations } from '../build/type-declarations.js';
|
|
8
|
+
import { rewriteSoftNavigationHrefBasePathInHtmlFiles } from '../base-path-html.js';
|
|
8
9
|
import { injectImageMaterializationIntoRouterManifest } from '../images/router-manifest.js';
|
|
9
10
|
import { buildImageArtifacts } from '../images/service.js';
|
|
10
11
|
import { materializeImageMarkupInHtmlFiles } from '../images/materialize.js';
|
|
11
12
|
import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } from '../images/payload.js';
|
|
13
|
+
import { writeResourceRouteManifest } from '../resource-manifest.js';
|
|
12
14
|
import { createStartupProfiler } from '../startup-profile.js';
|
|
13
15
|
import { resolveBuildAdapter } from '../adapters/resolve-adapter.js';
|
|
14
16
|
import { supportsTargetRouteCheck } from '../route-check-support.js';
|
|
@@ -34,6 +36,9 @@ export function createDevBuildSession(options) {
|
|
|
34
36
|
};
|
|
35
37
|
ensureToolchainCompatibility(bundlerBin);
|
|
36
38
|
const state = createDevBuildState(config, basePath);
|
|
39
|
+
function pageManifestEntries() {
|
|
40
|
+
return state.manifest.filter((entry) => entry?.route_kind !== 'resource');
|
|
41
|
+
}
|
|
37
42
|
async function syncImageState(startupProfile) {
|
|
38
43
|
const { manifest } = await startupProfile.measureAsync('build_image_artifacts', () => buildImageArtifacts({
|
|
39
44
|
projectRoot,
|
|
@@ -50,12 +55,13 @@ export function createDevBuildSession(options) {
|
|
|
50
55
|
}
|
|
51
56
|
async function runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo, bundlerOptions = {}) {
|
|
52
57
|
const orderedEnvelopes = bundlerOptions.envelopesOverride
|
|
53
|
-
|| orderEnvelopes(
|
|
58
|
+
|| orderEnvelopes(pageManifestEntries(), resolvedPagesDir, state.envelopeByFile);
|
|
54
59
|
if (!orderedEnvelopes || orderedEnvelopes.length === 0) {
|
|
55
60
|
throw new Error('Dev rebuild cache is incomplete; full rebuild required.');
|
|
56
61
|
}
|
|
57
62
|
await startupProfile.measureAsync('run_bundler', () => runBundler(orderedEnvelopes, outDir, projectRoot, activeLogger, showBundlerInfo, bundlerBin, {
|
|
58
63
|
routeCheck: routeCheckEnabled,
|
|
64
|
+
basePath,
|
|
59
65
|
devStableAssets: true,
|
|
60
66
|
rebuildStrategy: bundlerOptions.rebuildStrategy || 'full',
|
|
61
67
|
changedRoutes: bundlerOptions.changedRoutes || [],
|
|
@@ -63,6 +69,7 @@ export function createDevBuildSession(options) {
|
|
|
63
69
|
globalGraphHash: bundlerOptions.globalGraphHash || ''
|
|
64
70
|
}), { envelopes: orderedEnvelopes.length });
|
|
65
71
|
await startupProfile.measureAsync('inject_image_materialization_manifest', () => injectImageMaterializationIntoRouterManifest(outDir, orderedEnvelopes), { envelopes: orderedEnvelopes.length });
|
|
72
|
+
await startupProfile.measureAsync('rewrite_soft_navigation_base_path', () => rewriteSoftNavigationHrefBasePathInHtmlFiles(outDir, basePath));
|
|
66
73
|
const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
|
|
67
74
|
return { assets, envelopeCount: orderedEnvelopes.length };
|
|
68
75
|
}
|
|
@@ -78,14 +85,15 @@ export function createDevBuildSession(options) {
|
|
|
78
85
|
});
|
|
79
86
|
state.registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
|
|
80
87
|
state.manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(resolvedPagesDir));
|
|
88
|
+
const pageManifest = pageManifestEntries();
|
|
81
89
|
await startupProfile.measureAsync('ensure_zenith_type_declarations', () => ensureZenithTypeDeclarations({
|
|
82
|
-
manifest:
|
|
90
|
+
manifest: pageManifest,
|
|
83
91
|
pagesDir: resolvedPagesDir
|
|
84
92
|
}));
|
|
85
93
|
state.pageLoopCaches = createPageLoopCaches();
|
|
86
94
|
const emitCompilerWarning = buildCompilerWarningEmitter(activeLogger);
|
|
87
95
|
const { envelopes, expressionRewriteMetrics } = await buildPageEnvelopes({
|
|
88
|
-
manifest:
|
|
96
|
+
manifest: pageManifest,
|
|
89
97
|
pagesDir: resolvedPagesDir,
|
|
90
98
|
srcDir,
|
|
91
99
|
registry: state.registry,
|
|
@@ -98,20 +106,21 @@ export function createDevBuildSession(options) {
|
|
|
98
106
|
pageLoopCaches: state.pageLoopCaches
|
|
99
107
|
});
|
|
100
108
|
state.envelopeByFile = new Map(envelopes.map((entry) => [entry.file, entry]));
|
|
101
|
-
state.manifestEntryByPath = toManifestEntryMap(
|
|
109
|
+
state.manifestEntryByPath = toManifestEntryMap(pageManifest, resolvedPagesDir);
|
|
102
110
|
state.pageOnlyFastPathSignatureByFile = new Map(envelopes.map((entry) => [entry.file, buildPageOnlyFastPathSignature(entry)]));
|
|
103
111
|
state.globalGraphHash = buildGlobalGraphHash(envelopes);
|
|
104
112
|
const { assets } = await runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo);
|
|
113
|
+
await startupProfile.measureAsync('write_resource_manifest', () => writeResourceRouteManifest(outDir, state.manifest, basePath));
|
|
105
114
|
await syncImageState(startupProfile);
|
|
106
115
|
startupProfile.emit('build_complete', {
|
|
107
|
-
pages:
|
|
116
|
+
pages: pageManifest.length,
|
|
108
117
|
assets: assets.length,
|
|
109
118
|
compilerTotals,
|
|
110
119
|
expressionRewriteMetrics,
|
|
111
120
|
strategy: 'full'
|
|
112
121
|
});
|
|
113
122
|
state.hasSuccessfulBuild = true;
|
|
114
|
-
return { pages:
|
|
123
|
+
return { pages: pageManifest.length, assets, strategy: 'full' };
|
|
115
124
|
}
|
|
116
125
|
async function runBundleOnlyBuild(activeLogger, showBundlerInfo) {
|
|
117
126
|
const startupProfile = createStartupProfiler('cli-build');
|
|
@@ -120,7 +129,7 @@ export function createDevBuildSession(options) {
|
|
|
120
129
|
const { assets } = await runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo, { rebuildStrategy: 'bundle-only' });
|
|
121
130
|
await syncImageState(startupProfile);
|
|
122
131
|
startupProfile.emit('build_complete', {
|
|
123
|
-
pages:
|
|
132
|
+
pages: pageManifestEntries().length,
|
|
124
133
|
assets: assets.length,
|
|
125
134
|
compilerTotals,
|
|
126
135
|
expressionRewriteMetrics,
|
|
@@ -155,7 +164,7 @@ export function createDevBuildSession(options) {
|
|
|
155
164
|
state.envelopeByFile.set(envelope.file, envelope);
|
|
156
165
|
state.pageOnlyFastPathSignatureByFile.set(envelope.file, buildPageOnlyFastPathSignature(envelope));
|
|
157
166
|
}
|
|
158
|
-
const orderedEnvelopes = orderEnvelopes(
|
|
167
|
+
const orderedEnvelopes = orderEnvelopes(pageManifestEntries(), resolvedPagesDir, state.envelopeByFile);
|
|
159
168
|
if (!orderedEnvelopes || orderedEnvelopes.length === 0) {
|
|
160
169
|
throw new Error('Dev rebuild cache is incomplete; full rebuild required.');
|
|
161
170
|
}
|
|
@@ -169,14 +178,14 @@ export function createDevBuildSession(options) {
|
|
|
169
178
|
});
|
|
170
179
|
await syncImageState(startupProfile);
|
|
171
180
|
startupProfile.emit('build_complete', {
|
|
172
|
-
pages:
|
|
181
|
+
pages: pageManifestEntries().length,
|
|
173
182
|
assets: assets.length,
|
|
174
183
|
compilerTotals,
|
|
175
184
|
expressionRewriteMetrics,
|
|
176
185
|
strategy: 'page-only',
|
|
177
186
|
rebuiltPages: entries.length
|
|
178
187
|
});
|
|
179
|
-
return { pages:
|
|
188
|
+
return { pages: pageManifestEntries().length, assets, strategy: 'page-only', rebuiltPages: entries.length };
|
|
180
189
|
}
|
|
181
190
|
return {
|
|
182
191
|
async build(buildOptions = {}) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function buildDevErrorPayload({ pathname, state }: {
|
|
2
|
+
pathname: any;
|
|
3
|
+
state: any;
|
|
4
|
+
}): {
|
|
5
|
+
kind: string;
|
|
6
|
+
requestedPath: any;
|
|
7
|
+
buildId: any;
|
|
8
|
+
pendingBuildId: any;
|
|
9
|
+
buildStatus: any;
|
|
10
|
+
error: {
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
hint: string;
|
|
14
|
+
};
|
|
15
|
+
export function respondWithDevBuildError({ req, res, pathname, state, looksLikeJsonRequest }: {
|
|
16
|
+
req: any;
|
|
17
|
+
res: any;
|
|
18
|
+
pathname: any;
|
|
19
|
+
state: any;
|
|
20
|
+
looksLikeJsonRequest: any;
|
|
21
|
+
}): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function truncateMessage(message, limit = 1000) {
|
|
2
|
+
const value = String(message || 'Dev build failed.');
|
|
3
|
+
return value.length > limit ? `${value.slice(0, limit - 3)}...` : value;
|
|
4
|
+
}
|
|
5
|
+
function escapeHtml(value) {
|
|
6
|
+
return String(value)
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"');
|
|
11
|
+
}
|
|
12
|
+
export function buildDevErrorPayload({ pathname, state }) {
|
|
13
|
+
const message = truncateMessage(state.buildError?.message || 'Dev build failed.');
|
|
14
|
+
return {
|
|
15
|
+
kind: 'zenith_dev_build_failed',
|
|
16
|
+
requestedPath: pathname,
|
|
17
|
+
buildId: state.buildId,
|
|
18
|
+
pendingBuildId: state.pendingBuildId,
|
|
19
|
+
buildStatus: state.buildStatus,
|
|
20
|
+
error: { message },
|
|
21
|
+
hint: 'Fix the build error and save again.'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function respondWithDevBuildError({ req, res, pathname, state, looksLikeJsonRequest }) {
|
|
25
|
+
const payload = buildDevErrorPayload({ pathname, state });
|
|
26
|
+
if (looksLikeJsonRequest(req, pathname)) {
|
|
27
|
+
res.writeHead(503, {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Cache-Control': 'no-store',
|
|
30
|
+
'X-Zenith-Dev-Error': 'build-failed'
|
|
31
|
+
});
|
|
32
|
+
res.end(JSON.stringify(payload));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
res.writeHead(503, {
|
|
36
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
37
|
+
'Cache-Control': 'no-store',
|
|
38
|
+
'X-Zenith-Dev-Error': 'build-failed'
|
|
39
|
+
});
|
|
40
|
+
res.end([
|
|
41
|
+
'<!DOCTYPE html>',
|
|
42
|
+
'<html><head><meta charset="utf-8"><title>Zenith Dev Build Failed</title></head>',
|
|
43
|
+
'<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
|
|
44
|
+
'<h1 style="margin-top:0;">Zenith Dev Build Failed</h1>',
|
|
45
|
+
`<pre style="white-space: pre-wrap; line-height: 1.5;">Requested: ${escapeHtml(pathname)}\nStatus: build failed\nError: ${escapeHtml(payload.error.message)}\nHint: ${escapeHtml(payload.hint)}</pre>`,
|
|
46
|
+
'</body></html>'
|
|
47
|
+
].join(''));
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isPortConflict(error: any): any;
|
|
2
|
+
export function listenWithPortFallback({ server, port, host, maxAttempts }: {
|
|
3
|
+
server: any;
|
|
4
|
+
port: any;
|
|
5
|
+
host: any;
|
|
6
|
+
maxAttempts?: number | undefined;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
port: any;
|
|
9
|
+
requestedPort: any;
|
|
10
|
+
portFallback: {
|
|
11
|
+
requestedPort: any;
|
|
12
|
+
occupiedPorts: any[];
|
|
13
|
+
finalPort: any;
|
|
14
|
+
} | null;
|
|
15
|
+
}>;
|