@zenithbuild/cli 0.6.13 → 0.7.0
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/dist/build/compiler-runtime.d.ts +59 -0
- package/dist/build/compiler-runtime.js +277 -0
- package/dist/build/expression-rewrites.d.ts +88 -0
- package/dist/build/expression-rewrites.js +372 -0
- package/dist/build/hoisted-code-transforms.d.ts +44 -0
- package/dist/build/hoisted-code-transforms.js +316 -0
- package/dist/build/merge-component-ir.d.ts +16 -0
- package/dist/build/merge-component-ir.js +257 -0
- package/dist/build/page-component-loop.d.ts +92 -0
- package/dist/build/page-component-loop.js +257 -0
- package/dist/build/page-ir-normalization.d.ts +23 -0
- package/dist/build/page-ir-normalization.js +370 -0
- package/dist/build/page-loop-metrics.d.ts +100 -0
- package/dist/build/page-loop-metrics.js +131 -0
- package/dist/build/page-loop-state.d.ts +261 -0
- package/dist/build/page-loop-state.js +92 -0
- package/dist/build/page-loop.d.ts +33 -0
- package/dist/build/page-loop.js +217 -0
- package/dist/build/scoped-identifier-rewrite.d.ts +112 -0
- package/dist/build/scoped-identifier-rewrite.js +245 -0
- package/dist/build/server-script.d.ts +41 -0
- package/dist/build/server-script.js +210 -0
- package/dist/build/type-declarations.d.ts +16 -0
- package/dist/build/type-declarations.js +158 -0
- package/dist/build/typescript-expression-utils.d.ts +23 -0
- package/dist/build/typescript-expression-utils.js +272 -0
- package/dist/build.d.ts +10 -18
- package/dist/build.js +74 -2261
- package/dist/component-instance-ir.d.ts +2 -2
- package/dist/component-instance-ir.js +146 -39
- package/dist/component-occurrences.js +63 -15
- package/dist/config.d.ts +66 -0
- package/dist/config.js +86 -0
- package/dist/debug-script.d.ts +1 -0
- package/dist/debug-script.js +8 -0
- package/dist/dev-build-session.d.ts +23 -0
- package/dist/dev-build-session.js +421 -0
- package/dist/dev-server.js +405 -58
- package/dist/framework-components/Image.zen +316 -0
- package/dist/images/materialize.d.ts +17 -0
- package/dist/images/materialize.js +200 -0
- package/dist/images/payload.d.ts +18 -0
- package/dist/images/payload.js +65 -0
- package/dist/images/runtime.d.ts +4 -0
- package/dist/images/runtime.js +254 -0
- package/dist/images/service.d.ts +4 -0
- package/dist/images/service.js +302 -0
- package/dist/images/shared.d.ts +58 -0
- package/dist/images/shared.js +306 -0
- package/dist/index.js +2 -17
- package/dist/manifest.js +45 -0
- package/dist/preview.d.ts +4 -1
- package/dist/preview.js +59 -6
- package/dist/resolve-components.js +20 -3
- package/dist/server-contract.js +3 -2
- package/dist/server-script-composition.d.ts +39 -0
- package/dist/server-script-composition.js +133 -0
- package/dist/startup-profile.d.ts +10 -0
- package/dist/startup-profile.js +62 -0
- package/dist/toolchain-paths.d.ts +1 -0
- package/dist/toolchain-paths.js +31 -0
- package/dist/version-check.d.ts +2 -1
- package/dist/version-check.js +12 -5
- package/package.json +5 -4
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { findNextKnownComponentTag } from '../component-tag-parser.js';
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} source
|
|
5
|
+
* @param {string} sourceFile
|
|
6
|
+
* @param {object} [compilerOpts]
|
|
7
|
+
* @returns {{ source: string, serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null }}
|
|
8
|
+
*/
|
|
9
|
+
export function extractServerScript(source, sourceFile, compilerOpts = {}) {
|
|
10
|
+
const scriptRe = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
11
|
+
const serverMatches = [];
|
|
12
|
+
const reservedServerExportRe = /\bexport\s+const\s+(?:data|prerender|guard|load|ssr_data|props|ssr)\b|\bexport\s+(?:async\s+)?function\s+(?:load|guard)\s*\(|\bexport\s+const\s+(?:load|guard)\s*=/;
|
|
13
|
+
for (const match of source.matchAll(scriptRe)) {
|
|
14
|
+
const attrs = String(match[1] || '');
|
|
15
|
+
const body = String(match[2] || '');
|
|
16
|
+
const isServer = /\bserver\b/i.test(attrs);
|
|
17
|
+
if (!isServer && reservedServerExportRe.test(body)) {
|
|
18
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
19
|
+
` File: ${sourceFile}\n` +
|
|
20
|
+
` Reason: guard/load/data exports are only allowed in <script server lang="ts"> or adjacent .guard.ts / .load.ts files\n` +
|
|
21
|
+
` Example: move the export into <script server lang="ts">`);
|
|
22
|
+
}
|
|
23
|
+
if (isServer) {
|
|
24
|
+
serverMatches.push(match);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (serverMatches.length === 0) {
|
|
28
|
+
return { source, serverScript: null };
|
|
29
|
+
}
|
|
30
|
+
if (serverMatches.length > 1) {
|
|
31
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
32
|
+
` File: ${sourceFile}\n` +
|
|
33
|
+
` Reason: multiple <script server> blocks are not supported\n` +
|
|
34
|
+
` Example: keep exactly one <script server>...</script> block`);
|
|
35
|
+
}
|
|
36
|
+
const match = serverMatches[0];
|
|
37
|
+
const full = match[0] || '';
|
|
38
|
+
const attrs = String(match[1] || '');
|
|
39
|
+
const hasLangTs = /\blang\s*=\s*["']ts["']/i.test(attrs);
|
|
40
|
+
const hasLangJs = /\blang\s*=\s*["'](?:js|javascript)["']/i.test(attrs);
|
|
41
|
+
const hasAnyLang = /\blang\s*=/i.test(attrs);
|
|
42
|
+
const isTypescriptDefault = compilerOpts && compilerOpts.typescriptDefault === true;
|
|
43
|
+
if (!hasLangTs) {
|
|
44
|
+
if (!isTypescriptDefault || hasLangJs || hasAnyLang) {
|
|
45
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
46
|
+
` File: ${sourceFile}\n` +
|
|
47
|
+
` Reason: Zenith requires TypeScript server scripts. Add lang="ts" (or enable typescriptDefault).\n` +
|
|
48
|
+
` Example: <script server lang="ts">`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const serverSource = String(match[2] || '').trim();
|
|
52
|
+
if (!serverSource) {
|
|
53
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
54
|
+
` File: ${sourceFile}\n` +
|
|
55
|
+
` Reason: <script server> block is empty\n` +
|
|
56
|
+
` Example: export const data = { ... }`);
|
|
57
|
+
}
|
|
58
|
+
const loadFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+load\s*\(([^)]*)\)/);
|
|
59
|
+
const loadConstParenMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
|
|
60
|
+
const loadConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
|
|
61
|
+
const hasLoad = Boolean(loadFnMatch || loadConstParenMatch || loadConstSingleArgMatch);
|
|
62
|
+
const loadMatchCount = Number(Boolean(loadFnMatch)) +
|
|
63
|
+
Number(Boolean(loadConstParenMatch)) +
|
|
64
|
+
Number(Boolean(loadConstSingleArgMatch));
|
|
65
|
+
if (loadMatchCount > 1) {
|
|
66
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
67
|
+
` File: ${sourceFile}\n` +
|
|
68
|
+
` Reason: multiple load exports detected\n` +
|
|
69
|
+
` Example: keep exactly one export const load = async (ctx) => ({ ... })`);
|
|
70
|
+
}
|
|
71
|
+
const guardFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+guard\s*\(([^)]*)\)/);
|
|
72
|
+
const guardConstParenMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
|
|
73
|
+
const guardConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
|
|
74
|
+
const hasGuard = Boolean(guardFnMatch || guardConstParenMatch || guardConstSingleArgMatch);
|
|
75
|
+
const guardMatchCount = Number(Boolean(guardFnMatch)) +
|
|
76
|
+
Number(Boolean(guardConstParenMatch)) +
|
|
77
|
+
Number(Boolean(guardConstSingleArgMatch));
|
|
78
|
+
if (guardMatchCount > 1) {
|
|
79
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
80
|
+
` File: ${sourceFile}\n` +
|
|
81
|
+
` Reason: multiple guard exports detected\n` +
|
|
82
|
+
` Example: keep exactly one export const guard = async (ctx) => ({ ... })`);
|
|
83
|
+
}
|
|
84
|
+
const hasData = /\bexport\s+const\s+data\b/.test(serverSource);
|
|
85
|
+
const hasSsrData = /\bexport\s+const\s+ssr_data\b/.test(serverSource);
|
|
86
|
+
const hasSsr = /\bexport\s+const\s+ssr\b/.test(serverSource);
|
|
87
|
+
const hasProps = /\bexport\s+const\s+props\b/.test(serverSource);
|
|
88
|
+
if (hasData && hasLoad) {
|
|
89
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
90
|
+
` File: ${sourceFile}\n` +
|
|
91
|
+
` Reason: export either data or load(ctx), not both\n` +
|
|
92
|
+
` Example: remove data and return payload from load(ctx)`);
|
|
93
|
+
}
|
|
94
|
+
if ((hasData || hasLoad) && (hasSsrData || hasSsr || hasProps)) {
|
|
95
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
96
|
+
` File: ${sourceFile}\n` +
|
|
97
|
+
` Reason: data/load cannot be combined with legacy ssr_data/ssr/props exports\n` +
|
|
98
|
+
` Example: use only export const data or export const load`);
|
|
99
|
+
}
|
|
100
|
+
if (hasLoad) {
|
|
101
|
+
const singleArg = String(loadConstSingleArgMatch?.[1] || '').trim();
|
|
102
|
+
const paramsText = String((loadFnMatch || loadConstParenMatch)?.[1] || '').trim();
|
|
103
|
+
const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
|
|
104
|
+
if (arity !== 1) {
|
|
105
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
106
|
+
` File: ${sourceFile}\n` +
|
|
107
|
+
` Reason: load(ctx) must accept exactly one argument\n` +
|
|
108
|
+
` Example: export const load = async (ctx) => ({ ... })`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (hasGuard) {
|
|
112
|
+
const singleArg = String(guardConstSingleArgMatch?.[1] || '').trim();
|
|
113
|
+
const paramsText = String((guardFnMatch || guardConstParenMatch)?.[1] || '').trim();
|
|
114
|
+
const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
|
|
115
|
+
if (arity !== 1) {
|
|
116
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
117
|
+
` File: ${sourceFile}\n` +
|
|
118
|
+
` Reason: guard(ctx) must accept exactly one argument\n` +
|
|
119
|
+
` Example: export const guard = async (ctx) => ({ ... })`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const prerenderMatch = serverSource.match(/\bexport\s+const\s+prerender\s*=\s*([^\n;]+)/);
|
|
123
|
+
let prerender = false;
|
|
124
|
+
if (prerenderMatch) {
|
|
125
|
+
const rawValue = String(prerenderMatch[1] || '').trim();
|
|
126
|
+
if (!/^(true|false)\b/.test(rawValue)) {
|
|
127
|
+
throw new Error(`Zenith server script contract violation:\n` +
|
|
128
|
+
` File: ${sourceFile}\n` +
|
|
129
|
+
` Reason: prerender must be a boolean literal\n` +
|
|
130
|
+
` Example: export const prerender = true`);
|
|
131
|
+
}
|
|
132
|
+
prerender = rawValue.startsWith('true');
|
|
133
|
+
}
|
|
134
|
+
const start = match.index ?? -1;
|
|
135
|
+
if (start < 0) {
|
|
136
|
+
return {
|
|
137
|
+
source,
|
|
138
|
+
serverScript: {
|
|
139
|
+
source: serverSource,
|
|
140
|
+
prerender,
|
|
141
|
+
has_guard: hasGuard,
|
|
142
|
+
has_load: hasLoad,
|
|
143
|
+
source_path: sourceFile
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const end = start + full.length;
|
|
148
|
+
const stripped = `${source.slice(0, start)}${source.slice(end)}`;
|
|
149
|
+
return {
|
|
150
|
+
source: stripped,
|
|
151
|
+
serverScript: {
|
|
152
|
+
source: serverSource,
|
|
153
|
+
prerender,
|
|
154
|
+
has_guard: hasGuard,
|
|
155
|
+
has_load: hasLoad,
|
|
156
|
+
source_path: sourceFile
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} source
|
|
162
|
+
* @param {Map<string, string>} registry
|
|
163
|
+
* @param {string | null} [ownerPath]
|
|
164
|
+
* @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
|
|
165
|
+
*/
|
|
166
|
+
export function collectComponentUsageAttrs(source, registry, ownerPath = null) {
|
|
167
|
+
const out = new Map();
|
|
168
|
+
let cursor = 0;
|
|
169
|
+
while (cursor < source.length) {
|
|
170
|
+
const tag = findNextKnownComponentTag(source, registry, cursor);
|
|
171
|
+
if (!tag) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
const name = tag.name;
|
|
175
|
+
const attrs = String(tag.attrs || '').trim();
|
|
176
|
+
if (!out.has(name)) {
|
|
177
|
+
out.set(name, []);
|
|
178
|
+
}
|
|
179
|
+
out.get(name).push({ attrs, ownerPath });
|
|
180
|
+
cursor = tag.end;
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* @param {string} source
|
|
186
|
+
* @param {Map<string, string>} registry
|
|
187
|
+
* @param {string | null} [ownerPath]
|
|
188
|
+
* @param {Set<string>} [visitedFiles]
|
|
189
|
+
* @param {Map<string, Array<{ attrs: string, ownerPath: string | null }>>} [out]
|
|
190
|
+
* @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
|
|
191
|
+
*/
|
|
192
|
+
export function collectRecursiveComponentUsageAttrs(source, registry, ownerPath = null, visitedFiles = new Set(), out = new Map()) {
|
|
193
|
+
const local = collectComponentUsageAttrs(source, registry, ownerPath);
|
|
194
|
+
for (const [name, attrsList] of local.entries()) {
|
|
195
|
+
if (!out.has(name)) {
|
|
196
|
+
out.set(name, []);
|
|
197
|
+
}
|
|
198
|
+
out.get(name).push(...attrsList);
|
|
199
|
+
}
|
|
200
|
+
for (const name of local.keys()) {
|
|
201
|
+
const compPath = registry.get(name);
|
|
202
|
+
if (!compPath || visitedFiles.has(compPath)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
visitedFiles.add(compPath);
|
|
206
|
+
const componentSource = readFileSync(compPath, 'utf8');
|
|
207
|
+
collectRecursiveComponentUsageAttrs(componentSource, registry, compPath, visitedFiles, out);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} pagesDir
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function deriveProjectRootFromPagesDir(pagesDir: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* @param {{ manifest: Array<{ path: string, file: string }>, pagesDir: string }} input
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export function ensureZenithTypeDeclarations(input: {
|
|
11
|
+
manifest: Array<{
|
|
12
|
+
path: string;
|
|
13
|
+
file: string;
|
|
14
|
+
}>;
|
|
15
|
+
pagesDir: string;
|
|
16
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} targetPath
|
|
6
|
+
* @param {string} next
|
|
7
|
+
*/
|
|
8
|
+
function writeIfChanged(targetPath, next) {
|
|
9
|
+
const previous = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : null;
|
|
10
|
+
if (previous === next) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
writeFileSync(targetPath, next, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} routePath
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function routeParamsType(routePath) {
|
|
20
|
+
const segments = String(routePath || '').split('/').filter(Boolean);
|
|
21
|
+
const fields = [];
|
|
22
|
+
for (const segment of segments) {
|
|
23
|
+
if (segment.startsWith(':')) {
|
|
24
|
+
fields.push(`${segment.slice(1)}: string`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (segment.startsWith('*')) {
|
|
28
|
+
const raw = segment.slice(1);
|
|
29
|
+
const name = raw.endsWith('?') ? raw.slice(0, -1) : raw;
|
|
30
|
+
fields.push(`${name}: string`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (fields.length === 0) {
|
|
34
|
+
return '{}';
|
|
35
|
+
}
|
|
36
|
+
return `{ ${fields.join(', ')} }`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {Array<{ path: string, file: string }>} manifest
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function renderZenithRouteDts(manifest) {
|
|
43
|
+
const lines = [
|
|
44
|
+
'// Auto-generated by Zenith CLI. Do not edit manually.',
|
|
45
|
+
'export {};',
|
|
46
|
+
'',
|
|
47
|
+
'declare global {',
|
|
48
|
+
' namespace Zenith {',
|
|
49
|
+
' interface RouteParamsMap {'
|
|
50
|
+
];
|
|
51
|
+
const sortedManifest = [...manifest].sort((a, b) => a.path.localeCompare(b.path));
|
|
52
|
+
for (const entry of sortedManifest) {
|
|
53
|
+
lines.push(` ${JSON.stringify(entry.path)}: ${routeParamsType(entry.path)};`);
|
|
54
|
+
}
|
|
55
|
+
lines.push(' }');
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(' type ParamsFor<P extends keyof RouteParamsMap> = RouteParamsMap[P];');
|
|
58
|
+
lines.push(' }');
|
|
59
|
+
lines.push('}');
|
|
60
|
+
lines.push('');
|
|
61
|
+
return `${lines.join('\n')}\n`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function renderZenithEnvDts() {
|
|
67
|
+
return [
|
|
68
|
+
'// Auto-generated by Zenith CLI. Do not edit manually.',
|
|
69
|
+
'export {};',
|
|
70
|
+
'',
|
|
71
|
+
'declare global {',
|
|
72
|
+
' namespace Zenith {',
|
|
73
|
+
' type Params = Record<string, string>;',
|
|
74
|
+
'',
|
|
75
|
+
' interface ErrorState {',
|
|
76
|
+
' status?: number;',
|
|
77
|
+
' code?: string;',
|
|
78
|
+
' message: string;',
|
|
79
|
+
' }',
|
|
80
|
+
'',
|
|
81
|
+
' type PageData = Record<string, unknown> & { __zenith_error?: ErrorState };',
|
|
82
|
+
'',
|
|
83
|
+
' interface RouteMeta {',
|
|
84
|
+
' id: string;',
|
|
85
|
+
' file: string;',
|
|
86
|
+
' pattern: string;',
|
|
87
|
+
' }',
|
|
88
|
+
'',
|
|
89
|
+
' interface LoadContext {',
|
|
90
|
+
' params: Params;',
|
|
91
|
+
' url: URL;',
|
|
92
|
+
' request: Request;',
|
|
93
|
+
' route: RouteMeta;',
|
|
94
|
+
' }',
|
|
95
|
+
'',
|
|
96
|
+
' type Load<T extends PageData = PageData> = (ctx: LoadContext) => Promise<T> | T;',
|
|
97
|
+
'',
|
|
98
|
+
' interface Fragment {',
|
|
99
|
+
' __zenith_fragment: true;',
|
|
100
|
+
' mount: (anchor: Node | null) => void;',
|
|
101
|
+
' unmount: () => void;',
|
|
102
|
+
' }',
|
|
103
|
+
'',
|
|
104
|
+
' type Renderable =',
|
|
105
|
+
' | string',
|
|
106
|
+
' | number',
|
|
107
|
+
' | boolean',
|
|
108
|
+
' | null',
|
|
109
|
+
' | undefined',
|
|
110
|
+
' | Renderable[]',
|
|
111
|
+
' | Fragment;',
|
|
112
|
+
' }',
|
|
113
|
+
'}',
|
|
114
|
+
''
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} pagesDir
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
export function deriveProjectRootFromPagesDir(pagesDir) {
|
|
122
|
+
const normalized = resolve(pagesDir);
|
|
123
|
+
const parent = dirname(normalized);
|
|
124
|
+
if (basename(parent) === 'src') {
|
|
125
|
+
return dirname(parent);
|
|
126
|
+
}
|
|
127
|
+
return parent;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @param {{ manifest: Array<{ path: string, file: string }>, pagesDir: string }} input
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
export async function ensureZenithTypeDeclarations(input) {
|
|
134
|
+
const projectRoot = deriveProjectRootFromPagesDir(input.pagesDir);
|
|
135
|
+
const zenithDir = resolve(projectRoot, '.zenith');
|
|
136
|
+
await mkdir(zenithDir, { recursive: true });
|
|
137
|
+
const envPath = join(zenithDir, 'zenith-env.d.ts');
|
|
138
|
+
const routesPath = join(zenithDir, 'zenith-routes.d.ts');
|
|
139
|
+
writeIfChanged(envPath, renderZenithEnvDts());
|
|
140
|
+
writeIfChanged(routesPath, renderZenithRouteDts(input.manifest));
|
|
141
|
+
const tsconfigPath = resolve(projectRoot, 'tsconfig.json');
|
|
142
|
+
if (!existsSync(tsconfigPath)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const raw = readFileSync(tsconfigPath, 'utf8');
|
|
147
|
+
const parsed = JSON.parse(raw);
|
|
148
|
+
const include = Array.isArray(parsed.include) ? [...parsed.include] : [];
|
|
149
|
+
if (!include.includes('.zenith/**/*.d.ts')) {
|
|
150
|
+
include.push('.zenith/**/*.d.ts');
|
|
151
|
+
parsed.include = include;
|
|
152
|
+
writeIfChanged(tsconfigPath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Non-JSON tsconfig variants are left untouched.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} key
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function renderObjectKey(key: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} source
|
|
8
|
+
* @returns {string[]}
|
|
9
|
+
*/
|
|
10
|
+
export function extractDeclaredIdentifiers(source: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} expr
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeTypeScriptExpression(expr: string): string;
|
|
16
|
+
export function expandScopedShorthandPropertiesInSource(source: any): string;
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} expr
|
|
19
|
+
* @param {Map<string, string>} scopeMap
|
|
20
|
+
* @param {Set<string> | null | undefined} scopeAmbiguous
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function rewriteIdentifiersWithinExpression(expr: string, scopeMap: Map<string, string>, scopeAmbiguous: Set<string> | null | undefined): string;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { loadTypeScriptApi } from './compiler-runtime.js';
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} key
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export function renderObjectKey(key) {
|
|
7
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
|
|
8
|
+
return key;
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(key);
|
|
11
|
+
}
|
|
12
|
+
function deriveScopedIdentifierAlias(value) {
|
|
13
|
+
const ident = String(value || '').trim();
|
|
14
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(ident)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const parts = ident.split('_').filter(Boolean);
|
|
18
|
+
const candidate = parts.length > 1 ? parts[parts.length - 1] : ident;
|
|
19
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(candidate) ? candidate : ident;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} source
|
|
23
|
+
* @returns {string[]}
|
|
24
|
+
*/
|
|
25
|
+
export function extractDeclaredIdentifiers(source) {
|
|
26
|
+
const text = String(source || '').trim();
|
|
27
|
+
if (!text) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const ts = loadTypeScriptApi();
|
|
31
|
+
if (ts) {
|
|
32
|
+
const sourceFile = ts.createSourceFile('zenith-hoisted-declaration.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
33
|
+
const identifiers = [];
|
|
34
|
+
const collectBindingNames = (name) => {
|
|
35
|
+
if (ts.isIdentifier(name)) {
|
|
36
|
+
identifiers.push(name.text);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
40
|
+
for (const element of name.elements) {
|
|
41
|
+
if (ts.isBindingElement(element)) {
|
|
42
|
+
collectBindingNames(element.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
for (const statement of sourceFile.statements) {
|
|
48
|
+
if (!ts.isVariableStatement(statement)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
52
|
+
collectBindingNames(declaration.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (identifiers.length > 0) {
|
|
56
|
+
return identifiers;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const fallback = [];
|
|
60
|
+
const match = text.match(/^\s*(?:const|let|var)\s+([\s\S]+?);?\s*$/);
|
|
61
|
+
if (!match) {
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
const declarationList = match[1];
|
|
65
|
+
const identifierRe = /(?:^|,)\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=,]+)?=/g;
|
|
66
|
+
let found;
|
|
67
|
+
while ((found = identifierRe.exec(declarationList)) !== null) {
|
|
68
|
+
fallback.push(found[1]);
|
|
69
|
+
}
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} expr
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeTypeScriptExpression(expr) {
|
|
77
|
+
const source = String(expr || '').trim();
|
|
78
|
+
if (!source) {
|
|
79
|
+
return source;
|
|
80
|
+
}
|
|
81
|
+
const ts = loadTypeScriptApi();
|
|
82
|
+
if (!ts) {
|
|
83
|
+
return source;
|
|
84
|
+
}
|
|
85
|
+
const wrapped = `const __zenith_expr__ = (${source});`;
|
|
86
|
+
let transpiled;
|
|
87
|
+
try {
|
|
88
|
+
transpiled = ts.transpileModule(wrapped, {
|
|
89
|
+
fileName: 'zenith-expression.ts',
|
|
90
|
+
compilerOptions: {
|
|
91
|
+
module: ts.ModuleKind.ESNext,
|
|
92
|
+
target: ts.ScriptTarget.ESNext,
|
|
93
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
94
|
+
},
|
|
95
|
+
reportDiagnostics: false,
|
|
96
|
+
}).outputText;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return source;
|
|
100
|
+
}
|
|
101
|
+
let sourceFile;
|
|
102
|
+
try {
|
|
103
|
+
sourceFile = ts.createSourceFile('zenith-expression.js', transpiled, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return source;
|
|
107
|
+
}
|
|
108
|
+
for (const statement of sourceFile.statements) {
|
|
109
|
+
if (!ts.isVariableStatement(statement)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const declaration = statement.declarationList.declarations.find((entry) => ts.isIdentifier(entry.name) && entry.name.text === '__zenith_expr__');
|
|
113
|
+
const initializer = declaration?.initializer;
|
|
114
|
+
if (!initializer) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const root = ts.isParenthesizedExpression(initializer) ? initializer.expression : initializer;
|
|
118
|
+
return ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
|
|
119
|
+
.printNode(ts.EmitHint.Unspecified, root, sourceFile)
|
|
120
|
+
.trim();
|
|
121
|
+
}
|
|
122
|
+
return source;
|
|
123
|
+
}
|
|
124
|
+
export function expandScopedShorthandPropertiesInSource(source) {
|
|
125
|
+
const text = String(source || '');
|
|
126
|
+
if (!text.trim()) {
|
|
127
|
+
return text;
|
|
128
|
+
}
|
|
129
|
+
const ts = loadTypeScriptApi();
|
|
130
|
+
if (!ts) {
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
133
|
+
let sourceFile;
|
|
134
|
+
try {
|
|
135
|
+
sourceFile = ts.createSourceFile('zenith-shorthand-fix.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
140
|
+
const transformer = (context) => {
|
|
141
|
+
const visit = (node) => {
|
|
142
|
+
if (ts.isShorthandPropertyAssignment(node)) {
|
|
143
|
+
const alias = deriveScopedIdentifierAlias(node.name.text);
|
|
144
|
+
if (typeof alias === 'string' && alias.length > 0 && alias !== node.name.text) {
|
|
145
|
+
return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(alias), ts.factory.createIdentifier(node.name.text));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return ts.visitEachChild(node, visit, context);
|
|
149
|
+
};
|
|
150
|
+
return (node) => ts.visitNode(node, visit);
|
|
151
|
+
};
|
|
152
|
+
const result = ts.transform(sourceFile, [transformer]);
|
|
153
|
+
try {
|
|
154
|
+
return ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
|
|
155
|
+
.printFile(result.transformed[0])
|
|
156
|
+
.trimEnd();
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
result.dispose();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} expr
|
|
167
|
+
* @param {Map<string, string>} scopeMap
|
|
168
|
+
* @param {Set<string> | null | undefined} scopeAmbiguous
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
export function rewriteIdentifiersWithinExpression(expr, scopeMap, scopeAmbiguous) {
|
|
172
|
+
const ts = loadTypeScriptApi();
|
|
173
|
+
if (!(scopeMap instanceof Map) || !ts) {
|
|
174
|
+
return normalizeTypeScriptExpression(expr);
|
|
175
|
+
}
|
|
176
|
+
const wrapped = `const __zenith_expr__ = (${expr});`;
|
|
177
|
+
let sourceFile;
|
|
178
|
+
try {
|
|
179
|
+
sourceFile = ts.createSourceFile('zenith-expression.ts', wrapped, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return expr;
|
|
183
|
+
}
|
|
184
|
+
const statement = sourceFile.statements[0];
|
|
185
|
+
if (!statement || !ts.isVariableStatement(statement)) {
|
|
186
|
+
return expr;
|
|
187
|
+
}
|
|
188
|
+
const initializer = statement.declarationList.declarations[0]?.initializer;
|
|
189
|
+
const root = initializer && ts.isParenthesizedExpression(initializer) ? initializer.expression : initializer;
|
|
190
|
+
if (!root) {
|
|
191
|
+
return expr;
|
|
192
|
+
}
|
|
193
|
+
const replacements = [];
|
|
194
|
+
const collectBoundNames = (name, target) => {
|
|
195
|
+
if (ts.isIdentifier(name)) {
|
|
196
|
+
target.add(name.text);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
200
|
+
for (const element of name.elements) {
|
|
201
|
+
if (ts.isBindingElement(element)) {
|
|
202
|
+
collectBoundNames(element.name, target);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const shouldSkipIdentifier = (node, localBindings) => {
|
|
208
|
+
if (localBindings.has(node.text)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
const parent = node.parent;
|
|
212
|
+
if (!parent) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (ts.isShorthandPropertyAssignment(parent)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (ts.isBindingElement(parent) && parent.name === node) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (ts.isParameter(parent) && parent.name === node) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
};
|
|
232
|
+
const visit = (node, localBindings) => {
|
|
233
|
+
let nextBindings = localBindings;
|
|
234
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
235
|
+
nextBindings = new Set(localBindings);
|
|
236
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
237
|
+
nextBindings.add(node.name.text);
|
|
238
|
+
}
|
|
239
|
+
for (const param of node.parameters) {
|
|
240
|
+
collectBoundNames(param.name, nextBindings);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (ts.isIdentifier(node) && !shouldSkipIdentifier(node, nextBindings)) {
|
|
244
|
+
const rewritten = scopeMap.get(node.text);
|
|
245
|
+
if (typeof rewritten === 'string' &&
|
|
246
|
+
rewritten.length > 0 &&
|
|
247
|
+
rewritten !== node.text &&
|
|
248
|
+
!(scopeAmbiguous instanceof Set && scopeAmbiguous.has(node.text))) {
|
|
249
|
+
replacements.push({
|
|
250
|
+
start: node.getStart(sourceFile),
|
|
251
|
+
end: node.getEnd(),
|
|
252
|
+
text: rewritten
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
ts.forEachChild(node, (child) => visit(child, nextBindings));
|
|
257
|
+
};
|
|
258
|
+
visit(root, new Set());
|
|
259
|
+
if (replacements.length === 0) {
|
|
260
|
+
return normalizeTypeScriptExpression(expr);
|
|
261
|
+
}
|
|
262
|
+
let rewritten = wrapped;
|
|
263
|
+
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
|
264
|
+
rewritten = `${rewritten.slice(0, replacement.start)}${replacement.text}${rewritten.slice(replacement.end)}`;
|
|
265
|
+
}
|
|
266
|
+
const prefix = 'const __zenith_expr__ = (';
|
|
267
|
+
const suffix = ');';
|
|
268
|
+
if (!rewritten.startsWith(prefix) || !rewritten.endsWith(suffix)) {
|
|
269
|
+
return normalizeTypeScriptExpression(expr);
|
|
270
|
+
}
|
|
271
|
+
return normalizeTypeScriptExpression(rewritten.slice(prefix.length, rewritten.length - suffix.length));
|
|
272
|
+
}
|