@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function classifyPageRoute({ file, serverScript, hasScopedServerData = false }) {
|
|
2
|
+
const hasGuard = serverScript?.has_guard === true;
|
|
3
|
+
const hasLoad = serverScript?.has_load === true;
|
|
4
|
+
const hasAction = serverScript?.has_action === true;
|
|
5
|
+
const prerender = serverScript?.prerender === true;
|
|
6
|
+
if (prerender && (hasGuard || hasLoad || hasAction)) {
|
|
7
|
+
throw new Error(`[zenith] Build failed for ${file}: protected routes require SSR/runtime. ` +
|
|
8
|
+
'Cannot prerender a static route with a `guard`, `load`, or `action` function.');
|
|
9
|
+
}
|
|
10
|
+
if (prerender && hasScopedServerData) {
|
|
11
|
+
throw new Error(`[zenith] Build failed for ${file}: CSV012 scoped server data cannot be combined with prerender=true in v1.`);
|
|
12
|
+
}
|
|
13
|
+
const needsServerRender = !prerender && (Boolean(serverScript) || hasScopedServerData);
|
|
14
|
+
return {
|
|
15
|
+
prerender,
|
|
16
|
+
renderMode: needsServerRender ? 'server' : 'prerender',
|
|
17
|
+
hasGuard,
|
|
18
|
+
hasLoad,
|
|
19
|
+
hasAction
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} source
|
|
3
|
+
* @param {'guard' | 'load' | 'action'} name
|
|
4
|
+
* @returns {{
|
|
5
|
+
* fnMatch: RegExpMatchArray | null,
|
|
6
|
+
* constParenMatch: RegExpMatchArray | null,
|
|
7
|
+
* constSingleArgMatch: RegExpMatchArray | null,
|
|
8
|
+
* constMiddlewareMatch: RegExpMatchArray | null,
|
|
9
|
+
* hasExport: boolean,
|
|
10
|
+
* matchCount: number,
|
|
11
|
+
* arity: number | null
|
|
12
|
+
* }}
|
|
13
|
+
*/
|
|
14
|
+
export function readRouteHandlerExport(source: string, name: "guard" | "load" | "action"): {
|
|
15
|
+
fnMatch: RegExpMatchArray | null;
|
|
16
|
+
constParenMatch: RegExpMatchArray | null;
|
|
17
|
+
constSingleArgMatch: RegExpMatchArray | null;
|
|
18
|
+
constMiddlewareMatch: RegExpMatchArray | null;
|
|
19
|
+
hasExport: boolean;
|
|
20
|
+
matchCount: number;
|
|
21
|
+
arity: number | null;
|
|
22
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function routeHandlerExportPattern(name, tail) {
|
|
2
|
+
return new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*${tail}`);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} source
|
|
6
|
+
* @param {'guard' | 'load' | 'action'} name
|
|
7
|
+
* @returns {{
|
|
8
|
+
* fnMatch: RegExpMatchArray | null,
|
|
9
|
+
* constParenMatch: RegExpMatchArray | null,
|
|
10
|
+
* constSingleArgMatch: RegExpMatchArray | null,
|
|
11
|
+
* constMiddlewareMatch: RegExpMatchArray | null,
|
|
12
|
+
* hasExport: boolean,
|
|
13
|
+
* matchCount: number,
|
|
14
|
+
* arity: number | null
|
|
15
|
+
* }}
|
|
16
|
+
*/
|
|
17
|
+
export function readRouteHandlerExport(source, name) {
|
|
18
|
+
const fnMatch = source.match(new RegExp(`\\bexport\\s+(?:async\\s+)?function\\s+${name}\\s*\\(([^)]*)\\)`));
|
|
19
|
+
const constParenMatch = source.match(routeHandlerExportPattern(name, '(?:async\\s*)?\\(([^)]*)\\)\\s*=>'));
|
|
20
|
+
const constSingleArgMatch = source.match(routeHandlerExportPattern(name, '(?:async\\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=>'));
|
|
21
|
+
const constMiddlewareMatch = source.match(routeHandlerExportPattern(name, 'withMiddleware\\s*\\('));
|
|
22
|
+
const matchCount = Number(Boolean(fnMatch)) +
|
|
23
|
+
Number(Boolean(constParenMatch)) +
|
|
24
|
+
Number(Boolean(constSingleArgMatch)) +
|
|
25
|
+
Number(Boolean(constMiddlewareMatch));
|
|
26
|
+
let arity = null;
|
|
27
|
+
if (!constMiddlewareMatch) {
|
|
28
|
+
const singleArg = String(constSingleArgMatch?.[1] || '').trim();
|
|
29
|
+
const paramsText = String((fnMatch || constParenMatch)?.[1] || '').trim();
|
|
30
|
+
arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
fnMatch,
|
|
34
|
+
constParenMatch,
|
|
35
|
+
constSingleArgMatch,
|
|
36
|
+
constMiddlewareMatch,
|
|
37
|
+
hasExport: matchCount > 0,
|
|
38
|
+
matchCount,
|
|
39
|
+
arity
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CompilerOptsLike, OwnerFileAnalysisResult } from './types.js';
|
|
2
|
+
export declare const RESERVED_LEVEL1_BINDING_NAMES: readonly ["data", "props", "params", "ssr", "ssr_data", "ctx"];
|
|
3
|
+
export declare function analyzeOwnerServerFile(ownerSource: string, ownerPath: string, compilerOpts?: CompilerOptsLike): OwnerFileAnalysisResult;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readRouteHandlerExport } from '../route-handler-export-analysis.js';
|
|
2
|
+
import { extractTemplate } from '../resolve-components.js';
|
|
3
|
+
import { createScopedServerDiagnostic, SCOPED_SERVER_DIAGNOSTIC } from './diagnostics.js';
|
|
4
|
+
import { partitionScriptBlocks, serverBlockRequiresLangTs } from './parse-owner-server-block.js';
|
|
5
|
+
import { computeSerializationSet } from './serialization-set.js';
|
|
6
|
+
export const RESERVED_LEVEL1_BINDING_NAMES = [
|
|
7
|
+
'data',
|
|
8
|
+
'props',
|
|
9
|
+
'params',
|
|
10
|
+
'ssr',
|
|
11
|
+
'ssr_data',
|
|
12
|
+
'ctx'
|
|
13
|
+
];
|
|
14
|
+
const TOP_LEVEL_CONST_RE = /(?:^|\n)\s*const\s+([A-Za-z_$][\w$]*)\s*=/g;
|
|
15
|
+
const TOP_LEVEL_LET_RE = /(?:^|\n)\s*let\s+([A-Za-z_$][\w$]*)\s*=/g;
|
|
16
|
+
const EXPLICIT_DATA_EXPORT_RE = /\bexport\s+const\s+data\s*=\s*/;
|
|
17
|
+
export function analyzeOwnerServerFile(ownerSource, ownerPath, compilerOpts = {}) {
|
|
18
|
+
const diagnostics = [];
|
|
19
|
+
const { serverBlocks, clientBlocks } = partitionScriptBlocks(ownerSource);
|
|
20
|
+
if (serverBlocks.length === 0) {
|
|
21
|
+
return { owner: null, diagnostics };
|
|
22
|
+
}
|
|
23
|
+
if (serverBlocks.length > 1) {
|
|
24
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.MULTIPLE_SERVER_BLOCKS, 'error', 'Multiple <script server> blocks are not supported in layout/component owners.', ownerPath));
|
|
25
|
+
return { owner: null, diagnostics };
|
|
26
|
+
}
|
|
27
|
+
const serverBlock = serverBlocks[0];
|
|
28
|
+
const serverBody = String(serverBlock.body || '').trim();
|
|
29
|
+
if (serverBlockRequiresLangTs(serverBlock.attrs, compilerOpts)) {
|
|
30
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.MISSING_LANG_TS, 'error', 'Layout/component server blocks require lang="ts" (or typescriptDefault).', ownerPath));
|
|
31
|
+
}
|
|
32
|
+
diagnostics.push(...collectRouteControlMisuseDiagnostics(serverBody, ownerPath));
|
|
33
|
+
diagnostics.push(...collectLetDiagnostics(serverBody, ownerPath));
|
|
34
|
+
const hasExplicitData = EXPLICIT_DATA_EXPORT_RE.test(serverBody);
|
|
35
|
+
const level1Names = collectTopLevelConstNames(serverBody);
|
|
36
|
+
if (hasExplicitData && level1Names.length > 0) {
|
|
37
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.MIXED_LEVEL1_AND_DATA, 'error', 'Use either top-level server const values or export const data, not both.', ownerPath));
|
|
38
|
+
}
|
|
39
|
+
diagnostics.push(...collectReservedBindingDiagnostics(level1Names, ownerPath));
|
|
40
|
+
diagnostics.push(...collectClientLeakDiagnostics(level1Names, clientBlocks, ownerPath));
|
|
41
|
+
if (diagnostics.some((item) => item.severity === 'error')) {
|
|
42
|
+
return { owner: null, diagnostics };
|
|
43
|
+
}
|
|
44
|
+
const template = extractTemplate(ownerSource);
|
|
45
|
+
const ownerKind = inferOwnerKind(ownerPath);
|
|
46
|
+
if (hasExplicitData) {
|
|
47
|
+
const owner = {
|
|
48
|
+
ownerKind,
|
|
49
|
+
ownerPath,
|
|
50
|
+
syntax: 'explicit-data',
|
|
51
|
+
serializedVariableNames: collectExplicitDataTemplateRefs(template),
|
|
52
|
+
exportName: 'data'
|
|
53
|
+
};
|
|
54
|
+
return { owner, diagnostics };
|
|
55
|
+
}
|
|
56
|
+
if (level1Names.length === 0) {
|
|
57
|
+
return { owner: null, diagnostics };
|
|
58
|
+
}
|
|
59
|
+
const serializedVariableNames = computeSerializationSet(level1Names, template);
|
|
60
|
+
for (const name of level1Names) {
|
|
61
|
+
if (!serializedVariableNames.includes(name)) {
|
|
62
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.UNREFERENCED_SERVER_VAR, 'warning', `Server variable "${name}" is not referenced by this owner's template and will not serialize.`, ownerPath));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const owner = {
|
|
66
|
+
ownerKind,
|
|
67
|
+
ownerPath,
|
|
68
|
+
syntax: 'variables',
|
|
69
|
+
serializedVariableNames,
|
|
70
|
+
level1VariableNames: level1Names,
|
|
71
|
+
exportName: 'data'
|
|
72
|
+
};
|
|
73
|
+
return { owner, diagnostics };
|
|
74
|
+
}
|
|
75
|
+
function collectRouteControlMisuseDiagnostics(serverBody, ownerPath) {
|
|
76
|
+
const diagnostics = [];
|
|
77
|
+
const load = readRouteHandlerExport(serverBody, 'load');
|
|
78
|
+
if (load.hasExport) {
|
|
79
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.OWNER_LOAD_MISUSE, 'error', '`load()` is route-only in Zenith. Use server variables or scoped data() inside layouts/components.', ownerPath));
|
|
80
|
+
}
|
|
81
|
+
const guard = readRouteHandlerExport(serverBody, 'guard');
|
|
82
|
+
if (guard.hasExport) {
|
|
83
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.OWNER_GUARD_MISUSE, 'error', '`guard()` is route-only in Zenith and cannot be declared in layout/component owners.', ownerPath));
|
|
84
|
+
}
|
|
85
|
+
const action = readRouteHandlerExport(serverBody, 'action');
|
|
86
|
+
if (action.hasExport) {
|
|
87
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.OWNER_ACTION_MISUSE, 'error', '`action()` is route-only in Zenith and cannot be declared in layout/component owners.', ownerPath));
|
|
88
|
+
}
|
|
89
|
+
return diagnostics;
|
|
90
|
+
}
|
|
91
|
+
function collectLetDiagnostics(serverBody, ownerPath) {
|
|
92
|
+
const diagnostics = [];
|
|
93
|
+
for (const match of serverBody.matchAll(TOP_LEVEL_LET_RE)) {
|
|
94
|
+
const name = String(match[1] || '');
|
|
95
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.LEVEL1_LET_REJECTED, 'error', `Level 1 server variable "${name}" must use const, not let.`, ownerPath));
|
|
96
|
+
}
|
|
97
|
+
return diagnostics;
|
|
98
|
+
}
|
|
99
|
+
function collectTopLevelConstNames(serverBody) {
|
|
100
|
+
const names = [];
|
|
101
|
+
for (const match of serverBody.matchAll(TOP_LEVEL_CONST_RE)) {
|
|
102
|
+
const name = String(match[1] || '');
|
|
103
|
+
if (name && !names.includes(name)) {
|
|
104
|
+
names.push(name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return names;
|
|
108
|
+
}
|
|
109
|
+
function collectReservedBindingDiagnostics(level1Names, ownerPath) {
|
|
110
|
+
const diagnostics = [];
|
|
111
|
+
const reserved = new Set(RESERVED_LEVEL1_BINDING_NAMES);
|
|
112
|
+
for (const name of level1Names) {
|
|
113
|
+
if (reserved.has(name)) {
|
|
114
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.RESERVED_BINDING, 'error', `Server variable "${name}" uses a reserved binding name.`, ownerPath));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return diagnostics;
|
|
118
|
+
}
|
|
119
|
+
function collectClientLeakDiagnostics(level1Names, clientBlocks, ownerPath) {
|
|
120
|
+
const diagnostics = [];
|
|
121
|
+
if (level1Names.length === 0 || clientBlocks.length === 0) {
|
|
122
|
+
return diagnostics;
|
|
123
|
+
}
|
|
124
|
+
const clientSource = clientBlocks.map((block) => block.body).join('\n');
|
|
125
|
+
for (const name of level1Names) {
|
|
126
|
+
const refRe = new RegExp(`\\b${escapeRegExp(name)}\\b`);
|
|
127
|
+
if (refRe.test(clientSource)) {
|
|
128
|
+
diagnostics.push(createScopedServerDiagnostic(SCOPED_SERVER_DIAGNOSTIC.CLIENT_SCRIPT_LEAK, 'error', `Server variable "${name}" is referenced from a client script block.`, ownerPath));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return diagnostics;
|
|
132
|
+
}
|
|
133
|
+
function collectExplicitDataTemplateRefs(template) {
|
|
134
|
+
const refs = [];
|
|
135
|
+
for (const match of String(template || '').matchAll(/\{data\.([A-Za-z_$][\w$]*)/g)) {
|
|
136
|
+
const name = String(match[1] || '');
|
|
137
|
+
if (name && !refs.includes(name)) {
|
|
138
|
+
refs.push(name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return refs.sort();
|
|
142
|
+
}
|
|
143
|
+
function inferOwnerKind(ownerPath) {
|
|
144
|
+
const normalized = String(ownerPath || '').replace(/\\/g, '/');
|
|
145
|
+
return normalized.includes('/layouts/') ? 'layout' : 'component';
|
|
146
|
+
}
|
|
147
|
+
function escapeRegExp(value) {
|
|
148
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
149
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ScopedServerDiagnostic, ScopedServerDiagnosticSeverity } from './types.js';
|
|
2
|
+
export declare const SCOPED_SERVER_DIAGNOSTIC: {
|
|
3
|
+
readonly OWNER_LOAD_MISUSE: "CSV001";
|
|
4
|
+
readonly OWNER_GUARD_MISUSE: "CSV002";
|
|
5
|
+
readonly OWNER_ACTION_MISUSE: "CSV003";
|
|
6
|
+
readonly RESERVED_BINDING: "CSV004";
|
|
7
|
+
readonly LEVEL1_LET_REJECTED: "CSV005";
|
|
8
|
+
readonly MULTIPLE_SERVER_BLOCKS: "CSV006";
|
|
9
|
+
readonly CLIENT_SCRIPT_LEAK: "CSV007";
|
|
10
|
+
readonly COMPETING_DOCUMENT_ROOTS: "CSV008";
|
|
11
|
+
readonly MIXED_LEVEL1_AND_DATA: "CSV009";
|
|
12
|
+
readonly MISSING_LANG_TS: "CSV010";
|
|
13
|
+
readonly UNREFERENCED_SERVER_VAR: "CSV011";
|
|
14
|
+
readonly PRERENDER_WITH_SCOPED_DATA: "CSV012";
|
|
15
|
+
readonly UNSUPPORTED_COMPONENT_PROP: "CSV013";
|
|
16
|
+
};
|
|
17
|
+
export declare function createScopedServerDiagnostic(code: string, severity: ScopedServerDiagnosticSeverity, message: string, filePath: string): ScopedServerDiagnostic;
|
|
18
|
+
export declare function sortScopedServerDiagnostics(diagnostics: ScopedServerDiagnostic[]): ScopedServerDiagnostic[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const SCOPED_SERVER_DIAGNOSTIC = {
|
|
2
|
+
OWNER_LOAD_MISUSE: 'CSV001',
|
|
3
|
+
OWNER_GUARD_MISUSE: 'CSV002',
|
|
4
|
+
OWNER_ACTION_MISUSE: 'CSV003',
|
|
5
|
+
RESERVED_BINDING: 'CSV004',
|
|
6
|
+
LEVEL1_LET_REJECTED: 'CSV005',
|
|
7
|
+
MULTIPLE_SERVER_BLOCKS: 'CSV006',
|
|
8
|
+
CLIENT_SCRIPT_LEAK: 'CSV007',
|
|
9
|
+
COMPETING_DOCUMENT_ROOTS: 'CSV008',
|
|
10
|
+
MIXED_LEVEL1_AND_DATA: 'CSV009',
|
|
11
|
+
MISSING_LANG_TS: 'CSV010',
|
|
12
|
+
UNREFERENCED_SERVER_VAR: 'CSV011',
|
|
13
|
+
PRERENDER_WITH_SCOPED_DATA: 'CSV012',
|
|
14
|
+
UNSUPPORTED_COMPONENT_PROP: 'CSV013'
|
|
15
|
+
};
|
|
16
|
+
export function createScopedServerDiagnostic(code, severity, message, filePath) {
|
|
17
|
+
return {
|
|
18
|
+
code,
|
|
19
|
+
severity,
|
|
20
|
+
message,
|
|
21
|
+
filePath
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function sortScopedServerDiagnostics(diagnostics) {
|
|
25
|
+
return [...diagnostics].sort((left, right) => {
|
|
26
|
+
const fileCmp = left.filePath.localeCompare(right.filePath);
|
|
27
|
+
if (fileCmp !== 0) {
|
|
28
|
+
return fileCmp;
|
|
29
|
+
}
|
|
30
|
+
return left.code.localeCompare(right.code);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CompilerOptsLike, ManifestScopedServerDataEntry } from './types.js';
|
|
2
|
+
export interface LowerRouteScopedServerDataOptions {
|
|
3
|
+
pageSource: string;
|
|
4
|
+
pageFile: string;
|
|
5
|
+
registry: Map<string, string>;
|
|
6
|
+
srcDir: string;
|
|
7
|
+
projectRoot?: string;
|
|
8
|
+
compilerOpts?: CompilerOptsLike;
|
|
9
|
+
scopedServerData?: ManifestScopedServerDataEntry[];
|
|
10
|
+
}
|
|
11
|
+
export interface LoweredScopedServerDataEntry extends ManifestScopedServerDataEntry {
|
|
12
|
+
module: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LoweredScopedServerDataModule {
|
|
15
|
+
ownerKey: string;
|
|
16
|
+
ownerPath: string;
|
|
17
|
+
module: string;
|
|
18
|
+
source: string;
|
|
19
|
+
sourcePath: string;
|
|
20
|
+
}
|
|
21
|
+
export interface LoweredScopedServerDataRoute {
|
|
22
|
+
scopedServerData: LoweredScopedServerDataEntry[];
|
|
23
|
+
modules: LoweredScopedServerDataModule[];
|
|
24
|
+
}
|
|
25
|
+
export declare function lowerRouteScopedServerData(options: LowerRouteScopedServerDataOptions): LoweredScopedServerDataRoute;
|
|
26
|
+
export declare function scopedServerModulePathForOwnerKey(ownerKey: string): string;
|
|
27
|
+
export declare function resolveScopedServerModuleOutputPath(serverDir: string, modulePath: string): string;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { collectExpandedComponentOccurrences } from '../component-occurrences.js';
|
|
5
|
+
import { scanRouteScopedServerOwners, toOwnerKey } from './owner-scanner.js';
|
|
6
|
+
import { partitionScriptBlocks } from './parse-owner-server-block.js';
|
|
7
|
+
const PACKAGE_REQUIRE = createRequire(import.meta.url);
|
|
8
|
+
const INVALID_OWNER_KEY_ERROR = '[Zenith:ScopedServerData] Invalid scoped server data owner key.';
|
|
9
|
+
const INVALID_MODULE_PATH_ERROR = '[Zenith:ScopedServerData] Invalid scoped server data module path.';
|
|
10
|
+
export function lowerRouteScopedServerData(options) {
|
|
11
|
+
const pageSource = String(options.pageSource || '');
|
|
12
|
+
const pageFile = resolve(String(options.pageFile || ''));
|
|
13
|
+
const srcDir = resolve(String(options.srcDir || ''));
|
|
14
|
+
const registry = options.registry;
|
|
15
|
+
const compilerOpts = options.compilerOpts || {};
|
|
16
|
+
const metadata = Array.isArray(options.scopedServerData) ? options.scopedServerData : [];
|
|
17
|
+
const scanResult = scanRouteScopedServerOwners({
|
|
18
|
+
pageSource,
|
|
19
|
+
pageFile,
|
|
20
|
+
registry,
|
|
21
|
+
srcDir,
|
|
22
|
+
compilerOpts
|
|
23
|
+
});
|
|
24
|
+
assertNoLoweringDiagnostics(scanResult.diagnostics, pageFile);
|
|
25
|
+
const ownerByKey = new Map();
|
|
26
|
+
for (const owner of scanResult.owners) {
|
|
27
|
+
ownerByKey.set(owner.ownerKey, owner);
|
|
28
|
+
}
|
|
29
|
+
const metadataByKey = new Map();
|
|
30
|
+
for (const entry of metadata) {
|
|
31
|
+
if (entry && typeof entry.ownerKey === 'string') {
|
|
32
|
+
metadataByKey.set(entry.ownerKey, entry);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const orderedKeys = buildEncounterOrderedOwnerKeys({
|
|
36
|
+
pageSource,
|
|
37
|
+
pageFile,
|
|
38
|
+
registry,
|
|
39
|
+
srcDir,
|
|
40
|
+
ownerByKey,
|
|
41
|
+
metadata
|
|
42
|
+
});
|
|
43
|
+
const ts = resolveTypeScriptApi(options.projectRoot);
|
|
44
|
+
const scopedServerData = [];
|
|
45
|
+
const modules = [];
|
|
46
|
+
for (const ownerKey of orderedKeys) {
|
|
47
|
+
const owner = ownerByKey.get(ownerKey);
|
|
48
|
+
if (!owner) {
|
|
49
|
+
throw new Error(`[Zenith:ScopedServerData] Cannot lower missing scoped server data owner "${ownerKey}" for "${pageFile}".`);
|
|
50
|
+
}
|
|
51
|
+
const modulePath = scopedServerModulePathForOwnerKey(ownerKey);
|
|
52
|
+
const source = lowerOwnerSource(owner, ts);
|
|
53
|
+
const entry = {
|
|
54
|
+
...(metadataByKey.get(ownerKey) || toFallbackManifestEntry(owner)),
|
|
55
|
+
module: modulePath
|
|
56
|
+
};
|
|
57
|
+
if (owner.syntax === 'variables' &&
|
|
58
|
+
owner.serializedVariableNames.length > 0 &&
|
|
59
|
+
!Array.isArray(entry.serializedVariableNames)) {
|
|
60
|
+
entry.serializedVariableNames = [...owner.serializedVariableNames];
|
|
61
|
+
}
|
|
62
|
+
scopedServerData.push(entry);
|
|
63
|
+
modules.push({
|
|
64
|
+
ownerKey,
|
|
65
|
+
ownerPath: owner.ownerPath,
|
|
66
|
+
module: modulePath,
|
|
67
|
+
source,
|
|
68
|
+
sourcePath: owner.ownerPath
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { scopedServerData, modules };
|
|
72
|
+
}
|
|
73
|
+
export function scopedServerModulePathForOwnerKey(ownerKey) {
|
|
74
|
+
const raw = String(ownerKey || '');
|
|
75
|
+
if (!raw || isAbsolute(raw) || /^[A-Za-z]:[\\/]/.test(raw)) {
|
|
76
|
+
throw new Error(INVALID_OWNER_KEY_ERROR);
|
|
77
|
+
}
|
|
78
|
+
const normalized = raw.replace(/\\/g, '/');
|
|
79
|
+
const parts = normalized.split('/');
|
|
80
|
+
if (parts.some((part) => part.length === 0 || part === '.' || part === '..')) {
|
|
81
|
+
throw new Error(INVALID_OWNER_KEY_ERROR);
|
|
82
|
+
}
|
|
83
|
+
return `scoped/${parts.join('/')}.mjs`;
|
|
84
|
+
}
|
|
85
|
+
export function resolveScopedServerModuleOutputPath(serverDir, modulePath) {
|
|
86
|
+
const raw = String(modulePath || '');
|
|
87
|
+
if (!raw || isAbsolute(raw) || /^[A-Za-z]:[\\/]/.test(raw)) {
|
|
88
|
+
throw new Error(INVALID_MODULE_PATH_ERROR);
|
|
89
|
+
}
|
|
90
|
+
const normalized = raw.replace(/\\/g, '/');
|
|
91
|
+
if (!normalized.startsWith('scoped/') || normalized.split('/').some((part) => part === '..' || part === '.')) {
|
|
92
|
+
throw new Error(INVALID_MODULE_PATH_ERROR);
|
|
93
|
+
}
|
|
94
|
+
const scopedRoot = resolve(serverDir, 'scoped');
|
|
95
|
+
const outputPath = resolve(serverDir, normalized);
|
|
96
|
+
if (outputPath !== scopedRoot && !outputPath.startsWith(`${scopedRoot}${sep}`)) {
|
|
97
|
+
throw new Error(INVALID_MODULE_PATH_ERROR);
|
|
98
|
+
}
|
|
99
|
+
return outputPath;
|
|
100
|
+
}
|
|
101
|
+
function buildEncounterOrderedOwnerKeys({ pageSource, pageFile, registry, srcDir, ownerByKey, metadata }) {
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const keys = [];
|
|
104
|
+
for (const occurrence of collectExpandedComponentOccurrences(pageSource, registry, pageFile)) {
|
|
105
|
+
if (typeof occurrence.componentPath !== 'string' || occurrence.componentPath.length === 0) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const ownerKey = toOwnerKey(occurrence.componentPath, srcDir);
|
|
109
|
+
if (!ownerByKey.has(ownerKey) || seen.has(ownerKey)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
seen.add(ownerKey);
|
|
113
|
+
keys.push(ownerKey);
|
|
114
|
+
}
|
|
115
|
+
for (const entry of metadata) {
|
|
116
|
+
const ownerKey = String(entry?.ownerKey || '');
|
|
117
|
+
if (ownerKey && ownerByKey.has(ownerKey) && !seen.has(ownerKey)) {
|
|
118
|
+
seen.add(ownerKey);
|
|
119
|
+
keys.push(ownerKey);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const ownerKey of ownerByKey.keys()) {
|
|
123
|
+
if (!seen.has(ownerKey)) {
|
|
124
|
+
keys.push(ownerKey);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return keys;
|
|
128
|
+
}
|
|
129
|
+
function lowerOwnerSource(owner, ts) {
|
|
130
|
+
const ownerSource = readFileSync(owner.ownerPath, 'utf8');
|
|
131
|
+
const serverBody = readSingleServerBody(ownerSource, owner.ownerPath);
|
|
132
|
+
assertNoRouteResultMisuse(serverBody, owner.ownerPath);
|
|
133
|
+
if (owner.syntax === 'explicit-data') {
|
|
134
|
+
return ensureTrailingNewline(serverBody);
|
|
135
|
+
}
|
|
136
|
+
const { imports, body } = partitionTopLevelImports(ts, serverBody, owner.ownerPath);
|
|
137
|
+
const lines = [
|
|
138
|
+
imports,
|
|
139
|
+
'export async function data(ctx, props) {',
|
|
140
|
+
indentBlock(body),
|
|
141
|
+
' return {',
|
|
142
|
+
...owner.serializedVariableNames.map((name) => ` ${name},`),
|
|
143
|
+
' };',
|
|
144
|
+
'}',
|
|
145
|
+
''
|
|
146
|
+
].filter((line) => line !== null && line !== undefined);
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
function readSingleServerBody(ownerSource, ownerPath) {
|
|
150
|
+
const { serverBlocks } = partitionScriptBlocks(ownerSource);
|
|
151
|
+
if (serverBlocks.length !== 1) {
|
|
152
|
+
throw new Error(`[Zenith:ScopedServerData] Cannot lower scoped server data owner "${ownerPath}" because it must contain exactly one server block.`);
|
|
153
|
+
}
|
|
154
|
+
return String(serverBlocks[0]?.body || '').trim();
|
|
155
|
+
}
|
|
156
|
+
function partitionTopLevelImports(ts, source, filePath) {
|
|
157
|
+
const parsed = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
158
|
+
const importRanges = [];
|
|
159
|
+
for (const statement of parsed.statements) {
|
|
160
|
+
if (ts.isImportDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) {
|
|
161
|
+
importRanges.push([statement.getFullStart(), statement.end]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (importRanges.length === 0) {
|
|
165
|
+
return { imports: '', body: source.trim() };
|
|
166
|
+
}
|
|
167
|
+
const imports = importRanges
|
|
168
|
+
.map(([start, end]) => source.slice(start, end).trim())
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join('\n');
|
|
171
|
+
let cursor = 0;
|
|
172
|
+
const bodyParts = [];
|
|
173
|
+
for (const [start, end] of importRanges) {
|
|
174
|
+
bodyParts.push(source.slice(cursor, start));
|
|
175
|
+
cursor = end;
|
|
176
|
+
}
|
|
177
|
+
bodyParts.push(source.slice(cursor));
|
|
178
|
+
return {
|
|
179
|
+
imports,
|
|
180
|
+
body: bodyParts.join('').trim()
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function assertNoRouteResultMisuse(serverBody, ownerPath) {
|
|
184
|
+
const ctxMisuse = serverBody.match(/\bctx\s*\.\s*(redirect|deny|data)\s*\(/);
|
|
185
|
+
if (ctxMisuse) {
|
|
186
|
+
throw new Error(`[Zenith:ScopedServerData] Scoped server data owner "${ownerPath}" cannot use route-only result API ctx.${ctxMisuse[1]}().`);
|
|
187
|
+
}
|
|
188
|
+
if (/\bexport\s+(?:async\s+)?function\s+action\b|\bexport\s+const\s+action\s*=/.test(serverBody)) {
|
|
189
|
+
throw new Error(`[Zenith:ScopedServerData] Scoped server data owner "${ownerPath}" cannot declare route-only action().`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function assertNoLoweringDiagnostics(diagnostics, pageFile) {
|
|
193
|
+
const errors = diagnostics.filter((item) => item.severity === 'error');
|
|
194
|
+
if (errors.length === 0) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const first = errors[0];
|
|
198
|
+
throw new Error(`[Zenith:ScopedServerData] Cannot lower scoped server data for ${pageFile}: ${first.code} ${first.message} (${first.filePath})`);
|
|
199
|
+
}
|
|
200
|
+
function resolveTypeScriptApi(projectRoot) {
|
|
201
|
+
if (projectRoot) {
|
|
202
|
+
try {
|
|
203
|
+
const projectRequire = createRequire(join(projectRoot, '__zenith_scoped_server_data_lowering__.js'));
|
|
204
|
+
return projectRequire('typescript');
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Fall through to the CLI workspace/package dependency.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return PACKAGE_REQUIRE('typescript');
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
throw new Error('[Zenith:ScopedServerData] Scoped server data lowering requires the `typescript` package.');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function toFallbackManifestEntry(owner) {
|
|
218
|
+
const entry = {
|
|
219
|
+
ownerKind: owner.ownerKind,
|
|
220
|
+
ownerKey: owner.ownerKey,
|
|
221
|
+
syntax: owner.syntax,
|
|
222
|
+
exportName: owner.exportName,
|
|
223
|
+
instanceStrategy: owner.ownerKind === 'layout' ? 'singleton' : 'singleton'
|
|
224
|
+
};
|
|
225
|
+
if (owner.syntax === 'variables' && owner.serializedVariableNames.length > 0) {
|
|
226
|
+
entry.serializedVariableNames = [...owner.serializedVariableNames];
|
|
227
|
+
}
|
|
228
|
+
return entry;
|
|
229
|
+
}
|
|
230
|
+
function indentBlock(source) {
|
|
231
|
+
const trimmed = String(source || '').trim();
|
|
232
|
+
if (!trimmed) {
|
|
233
|
+
return '';
|
|
234
|
+
}
|
|
235
|
+
return trimmed
|
|
236
|
+
.split('\n')
|
|
237
|
+
.map((line) => ` ${line}`)
|
|
238
|
+
.join('\n');
|
|
239
|
+
}
|
|
240
|
+
function ensureTrailingNewline(source) {
|
|
241
|
+
return source.endsWith('\n') ? source : `${source}\n`;
|
|
242
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalyzeRouteScopedServerMetadataOptions, AnalyzeRouteScopedServerMetadataResult, ScopedServerDiagnostic } from './types.js';
|
|
2
|
+
export type { AnalyzeRouteScopedServerMetadataOptions, AnalyzeRouteScopedServerMetadataResult, ManifestScopedServerDataEntry } from './types.js';
|
|
3
|
+
export declare function analyzeRouteScopedServerMetadata(options: AnalyzeRouteScopedServerMetadataOptions): AnalyzeRouteScopedServerMetadataResult;
|
|
4
|
+
export declare function assertNoScopedServerBuildErrors(diagnostics: ScopedServerDiagnostic[], contextFile: string): void;
|