@pracht/vite-plugin 0.11.0 → 0.12.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/index.d.mts +37 -37
- package/dist/index.mjs +918 -537
- package/dist/pages-router-BI34Cani.mjs +906 -0
- package/dist/pages-router.d.mts +105 -1
- package/dist/pages-router.mjs +2 -2
- package/package.json +13 -6
- package/virtual.d.ts +255 -0
- package/dist/pages-router-MA9rOl88.mjs +0 -618
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { parseAst } from "vite";
|
|
4
|
+
import { PAGES_APP_CONFIG_EXPORTS, PAGES_APP_CONFIG_EXPORTS as PAGES_APP_CONFIG_EXPORTS$1, evaluateLiteral, findOwningPagesShell, findOwningPagesShell as findOwningPagesShell$1, hasNamedMiddlewareExport, hasNamedValueExport, hasValueStarExport, maskCommentsAndStrings, maskMarkdownFences, pagesShellName, pagesShellName as pagesShellName$1, readNamedExportInitializer, readPageRevalidation, readPageStreaming, readPageStringExport, resolvePagesCapabilityName } from "@pracht/capabilities/static";
|
|
5
|
+
import { parse } from "@babel/parser";
|
|
6
|
+
import { initSync, parse as parse$1 } from "es-module-lexer";
|
|
7
|
+
//#region src/client-module-query.ts
|
|
8
|
+
const CLIENT_MODULE_QUERY = "pracht-client";
|
|
9
|
+
const PRACHT_CLIENT_MODULE_QUERY = `?${CLIENT_MODULE_QUERY}`;
|
|
10
|
+
function isPrachtClientModuleId(id) {
|
|
11
|
+
const queryStart = id.indexOf("?");
|
|
12
|
+
if (queryStart === -1) return false;
|
|
13
|
+
return id.slice(queryStart + 1).split("&").includes(CLIENT_MODULE_QUERY);
|
|
14
|
+
}
|
|
15
|
+
function stripPrachtClientModuleQuery(id) {
|
|
16
|
+
const queryStart = id.indexOf("?");
|
|
17
|
+
if (queryStart === -1) return id;
|
|
18
|
+
const path = id.slice(0, queryStart);
|
|
19
|
+
const query = id.slice(queryStart + 1).split("&").filter((part) => part !== CLIENT_MODULE_QUERY);
|
|
20
|
+
return query.length > 0 ? `${path}?${query.join("&")}` : path;
|
|
21
|
+
}
|
|
22
|
+
/** Extensions `@prefresh/vite` accepts: `/\.(c|m)?(t|j)sx?$/`, anchored at end. */
|
|
23
|
+
const PREFRESH_EXTENSION_RE = /\.((?:c|m)?[tj]sx?)$/i;
|
|
24
|
+
function isPrefreshCompatibleId(id) {
|
|
25
|
+
return PREFRESH_EXTENSION_RE.test(id);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The id to hand `@prefresh/vite` for a pracht client module.
|
|
29
|
+
*
|
|
30
|
+
* Prefresh uses the id for exactly three things: its `/\.(c|m)?(t|j)sx?$/`
|
|
31
|
+
* filter, a `/\.tsx?$/` check that picks the TypeScript parser plugin, and the
|
|
32
|
+
* key it embeds in the `$RefreshReg$` it injects. A query-carrying id fails the
|
|
33
|
+
* first two, which is why route and shell modules got no Fast Refresh at all —
|
|
34
|
+
* but simply stripping the query fails the third: one file under `src/routes`
|
|
35
|
+
* can reach the browser as *two* module instances, once through the route glob
|
|
36
|
+
* as `…/x.tsx?pracht-client` and once as a plain import from a sibling route.
|
|
37
|
+
* Both would then register under the same key, and `@prefresh/core` treats a
|
|
38
|
+
* second `register()` for a known key with a different function object as a
|
|
39
|
+
* pending component replacement — which the next unrelated Fast Refresh
|
|
40
|
+
* flushes, tearing down and re-running the untouched copy's effects.
|
|
41
|
+
*
|
|
42
|
+
* A reserved, length-prefixed namespace keeps the real extension last, so the
|
|
43
|
+
* filter and parser check still pass, while giving each complete module id its
|
|
44
|
+
* own registration key. Keeping the authored id verbatim makes the mapping
|
|
45
|
+
* injective; keeping it behind a non-file prefix prevents a real sibling such
|
|
46
|
+
* as `x.pracht-client.tsx` from colliding with the synthetic key. The id is
|
|
47
|
+
* never resolved against the filesystem; the JSX dev transform has already
|
|
48
|
+
* stamped `_jsxFileName` from the real id by the time prefresh runs, so dev
|
|
49
|
+
* source locations and open-in-editor are unaffected.
|
|
50
|
+
*
|
|
51
|
+
* Compiled formats whose real extension prefresh rejects (`.md`, `.mdx`, and
|
|
52
|
+
* configured additional formats) instead keep that extension in the basename
|
|
53
|
+
* and receive a synthetic `.jsx`. Their companion Vite plugin has already
|
|
54
|
+
* turned the authored format into JavaScript by the time this id is used.
|
|
55
|
+
*/
|
|
56
|
+
function toPrachtClientPrefreshId(id) {
|
|
57
|
+
const stripped = stripPrachtClientModuleQuery(id);
|
|
58
|
+
const queryStart = stripped.indexOf("?");
|
|
59
|
+
const path = queryStart === -1 ? stripped : stripped.slice(0, queryStart);
|
|
60
|
+
const parserExtension = PREFRESH_EXTENSION_RE.exec(path)?.[1] ?? "jsx";
|
|
61
|
+
return `pracht-client:${id.length}:${id}.${parserExtension}`;
|
|
62
|
+
}
|
|
63
|
+
function getRolldownLang(id) {
|
|
64
|
+
const path = stripPrachtClientModuleQuery(id).split("?")[0];
|
|
65
|
+
if (/\.(c|m)?tsx$/i.test(path)) return "tsx";
|
|
66
|
+
if (/\.(c|m)?ts$/i.test(path)) return "ts";
|
|
67
|
+
if (/\.(c|m)?jsx$/i.test(path)) return "jsx";
|
|
68
|
+
if (/\.mdx?$/i.test(path)) return "jsx";
|
|
69
|
+
if (/\.(c|m)?js$/i.test(path)) return "js";
|
|
70
|
+
return "tsx";
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/route-extensions.ts
|
|
74
|
+
const BUILT_IN_ROUTE_EXTENSIONS = [
|
|
75
|
+
".ts",
|
|
76
|
+
".tsx",
|
|
77
|
+
".js",
|
|
78
|
+
".jsx",
|
|
79
|
+
".md",
|
|
80
|
+
".mdx"
|
|
81
|
+
];
|
|
82
|
+
const LEGACY_BARE_ROUTE_EXTENSIONS = [".tsrx"];
|
|
83
|
+
const DEFAULT_ROUTE_EXTENSIONS = [...BUILT_IN_ROUTE_EXTENSIONS, ...LEGACY_BARE_ROUTE_EXTENSIONS];
|
|
84
|
+
const DEFAULT_SHELL_EXTENSIONS = [
|
|
85
|
+
".ts",
|
|
86
|
+
".tsx",
|
|
87
|
+
".js",
|
|
88
|
+
".jsx",
|
|
89
|
+
...LEGACY_BARE_ROUTE_EXTENSIONS
|
|
90
|
+
];
|
|
91
|
+
const EXTENSION_RE = /^\.[a-z0-9][a-z0-9_-]*$/i;
|
|
92
|
+
function normalizeAdditionalExtensions(extensions) {
|
|
93
|
+
if (extensions === void 0) return [];
|
|
94
|
+
if (!Array.isArray(extensions)) throw new Error("pracht({ additionalExtensions }) expects an array of dot-prefixed extensions.");
|
|
95
|
+
const normalized = extensions.map((extension) => {
|
|
96
|
+
if (typeof extension !== "string" || !EXTENSION_RE.test(extension)) throw new Error(`pracht({ additionalExtensions }) expects dot-prefixed extensions such as ".vue", got ${JSON.stringify(extension)}.`);
|
|
97
|
+
return extension.toLowerCase();
|
|
98
|
+
});
|
|
99
|
+
const defaults = new Set(BUILT_IN_ROUTE_EXTENSIONS);
|
|
100
|
+
return [...new Set(normalized)].filter((extension) => !defaults.has(extension));
|
|
101
|
+
}
|
|
102
|
+
function extensionGlob(extensions) {
|
|
103
|
+
const names = extensions.map((extension) => extension.slice(1));
|
|
104
|
+
return names.length === 1 ? names[0] : `{${names.join(",")}}`;
|
|
105
|
+
}
|
|
106
|
+
function withAdditionalExtensions(defaults, additionalExtensions) {
|
|
107
|
+
return new Set([...defaults, ...additionalExtensions]);
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/route-loader-hints.ts
|
|
111
|
+
initSync();
|
|
112
|
+
function namedDeclarationRe(exportName) {
|
|
113
|
+
return new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+${exportName}\\b`);
|
|
114
|
+
}
|
|
115
|
+
const HEAD_DECLARATION_RE = namedDeclarationRe("head");
|
|
116
|
+
const HEADERS_DECLARATION_RE = namedDeclarationRe("headers");
|
|
117
|
+
const STATIC_PATHS_DECLARATION_RE = namedDeclarationRe("getStaticPaths");
|
|
118
|
+
const EXPORT_BLOCK_RE = /export\s*\{([^}]*)\}\s*(?:from\s*["'][^"']+["'])?/g;
|
|
119
|
+
const EXPORT_ALL_RE = /export\s+\*\s+from\b/;
|
|
120
|
+
const EXPORT_VARIABLE_DECLARATION_RE = /export\s+(?:const|let|var)\b/g;
|
|
121
|
+
function isExportAllStatement(source) {
|
|
122
|
+
const withoutComments = source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, " ");
|
|
123
|
+
return /^\s*export\s*\*/.test(withoutComments);
|
|
124
|
+
}
|
|
125
|
+
function exportedVariableDeclarationIncludesLoader(source) {
|
|
126
|
+
for (const declaration of source.matchAll(/\bexport\s+(?:const|let|var)\b/g)) {
|
|
127
|
+
let index = (declaration.index ?? 0) + declaration[0].length;
|
|
128
|
+
while (index < source.length) {
|
|
129
|
+
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
130
|
+
const bindingStart = index;
|
|
131
|
+
const opening = source[index];
|
|
132
|
+
if (opening === "{" || opening === "[") {
|
|
133
|
+
const closing = opening === "{" ? "}" : "]";
|
|
134
|
+
let depth = 0;
|
|
135
|
+
do {
|
|
136
|
+
const char = source[index++];
|
|
137
|
+
if (char === opening) depth += 1;
|
|
138
|
+
if (char === closing) depth -= 1;
|
|
139
|
+
} while (index < source.length && depth > 0);
|
|
140
|
+
if (/\bloader\b/.test(source.slice(bindingStart, index))) return true;
|
|
141
|
+
} else {
|
|
142
|
+
const binding = /^[A-Za-z_$][\w$]*/.exec(source.slice(index));
|
|
143
|
+
if (!binding) break;
|
|
144
|
+
if (binding[0] === "loader") return true;
|
|
145
|
+
index += binding[0].length;
|
|
146
|
+
}
|
|
147
|
+
let parentheses = 0;
|
|
148
|
+
let brackets = 0;
|
|
149
|
+
let braces = 0;
|
|
150
|
+
for (; index < source.length; index += 1) {
|
|
151
|
+
const char = source[index];
|
|
152
|
+
if (char === "(") parentheses += 1;
|
|
153
|
+
else if (char === ")") parentheses = Math.max(0, parentheses - 1);
|
|
154
|
+
else if (char === "[") brackets += 1;
|
|
155
|
+
else if (char === "]") brackets = Math.max(0, brackets - 1);
|
|
156
|
+
else if (char === "{") braces += 1;
|
|
157
|
+
else if (char === "}") braces = Math.max(0, braces - 1);
|
|
158
|
+
if (parentheses === 0 && brackets === 0 && braces === 0) {
|
|
159
|
+
if (char === ";") break;
|
|
160
|
+
if (char === ",") {
|
|
161
|
+
index += 1;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (source[index] === ";" || index >= source.length) break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
function detectLoaderExportFallback(source) {
|
|
172
|
+
const masked = maskCommentsAndStrings(source);
|
|
173
|
+
if (/\bexport\s+(?:async\s+)?function\s+loader\b/.test(masked)) return true;
|
|
174
|
+
if (exportedVariableDeclarationIncludesLoader(masked)) return true;
|
|
175
|
+
if (/\bexport\s*\*/.test(masked)) return true;
|
|
176
|
+
for (const match of masked.matchAll(/\bexport\s*\{([^}]*)\}/g)) if (match[1].split(",").map((specifier) => specifier.trim()).filter(Boolean).some((specifier) => {
|
|
177
|
+
const names = /^(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier);
|
|
178
|
+
if (!names || specifier.startsWith("type ")) return false;
|
|
179
|
+
return (names[2] ?? names[1]) === "loader";
|
|
180
|
+
})) return true;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function topLevelAssignmentIndex(source) {
|
|
184
|
+
let parentheses = 0;
|
|
185
|
+
let brackets = 0;
|
|
186
|
+
let braces = 0;
|
|
187
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
188
|
+
const char = source[index];
|
|
189
|
+
if (char === "(") parentheses += 1;
|
|
190
|
+
else if (char === ")") parentheses = Math.max(0, parentheses - 1);
|
|
191
|
+
else if (char === "[") brackets += 1;
|
|
192
|
+
else if (char === "]") brackets = Math.max(0, brackets - 1);
|
|
193
|
+
else if (char === "{") braces += 1;
|
|
194
|
+
else if (char === "}") braces = Math.max(0, braces - 1);
|
|
195
|
+
else if (char === "=" && parentheses === 0 && brackets === 0 && braces === 0) return index;
|
|
196
|
+
}
|
|
197
|
+
return -1;
|
|
198
|
+
}
|
|
199
|
+
function bindingExportsName(source, exportName) {
|
|
200
|
+
const assignmentIndex = topLevelAssignmentIndex(source);
|
|
201
|
+
const binding = assignmentIndex === -1 ? source : source.slice(0, assignmentIndex);
|
|
202
|
+
return new RegExp(`\\b${exportName}\\b`).test(binding);
|
|
203
|
+
}
|
|
204
|
+
function variableDeclarationExports(source, exportName) {
|
|
205
|
+
for (const match of source.matchAll(EXPORT_VARIABLE_DECLARATION_RE)) {
|
|
206
|
+
let declarationStart = (match.index ?? 0) + match[0].length;
|
|
207
|
+
let parentheses = 0;
|
|
208
|
+
let brackets = 0;
|
|
209
|
+
let braces = 0;
|
|
210
|
+
for (let index = declarationStart; index <= source.length; index += 1) {
|
|
211
|
+
const char = source[index];
|
|
212
|
+
if (char === "(") parentheses += 1;
|
|
213
|
+
else if (char === ")") parentheses = Math.max(0, parentheses - 1);
|
|
214
|
+
else if (char === "[") brackets += 1;
|
|
215
|
+
else if (char === "]") brackets = Math.max(0, brackets - 1);
|
|
216
|
+
else if (char === "{") braces += 1;
|
|
217
|
+
else if (char === "}") braces = Math.max(0, braces - 1);
|
|
218
|
+
if (parentheses === 0 && brackets === 0 && braces === 0 && (char === "," || char === ";" || char === void 0)) {
|
|
219
|
+
if (bindingExportsName(source.slice(declarationStart, index), exportName)) return true;
|
|
220
|
+
if (char !== ",") break;
|
|
221
|
+
declarationStart = index + 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
function exportSpecifiersInclude(specifiers, exportName) {
|
|
228
|
+
return specifiers.split(",").map((specifier) => specifier.trim()).filter(Boolean).some((specifier) => {
|
|
229
|
+
const match = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier);
|
|
230
|
+
if (!match) return false;
|
|
231
|
+
const [, localName, exportedName] = match;
|
|
232
|
+
return (exportedName ?? localName) === exportName;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Whether `source` exports `exportName`, via a declaration, an export block,
|
|
237
|
+
* or an `export *` re-export (which could expose anything, so it counts).
|
|
238
|
+
*
|
|
239
|
+
* Ordinary TS/JS is parsed exactly, including string-literal export names.
|
|
240
|
+
* Custom syntaxes fall back to masked lexical detection so prose or a string
|
|
241
|
+
* literal mentioning the name cannot produce a false positive.
|
|
242
|
+
*/
|
|
243
|
+
function detectNamedExport(source, exportName, declarationRe) {
|
|
244
|
+
const parsedResult = inspectParsedModule(source, exportName);
|
|
245
|
+
if (parsedResult !== void 0) return parsedResult;
|
|
246
|
+
return detectNamedExportInMasked(maskCommentsAndStrings(source), exportName, declarationRe);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The lexical half of `detectNamedExport()`, over already-masked source. Every
|
|
250
|
+
* hint table falls back for the same file, so the mask is computed once.
|
|
251
|
+
*/
|
|
252
|
+
function detectNamedExportInMasked(analysisSource, exportName, declarationRe) {
|
|
253
|
+
if (declarationRe.test(analysisSource) || variableDeclarationExports(analysisSource, exportName)) return true;
|
|
254
|
+
for (const match of analysisSource.matchAll(EXPORT_BLOCK_RE)) if (exportSpecifiersInclude(match[1], exportName)) return true;
|
|
255
|
+
return EXPORT_ALL_RE.test(analysisSource);
|
|
256
|
+
}
|
|
257
|
+
function detectHeadExport(source) {
|
|
258
|
+
return detectNamedExport(source, "head", HEAD_DECLARATION_RE);
|
|
259
|
+
}
|
|
260
|
+
/** Whether the route or shell module exports document response headers. */
|
|
261
|
+
function detectHeadersExport(source) {
|
|
262
|
+
return detectNamedExport(source, "headers", HEADERS_DECLARATION_RE);
|
|
263
|
+
}
|
|
264
|
+
function isSyntaxNode(value) {
|
|
265
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
266
|
+
}
|
|
267
|
+
function bindingIncludesName(node, exportName) {
|
|
268
|
+
if (!isSyntaxNode(node)) return false;
|
|
269
|
+
if (node.type === "Identifier") return node.name === exportName;
|
|
270
|
+
if (node.type === "AssignmentPattern") return bindingIncludesName(node.left, exportName);
|
|
271
|
+
if (node.type === "RestElement") return bindingIncludesName(node.argument, exportName);
|
|
272
|
+
if (node.type === "ArrayPattern") return Array.isArray(node.elements) && node.elements.some((element) => bindingIncludesName(element, exportName));
|
|
273
|
+
if (node.type === "ObjectPattern") return Array.isArray(node.properties) && node.properties.some((property) => {
|
|
274
|
+
if (!isSyntaxNode(property)) return false;
|
|
275
|
+
return property.type === "RestElement" ? bindingIncludesName(property.argument, exportName) : bindingIncludesName(property.value, exportName);
|
|
276
|
+
});
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
function exportedNameMatches(node, exportName) {
|
|
280
|
+
if (!isSyntaxNode(node)) return false;
|
|
281
|
+
if (node.type === "Identifier") return node.name === exportName;
|
|
282
|
+
if (node.type === "StringLiteral") return node.value === exportName;
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Which of `exportNames` the module exports, or `undefined` when no parser
|
|
287
|
+
* accepted the source (a custom route syntax such as TSRX).
|
|
288
|
+
*
|
|
289
|
+
* Every hint table asks the same question of the same file, so they share one
|
|
290
|
+
* parse: four separate `inspectParsedModule()` calls used to parse each route
|
|
291
|
+
* module four times, once per table.
|
|
292
|
+
*/
|
|
293
|
+
function inspectParsedModuleExports(source, exportNames) {
|
|
294
|
+
for (const plugins of [["typescript", "jsx"], ["typescript"]]) {
|
|
295
|
+
let body;
|
|
296
|
+
try {
|
|
297
|
+
body = parse(source, {
|
|
298
|
+
plugins: [...plugins],
|
|
299
|
+
sourceType: "module"
|
|
300
|
+
}).program.body;
|
|
301
|
+
} catch {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const found = {};
|
|
305
|
+
for (const exportName of exportNames) found[exportName] = false;
|
|
306
|
+
for (const statement of body) {
|
|
307
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
308
|
+
if (statement.exportKind !== "type") {
|
|
309
|
+
for (const exportName of exportNames) found[exportName] = true;
|
|
310
|
+
return found;
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
|
|
315
|
+
for (const exportName of exportNames) {
|
|
316
|
+
if (found[exportName]) continue;
|
|
317
|
+
if (Array.isArray(statement.specifiers) && statement.specifiers.some((specifier) => isSyntaxNode(specifier) && specifier.exportKind !== "type" && exportedNameMatches(specifier.exported, exportName))) {
|
|
318
|
+
found[exportName] = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const declaration = statement.declaration;
|
|
322
|
+
if (!isSyntaxNode(declaration)) continue;
|
|
323
|
+
if (declaration.declare === true || declaration.type.startsWith("TS")) continue;
|
|
324
|
+
if (declaration.type === "VariableDeclaration") {
|
|
325
|
+
if (Array.isArray(declaration.declarations) && declaration.declarations.some((declarator) => isSyntaxNode(declarator) && bindingIncludesName(declarator.id, exportName))) found[exportName] = true;
|
|
326
|
+
} else if (bindingIncludesName(declaration.id, exportName)) found[exportName] = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return found;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function inspectParsedModule(source, exportName) {
|
|
333
|
+
return inspectParsedModuleExports(source, [exportName])?.[exportName];
|
|
334
|
+
}
|
|
335
|
+
function detectLoaderExport(source) {
|
|
336
|
+
const parsedResult = inspectParsedModule(source, "loader");
|
|
337
|
+
if (parsedResult !== void 0) return parsedResult;
|
|
338
|
+
return detectLoaderExportWithoutParser(source);
|
|
339
|
+
}
|
|
340
|
+
/** The `loader` detection that applies once no standard parser accepted the source. */
|
|
341
|
+
function detectLoaderExportWithoutParser(source) {
|
|
342
|
+
try {
|
|
343
|
+
const [imports, exports] = parse$1(source);
|
|
344
|
+
if (exports.some((entry) => entry.n === "loader")) return true;
|
|
345
|
+
for (const entry of imports) if (entry.d === -1 && isExportAllStatement(source.slice(entry.ss, entry.se))) return true;
|
|
346
|
+
} catch {}
|
|
347
|
+
return detectLoaderExportFallback(source);
|
|
348
|
+
}
|
|
349
|
+
/** `readdirSync` failing because the directory is simply absent is not a gap. */
|
|
350
|
+
function isMissingDirectory(error) {
|
|
351
|
+
const code = error?.code;
|
|
352
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
353
|
+
}
|
|
354
|
+
function scanRouteFiles(dir, extensions, scan) {
|
|
355
|
+
let entries;
|
|
356
|
+
try {
|
|
357
|
+
entries = readdirSync(dir);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (!isMissingDirectory(error)) scan.incomplete = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
for (const entry of entries) {
|
|
363
|
+
const abs = join(dir, entry);
|
|
364
|
+
let stat;
|
|
365
|
+
try {
|
|
366
|
+
stat = statSync(abs);
|
|
367
|
+
} catch {
|
|
368
|
+
scan.incomplete = true;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (stat.isDirectory()) {
|
|
372
|
+
scanRouteFiles(abs, extensions, scan);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (extensions.has(extname(entry))) scan.files.push(abs);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function toPosixPath(path) {
|
|
379
|
+
return path.replace(/\\/g, "/");
|
|
380
|
+
}
|
|
381
|
+
const ROUTE_HINT_EXPORTS = [
|
|
382
|
+
"loader",
|
|
383
|
+
"head",
|
|
384
|
+
"headers",
|
|
385
|
+
"getStaticPaths"
|
|
386
|
+
];
|
|
387
|
+
function analyzeRouteExports(source) {
|
|
388
|
+
const parsed = inspectParsedModuleExports(source, ROUTE_HINT_EXPORTS);
|
|
389
|
+
if (parsed) return parsed;
|
|
390
|
+
const analysisSource = maskCommentsAndStrings(source);
|
|
391
|
+
return {
|
|
392
|
+
getStaticPaths: detectNamedExportInMasked(analysisSource, "getStaticPaths", STATIC_PATHS_DECLARATION_RE),
|
|
393
|
+
head: detectNamedExportInMasked(analysisSource, "head", HEAD_DECLARATION_RE),
|
|
394
|
+
headers: detectNamedExportInMasked(analysisSource, "headers", HEADERS_DECLARATION_RE),
|
|
395
|
+
loader: detectLoaderExportWithoutParser(source)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function routeHintKeys(routesDir, file, options) {
|
|
399
|
+
const keys = /* @__PURE__ */ new Set();
|
|
400
|
+
if (options.appFileDir) {
|
|
401
|
+
const relativeToAppFile = toPosixPath(relative(options.appFileDir, file));
|
|
402
|
+
keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
|
|
403
|
+
}
|
|
404
|
+
const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
|
|
405
|
+
if (routeRootPrefix) keys.add(`${routeRootPrefix}/${toPosixPath(relative(routesDir, file))}`);
|
|
406
|
+
return keys;
|
|
407
|
+
}
|
|
408
|
+
function createRouteHints(routesDir, options = {}) {
|
|
409
|
+
const additionalExtensions = normalizeAdditionalExtensions(options.additionalExtensions);
|
|
410
|
+
const extensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, additionalExtensions);
|
|
411
|
+
const scan = {
|
|
412
|
+
files: [],
|
|
413
|
+
incomplete: false
|
|
414
|
+
};
|
|
415
|
+
scanRouteFiles(routesDir, extensions, scan);
|
|
416
|
+
const hints = {
|
|
417
|
+
capabilities: {},
|
|
418
|
+
head: {},
|
|
419
|
+
headers: {},
|
|
420
|
+
incomplete: scan.incomplete,
|
|
421
|
+
loader: {},
|
|
422
|
+
staticPaths: {}
|
|
423
|
+
};
|
|
424
|
+
for (const file of scan.files) {
|
|
425
|
+
let source;
|
|
426
|
+
try {
|
|
427
|
+
source = readFileSync(file, "utf-8");
|
|
428
|
+
} catch {
|
|
429
|
+
hints.incomplete = true;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const extension = extname(file);
|
|
433
|
+
const compiledFormat = extension === ".md" || extension === ".mdx";
|
|
434
|
+
const synthesizable = additionalExtensions.includes(extension);
|
|
435
|
+
const exports = analyzeRouteExports(source);
|
|
436
|
+
const capabilityInitializer = readNamedExportInitializer(source, "CAPABILITIES");
|
|
437
|
+
const capabilityValue = capabilityInitializer ? evaluateLiteral(capabilityInitializer) : void 0;
|
|
438
|
+
const capabilities = Array.isArray(capabilityValue) ? capabilityValue.filter((name) => typeof name === "string") : [];
|
|
439
|
+
const values = {
|
|
440
|
+
head: compiledFormat || synthesizable || exports.head,
|
|
441
|
+
headers: compiledFormat || synthesizable || exports.headers,
|
|
442
|
+
loader: exports.loader,
|
|
443
|
+
staticPaths: synthesizable || exports.getStaticPaths
|
|
444
|
+
};
|
|
445
|
+
for (const key of routeHintKeys(routesDir, file, options)) {
|
|
446
|
+
hints.capabilities[key] = capabilities;
|
|
447
|
+
hints.head[key] = values.head;
|
|
448
|
+
hints.headers[key] = values.headers;
|
|
449
|
+
hints.loader[key] = values.loader;
|
|
450
|
+
hints.staticPaths[key] = values.staticPaths;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return hints;
|
|
454
|
+
}
|
|
455
|
+
function createRouteLoaderHints(routesDir, options = {}) {
|
|
456
|
+
return createRouteHints(routesDir, options).loader;
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/pages-router.ts
|
|
460
|
+
const GENERATED_PAGES_MANIFEST_MARKER = "Auto-generated from pages/ directory by @pracht/vite-plugin.";
|
|
461
|
+
const GENERATED_PAGES_LAYOUT_EXPORT = "__PRACHT_EJECTED_PAGES_LAYOUT__";
|
|
462
|
+
const MIDDLEWARE_EXTENSIONS = new Set([
|
|
463
|
+
".ts",
|
|
464
|
+
".tsx",
|
|
465
|
+
".js",
|
|
466
|
+
".jsx"
|
|
467
|
+
]);
|
|
468
|
+
const CAPABILITY_EXTENSIONS = new Set([
|
|
469
|
+
".ts",
|
|
470
|
+
".tsx",
|
|
471
|
+
".js",
|
|
472
|
+
".jsx"
|
|
473
|
+
]);
|
|
474
|
+
/**
|
|
475
|
+
* The root-level `_middleware.{ts,tsx,js,jsx}` file of a pages directory, or
|
|
476
|
+
* null when the app has none. Fails loudly on every shape that would
|
|
477
|
+
* otherwise fail open: a nested `_middleware` file, a `_middleware/`
|
|
478
|
+
* directory, any exact `_middleware` basename using an extension the runtime
|
|
479
|
+
* registry cannot load (all unsupported — they would be silently ignored while
|
|
480
|
+
* looking like an auth gate), and multiple root files competing for the same
|
|
481
|
+
* registration.
|
|
482
|
+
*/
|
|
483
|
+
function findPagesMiddlewareFile(pagesDir, _additionalExtensions = []) {
|
|
484
|
+
const allFiles = scanAllFiles(pagesDir);
|
|
485
|
+
const middlewareDirectories = scanAllDirectories(pagesDir).filter((directory) => basename(directory) === "_middleware");
|
|
486
|
+
if (middlewareDirectories.length > 0) {
|
|
487
|
+
const shown = middlewareDirectories.map((directory) => relative(pagesDir, directory).replace(/\\/g, "/"));
|
|
488
|
+
throw new Error(`[pracht] A \`_middleware\` directory is not supported: ${shown.map((file) => JSON.stringify(file)).join(", ")}. Pages middleware is a single root-level \`_middleware.ts\` file in the pages directory (it runs on every page route). Move the logic there, or eject to an explicit manifest for per-group middleware.`);
|
|
489
|
+
}
|
|
490
|
+
const unsupported = allFiles.filter((file) => basename(file, extname(file)) === "_middleware" && !MIDDLEWARE_EXTENSIONS.has(extname(file)));
|
|
491
|
+
if (unsupported.length > 0) {
|
|
492
|
+
const shown = unsupported.map((file) => relative(pagesDir, file).replace(/\\/g, "/"));
|
|
493
|
+
throw new Error(`[pracht] Pages middleware cannot use the ${shown.map((file) => JSON.stringify(extname(file))).join(", ")} extension (${shown.map((file) => JSON.stringify(file)).join(", ")}). The middleware registry loads \`.ts\`, \`.tsx\`, \`.js\`, and \`.jsx\` modules only — rename the file to \`_middleware.ts\`.`);
|
|
494
|
+
}
|
|
495
|
+
const middlewareFiles = allFiles.filter((file) => basename(file, extname(file)) === "_middleware" && MIDDLEWARE_EXTENSIONS.has(extname(file)));
|
|
496
|
+
const nested = middlewareFiles.filter((file) => relative(pagesDir, file).replace(/\\/g, "/").includes("/"));
|
|
497
|
+
if (nested.length > 0) {
|
|
498
|
+
const shown = nested.map((file) => relative(pagesDir, file).replace(/\\/g, "/"));
|
|
499
|
+
throw new Error(`[pracht] Nested pages middleware is not supported: ${shown.map((file) => JSON.stringify(file)).join(", ")}. Only a root-level \`_middleware.ts\` in the pages directory is applied (it runs on every page route). Move the logic there, or eject to an explicit manifest for per-group middleware.`);
|
|
500
|
+
}
|
|
501
|
+
if (middlewareFiles.length > 1) {
|
|
502
|
+
const shown = middlewareFiles.map((file) => basename(file));
|
|
503
|
+
throw new Error(`[pracht] Multiple pages middleware files resolve to the same registration: ${shown.map((file) => JSON.stringify(file)).join(", ")}. Keep exactly one root-level \`_middleware\` file.`);
|
|
504
|
+
}
|
|
505
|
+
const middlewareFile = middlewareFiles[0] ?? null;
|
|
506
|
+
if (middlewareFile && !exportsMiddleware(readFileSync(middlewareFile, "utf-8"), middlewareFile)) throw new Error(`[pracht] Pages middleware ${JSON.stringify(relative(pagesDir, middlewareFile).replace(/\\/g, "/"))} does not export \`middleware\`. It must declare a named value export such as \`export const middleware: MiddlewareFn = (args, next) => …\` (a default export is not used). The runtime validates that the exported value is callable.`);
|
|
507
|
+
return middlewareFile;
|
|
508
|
+
}
|
|
509
|
+
/** The registered shell name for an `_app` in `directory` (posix, `""` at the root). */
|
|
510
|
+
/**
|
|
511
|
+
* Every `_app` shell in a pages directory, deepest first.
|
|
512
|
+
*
|
|
513
|
+
* An `_app` in a subdirectory owns the routes in that subtree: like a group's
|
|
514
|
+
* `shell` in an explicit manifest, the nearest one wins and REPLACES the
|
|
515
|
+
* parent rather than rendering inside it — `resolveApp()` gives every route
|
|
516
|
+
* exactly one shell, so file-system nesting cannot mean something the manifest
|
|
517
|
+
* router cannot express.
|
|
518
|
+
*
|
|
519
|
+
* Two `_app` files in the same directory are rejected: they compete for one
|
|
520
|
+
* registration, and picking either silently drops the other's `head()` and
|
|
521
|
+
* `headers()` from every route below.
|
|
522
|
+
*/
|
|
523
|
+
function findPagesAppShellFiles(pagesDir, shellExtensions) {
|
|
524
|
+
const shells = scanAllFiles(pagesDir).filter((file) => {
|
|
525
|
+
if (basename(file, extname(file)) !== "_app" || !shellExtensions.has(extname(file))) return false;
|
|
526
|
+
return !relative(pagesDir, file).replace(/\\/g, "/").split("/").slice(0, -1).some((segment) => segment.startsWith("_"));
|
|
527
|
+
}).map((file) => {
|
|
528
|
+
if (readPageStreaming(readFileSync(file, "utf-8")) !== void 0) throw new Error(`[pracht] STREAMING belongs on a pages route, not ${JSON.stringify(file)}.`);
|
|
529
|
+
const directory = relative(pagesDir, file).replace(/\\/g, "/").split("/").slice(0, -1).join("/");
|
|
530
|
+
return {
|
|
531
|
+
absolutePath: file,
|
|
532
|
+
directory,
|
|
533
|
+
name: pagesShellName(directory)
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
const byDirectory = /* @__PURE__ */ new Map();
|
|
537
|
+
for (const shell of shells) byDirectory.set(shell.directory, [...byDirectory.get(shell.directory) ?? [], shell]);
|
|
538
|
+
for (const [directory, candidates] of byDirectory) {
|
|
539
|
+
if (candidates.length < 2) continue;
|
|
540
|
+
const shown = candidates.map((shell) => JSON.stringify(relative(pagesDir, shell.absolutePath).replace(/\\/g, "/"))).join(", ");
|
|
541
|
+
throw new Error(`[pracht] Multiple \`_app\` shells in ${JSON.stringify(directory || ".")} compete for the same registration (${JSON.stringify(pagesShellName(directory))}): ${shown}. Keep exactly one \`_app\` file per directory.`);
|
|
542
|
+
}
|
|
543
|
+
return shells.sort((left, right) => right.directory.length - left.directory.length);
|
|
544
|
+
}
|
|
545
|
+
/** The `_app` that owns a page: the nearest ancestor directory with one. */
|
|
546
|
+
/** Whether a middleware module explicitly exports, or may re-export, `middleware`. */
|
|
547
|
+
function exportsMiddleware(source, file) {
|
|
548
|
+
return hasNamedMiddlewareExport(parseAst(source, { lang: getRolldownLang(file) }));
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* The app-level config file of a pages directory: `_app.config.ts` at the
|
|
552
|
+
* pages root.
|
|
553
|
+
*
|
|
554
|
+
* Named after the shell it configures, because that is what it is — the
|
|
555
|
+
* app-level knobs a manifest passes to `defineApp()` that no single route
|
|
556
|
+
* owns. It is root-only for the same reason `_middleware` is: `agents` and
|
|
557
|
+
* `constraints` are app-wide, so a per-directory copy would look scoped while
|
|
558
|
+
* being ignored.
|
|
559
|
+
*/
|
|
560
|
+
const PAGES_APP_CONFIG_BASENAME = "_app.config";
|
|
561
|
+
/**
|
|
562
|
+
* The root-level `_app.config.{ts,tsx,js,jsx}` of a pages directory, or null
|
|
563
|
+
* when the app has none.
|
|
564
|
+
*
|
|
565
|
+
* Fails closed on every shape that would otherwise leave an app looking
|
|
566
|
+
* configured while nothing is registered: a nested file, an unsupported
|
|
567
|
+
* extension, duplicates, a module that exports none of the supported keys, and
|
|
568
|
+
* a value `export *` whose names cannot be known without loading the module.
|
|
569
|
+
*/
|
|
570
|
+
function findPagesAppConfigFile(pagesDir) {
|
|
571
|
+
const named = scanAllFiles(pagesDir).filter((file) => basename(file, extname(file)) === PAGES_APP_CONFIG_BASENAME);
|
|
572
|
+
if (named.length === 0) return null;
|
|
573
|
+
const show = (file) => JSON.stringify(relative(pagesDir, file).replace(/\\/g, "/"));
|
|
574
|
+
const nested = named.filter((file) => relative(pagesDir, file).replace(/\\/g, "/").includes("/"));
|
|
575
|
+
if (nested.length > 0) throw new Error(`[pracht] Nested \`${PAGES_APP_CONFIG_BASENAME}\` is not supported: ${nested.map(show).join(", ")}. \`agents\` and \`constraints\` are app-wide, so only a root-level \`${PAGES_APP_CONFIG_BASENAME}.ts\` in the pages directory is read.`);
|
|
576
|
+
const unsupported = named.filter((file) => !MIDDLEWARE_EXTENSIONS.has(extname(file)));
|
|
577
|
+
if (unsupported.length > 0) throw new Error(`[pracht] Pages app config cannot use the ${unsupported.map((file) => JSON.stringify(extname(file))).join(", ")} extension (${unsupported.map(show).join(", ")}). Rename the file to \`${PAGES_APP_CONFIG_BASENAME}.ts\`.`);
|
|
578
|
+
if (named.length > 1) throw new Error(`[pracht] Multiple pages app config files resolve to the same registration: ${named.map(show).join(", ")}. Keep exactly one root-level \`${PAGES_APP_CONFIG_BASENAME}\` file.`);
|
|
579
|
+
const file = named[0];
|
|
580
|
+
const program = parseAst(readFileSync(file, "utf-8"), { lang: getRolldownLang(file) });
|
|
581
|
+
if (hasValueStarExport(program)) throw new Error(`[pracht] Pages app config ${show(file)} re-exports \`export * from …\`, whose names cannot be read without loading the module. Re-export the keys explicitly, for example \`export { agents } from "./_config/agents.ts"\`.`);
|
|
582
|
+
const exports = PAGES_APP_CONFIG_EXPORTS.filter((name) => hasNamedValueExport(program, name));
|
|
583
|
+
if (exports.length === 0) throw new Error(`[pracht] Pages app config ${show(file)} exports none of ${PAGES_APP_CONFIG_EXPORTS.map((name) => `\`${name}\``).join(", ")}. It must declare named value exports such as \`export const agents: PrachtAgentsConfig = { … }\` (a default export is not used), or be deleted.`);
|
|
584
|
+
return {
|
|
585
|
+
absolutePath: file,
|
|
586
|
+
exports: [...exports]
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Every capability module in `capabilitiesDir`, keyed by the name it registers
|
|
591
|
+
* under.
|
|
592
|
+
*
|
|
593
|
+
* The pages router has no `capabilities` registry, so the directory *is* the
|
|
594
|
+
* registry: one module per capability, named by `defineCapability({ name })`
|
|
595
|
+
* or by its file stem. That keeps registration explicit — a file has to be in
|
|
596
|
+
* `src/capabilities/` to be reachable — without inventing a second place to
|
|
597
|
+
* repeat the name.
|
|
598
|
+
*/
|
|
599
|
+
function findPagesCapabilityFiles(capabilitiesDir) {
|
|
600
|
+
const files = scanAllFiles(capabilitiesDir).filter((file) => CAPABILITY_EXTENSIONS.has(extname(file))).filter((file) => !file.endsWith(".d.ts")).sort();
|
|
601
|
+
const capabilities = [];
|
|
602
|
+
const byName = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const file of files) {
|
|
604
|
+
const show = JSON.stringify(relative(capabilitiesDir, file).replace(/\\/g, "/"));
|
|
605
|
+
const resolved = resolvePagesCapabilityName(basename(file, extname(file)), readFileSync(file, "utf-8"));
|
|
606
|
+
if (!resolved.ok) throw new Error(`[pracht] Capability module ${show} ${resolved.error}`);
|
|
607
|
+
capabilities.push({
|
|
608
|
+
absolutePath: file,
|
|
609
|
+
name: resolved.name
|
|
610
|
+
});
|
|
611
|
+
byName.set(resolved.name, [...byName.get(resolved.name) ?? [], show]);
|
|
612
|
+
}
|
|
613
|
+
for (const [name, shown] of byName) {
|
|
614
|
+
if (shown.length < 2) continue;
|
|
615
|
+
throw new Error(`[pracht] Multiple capability modules register the name ${JSON.stringify(name)}: ${shown.join(", ")}. Keep one module per capability name.`);
|
|
616
|
+
}
|
|
617
|
+
return capabilities;
|
|
618
|
+
}
|
|
619
|
+
function scanPagesDirectory(pagesDir, additionalExtensions = []) {
|
|
620
|
+
const normalizedExtensions = normalizeAdditionalExtensions(additionalExtensions);
|
|
621
|
+
const pageExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, normalizedExtensions);
|
|
622
|
+
const shellExtensions = withAdditionalExtensions(DEFAULT_SHELL_EXTENSIONS, normalizedExtensions);
|
|
623
|
+
const pages = [];
|
|
624
|
+
scan(pagesDir, pagesDir, pages, pageExtensions, shellExtensions, new Set(normalizedExtensions));
|
|
625
|
+
const appShell = pages.find((page) => page.routePath === "__shell__");
|
|
626
|
+
if (appShell?.hasRevalidateExport) throw new Error(`[pracht] Pages app shell ${JSON.stringify(appShell.relativePath)} exports REVALIDATE, but app shells are not ISG routes. Declare the policy on each ISG page instead.`);
|
|
627
|
+
if ((appShell?.capabilities.length ?? 0) > 0) throw new Error(`[pracht] Pages app shell ${JSON.stringify(appShell.relativePath)} exports CAPABILITIES, but page tools are route-scoped. Declare CAPABILITIES on each page that should expose them.`);
|
|
628
|
+
return sortRoutes(pages);
|
|
629
|
+
}
|
|
630
|
+
function scan(dir, root, pages, pageExtensions, shellExtensions, additionalExtensions) {
|
|
631
|
+
let entries;
|
|
632
|
+
try {
|
|
633
|
+
entries = readdirSync(dir);
|
|
634
|
+
} catch {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
for (const entry of entries) {
|
|
638
|
+
const abs = join(dir, entry);
|
|
639
|
+
if (statSync(abs).isDirectory()) {
|
|
640
|
+
if (entry.startsWith("_")) continue;
|
|
641
|
+
scan(abs, root, pages, pageExtensions, shellExtensions, additionalExtensions);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const ext = extname(entry);
|
|
645
|
+
if (!pageExtensions.has(ext)) continue;
|
|
646
|
+
const name = basename(entry, ext);
|
|
647
|
+
const isRootApp = dir === root && name === "_app";
|
|
648
|
+
if (name === "_app" && (!isRootApp || !shellExtensions.has(ext))) continue;
|
|
649
|
+
if (name.startsWith("_") && !isRootApp) continue;
|
|
650
|
+
const rel = relative(root, abs);
|
|
651
|
+
const routePath = filePathToRoutePath(rel);
|
|
652
|
+
const analysisSource = maskMarkdownFences(readFileSync(abs, "utf-8"), rel);
|
|
653
|
+
const renderMode = extractQuotedPageExport(analysisSource, "RENDER_MODE", rel);
|
|
654
|
+
const hydrationMode = extractQuotedPageExport(analysisSource, "HYDRATION", rel);
|
|
655
|
+
const streaming = readPageStreaming(analysisSource);
|
|
656
|
+
if (streaming === "invalid") throw new Error(`[pracht] Pages module ${JSON.stringify(rel)} must export STREAMING once as a boolean literal.`);
|
|
657
|
+
if (streaming !== void 0 && (isRootApp || routePath === "/404")) throw new Error(`[pracht] STREAMING belongs on a pages route, not ${JSON.stringify(rel)}.`);
|
|
658
|
+
const capabilities = extractPageCapabilities(analysisSource, rel);
|
|
659
|
+
const revalidate = extractRevalidateSeconds(analysisSource, rel);
|
|
660
|
+
const hasLoader = detectLoaderExport(analysisSource);
|
|
661
|
+
const hasHead = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadExport(analysisSource);
|
|
662
|
+
const hasHeaders = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadersExport(analysisSource);
|
|
663
|
+
pages.push({
|
|
664
|
+
absolutePath: abs,
|
|
665
|
+
capabilities,
|
|
666
|
+
relativePath: rel,
|
|
667
|
+
routePath,
|
|
668
|
+
isIndex: name === "index",
|
|
669
|
+
isCatchAll: routePath.split("/").includes("*"),
|
|
670
|
+
isDynamic: routePath.split("/").some((segment) => segment.startsWith(":")),
|
|
671
|
+
renderMode,
|
|
672
|
+
hydrationMode,
|
|
673
|
+
streaming,
|
|
674
|
+
revalidateSeconds: revalidate.seconds,
|
|
675
|
+
hasRevalidateExport: revalidate.present,
|
|
676
|
+
hasLoader,
|
|
677
|
+
hasHead,
|
|
678
|
+
hasHeaders
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function extractPageCapabilities(source, relativePath) {
|
|
683
|
+
const initializer = readNamedExportInitializer(source, "CAPABILITIES");
|
|
684
|
+
if (initializer === null) {
|
|
685
|
+
if (/\bexport\s+const\s+CAPABILITIES\b/.test(maskCommentsAndStrings(source))) throw new Error(`[pracht] Pages route ${JSON.stringify(relativePath)} must export CAPABILITIES as an inline array of capability names (for example, \`export const CAPABILITIES = ["notes.search"]\`).`);
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
const value = evaluateLiteral(initializer);
|
|
689
|
+
if (!Array.isArray(value) || value.some((name) => typeof name !== "string" || name.length === 0)) throw new Error(`[pracht] Pages route ${JSON.stringify(relativePath)} must export CAPABILITIES as an inline array of non-empty capability names.`);
|
|
690
|
+
return [...new Set(value)];
|
|
691
|
+
}
|
|
692
|
+
function filePathToRoutePath(relativePath) {
|
|
693
|
+
const extension = extname(relativePath);
|
|
694
|
+
let route = extension ? relativePath.slice(0, -extension.length) : relativePath;
|
|
695
|
+
route = route.replace(/\\/g, "/");
|
|
696
|
+
if (route === "_app" || route.endsWith("/_app")) return "__shell__";
|
|
697
|
+
if (route === "index") return "/";
|
|
698
|
+
route = route.replace(/\/index$/, "");
|
|
699
|
+
route = route.replace(/\[([^\].]+)\]/g, ":$1");
|
|
700
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*");
|
|
701
|
+
return `/${route}`;
|
|
702
|
+
}
|
|
703
|
+
function sortRoutes(pages) {
|
|
704
|
+
return [...pages].filter((p) => p.routePath !== "__shell__").sort(comparePagesBySpecificity);
|
|
705
|
+
}
|
|
706
|
+
function comparePagesBySpecificity(left, right) {
|
|
707
|
+
const leftSegments = splitRoutePath(left.routePath);
|
|
708
|
+
const rightSegments = splitRoutePath(right.routePath);
|
|
709
|
+
const length = Math.max(leftSegments.length, rightSegments.length);
|
|
710
|
+
for (let index = 0; index < length; index += 1) {
|
|
711
|
+
const leftSegment = leftSegments[index];
|
|
712
|
+
const rightSegment = rightSegments[index];
|
|
713
|
+
if (!leftSegment) return -1;
|
|
714
|
+
if (!rightSegment) return 1;
|
|
715
|
+
const leftScore = getRouteSegmentSpecificity(leftSegment);
|
|
716
|
+
const rightScore = getRouteSegmentSpecificity(rightSegment);
|
|
717
|
+
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
718
|
+
if (leftScore === 3 && leftSegment !== rightSegment) return leftSegment.localeCompare(rightSegment);
|
|
719
|
+
}
|
|
720
|
+
return left.routePath.localeCompare(right.routePath);
|
|
721
|
+
}
|
|
722
|
+
function splitRoutePath(routePath) {
|
|
723
|
+
return routePath.split("/").filter(Boolean);
|
|
724
|
+
}
|
|
725
|
+
function getRouteSegmentSpecificity(segment) {
|
|
726
|
+
if (segment === "*") return 1;
|
|
727
|
+
if (segment.startsWith(":")) return 2;
|
|
728
|
+
return 3;
|
|
729
|
+
}
|
|
730
|
+
function extractQuotedPageExport(source, name, relativePath) {
|
|
731
|
+
const { count, value } = readPageStringExport(source, name);
|
|
732
|
+
if (count > 1) throw new Error(`[pracht] Pages route ${JSON.stringify(relativePath)} exports ${name} more than once.`);
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
735
|
+
function extractRevalidateSeconds(source, relativePath) {
|
|
736
|
+
const result = readPageRevalidation(source);
|
|
737
|
+
if (result.kind === "missing") return { present: false };
|
|
738
|
+
if (result.kind === "time") return {
|
|
739
|
+
present: true,
|
|
740
|
+
seconds: result.seconds
|
|
741
|
+
};
|
|
742
|
+
const prefix = `[pracht] Pages route ${JSON.stringify(relativePath)}`;
|
|
743
|
+
if (result.reason === "duplicate") throw new Error(`${prefix} exports REVALIDATE more than once.`);
|
|
744
|
+
const detail = result.reason === "range" ? "positive integer literal number of seconds within JavaScript's safe integer range." : "positive integer literal number of seconds (for example, `export const REVALIDATE = 60`).";
|
|
745
|
+
throw new Error(`${prefix} must export REVALIDATE as a ${detail}`);
|
|
746
|
+
}
|
|
747
|
+
function generatePagesManifestSource(pages, options) {
|
|
748
|
+
const pagesDir = options.pagesDir;
|
|
749
|
+
const defaultRender = options.pagesDefaultRender ?? "ssr";
|
|
750
|
+
const prefix = options.pagesDirPrefix;
|
|
751
|
+
const useImport = options.useImportSyntax ?? false;
|
|
752
|
+
const appShells = findPagesAppShellFiles(pagesDir, withAdditionalExtensions(DEFAULT_SHELL_EXTENSIONS, normalizeAdditionalExtensions(options.additionalExtensions)));
|
|
753
|
+
const rootAppShell = appShells.find((shell) => shell.directory === "");
|
|
754
|
+
const middlewareFile = findPagesMiddlewareFile(pagesDir, options.additionalExtensions);
|
|
755
|
+
const appConfig = options.target === "client" ? null : findPagesAppConfigFile(pagesDir);
|
|
756
|
+
const capabilitiesDir = options.capabilitiesDir === null ? null : options.capabilitiesDir ?? resolve(pagesDir, "..", "capabilities");
|
|
757
|
+
const capabilities = capabilitiesDir ? findPagesCapabilityFiles(capabilitiesDir) : [];
|
|
758
|
+
const lines = [`import { ${pages.some((page) => page.revalidateSeconds !== void 0) ? "defineApp, group, route, timeRevalidate" : "defineApp, group, route"} } from "@pracht/core/manifest";`];
|
|
759
|
+
const referenceBaseDir = options.referenceBaseDir ?? join(pagesDir, "..");
|
|
760
|
+
const relativeModuleRef = (file) => {
|
|
761
|
+
const path = relative(referenceBaseDir, file).replace(/\\/g, "/");
|
|
762
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
763
|
+
};
|
|
764
|
+
const pageFileRef = (page) => {
|
|
765
|
+
const path = prefix ? `${prefix}/${page.relativePath.replace(/\\/g, "/")}` : relativeModuleRef(page.absolutePath);
|
|
766
|
+
return useImport ? `() => import(${JSON.stringify(path)})` : JSON.stringify(path);
|
|
767
|
+
};
|
|
768
|
+
const capabilityFileRef = (file) => {
|
|
769
|
+
const path = options.capabilitiesDirPrefix && capabilitiesDir ? `${options.capabilitiesDirPrefix}/${relative(capabilitiesDir, file).replace(/\\/g, "/")}` : relativeModuleRef(file);
|
|
770
|
+
return useImport ? `() => import(${JSON.stringify(path)})` : JSON.stringify(path);
|
|
771
|
+
};
|
|
772
|
+
const appConfigImportIndex = lines.push("") - 1;
|
|
773
|
+
const usedAppConfigExports = [];
|
|
774
|
+
lines.push("");
|
|
775
|
+
const routeEntries = [];
|
|
776
|
+
const notFoundPage = pages.find((page) => page.routePath === "/404");
|
|
777
|
+
if (notFoundPage?.hasRevalidateExport) throw new Error(`[pracht] Pages not-found module ${JSON.stringify(notFoundPage.relativePath)} exports REVALIDATE, but not-found responses are never ISG routes.`);
|
|
778
|
+
if ((notFoundPage?.capabilities.length ?? 0) > 0) throw new Error(`[pracht] Pages not-found module ${JSON.stringify(notFoundPage.relativePath)} exports CAPABILITIES, but the not-found page does not participate in route-scoped WebMCP activation.`);
|
|
779
|
+
for (const page of pages) {
|
|
780
|
+
if (page === notFoundPage) continue;
|
|
781
|
+
const render = page.renderMode ?? defaultRender;
|
|
782
|
+
if (render === "isg" && page.revalidateSeconds === void 0) throw new Error(`[pracht] Pages route ${JSON.stringify(page.relativePath)} uses render mode "isg" but does not export a revalidation policy. Add \`export const REVALIDATE = 60\` with a positive integer number of seconds, or use another render mode.`);
|
|
783
|
+
if (render !== "isg" && page.hasRevalidateExport) throw new Error(`[pracht] Pages route ${JSON.stringify(page.relativePath)} exports REVALIDATE but its effective render mode is ${JSON.stringify(render)}. REVALIDATE is only valid with \`RENDER_MODE = "isg"\` (or \`pagesDefaultRender: "isg"\`).`);
|
|
784
|
+
if (page.streaming && (render !== "ssr" || (page.hydrationMode ?? "full") !== "full")) throw new Error(`[pracht] Pages route ${JSON.stringify(page.relativePath)} requires SSR and full hydration for STREAMING.`);
|
|
785
|
+
const fileRef = pageFileRef(page);
|
|
786
|
+
const metaParts = [
|
|
787
|
+
`render: ${JSON.stringify(render)}`,
|
|
788
|
+
`hasLoader: ${page.hasLoader ? "true" : "false"}`,
|
|
789
|
+
`hasHead: ${page.hasHead ? "true" : "false"}`
|
|
790
|
+
];
|
|
791
|
+
if (page.streaming !== void 0) metaParts.push(`streaming: ${page.streaming}`);
|
|
792
|
+
if (page.capabilities.length > 0) metaParts.push(`capabilities: ${JSON.stringify(page.capabilities)}`);
|
|
793
|
+
if (page.hydrationMode) metaParts.push(`hydration: ${JSON.stringify(page.hydrationMode)}`);
|
|
794
|
+
if (page.revalidateSeconds !== void 0) metaParts.push(`revalidate: timeRevalidate(${page.revalidateSeconds})`);
|
|
795
|
+
const owningShell = findOwningPagesShell(appShells, page.relativePath);
|
|
796
|
+
if (owningShell && owningShell !== rootAppShell) metaParts.push(`shell: ${JSON.stringify(owningShell.name)}`);
|
|
797
|
+
routeEntries.push(` route(${JSON.stringify(page.routePath)}, ${fileRef}, { ${metaParts.join(", ")} })`);
|
|
798
|
+
}
|
|
799
|
+
const notFoundEntry = notFoundPage ? buildNotFoundEntry(notFoundPage, {
|
|
800
|
+
fileRef: pageFileRef(notFoundPage),
|
|
801
|
+
withShell: !!rootAppShell
|
|
802
|
+
}) : null;
|
|
803
|
+
const specialFileRef = (file) => {
|
|
804
|
+
const path = prefix ? `${prefix}/${relative(pagesDir, file).replace(/\\/g, "/")}` : relativeModuleRef(file);
|
|
805
|
+
return useImport ? `() => import(${JSON.stringify(path)})` : JSON.stringify(path);
|
|
806
|
+
};
|
|
807
|
+
const groupMetaParts = [];
|
|
808
|
+
if (rootAppShell) groupMetaParts.push("shell: \"pages\"");
|
|
809
|
+
if (middlewareFile) groupMetaParts.push("middleware: [\"pages\"]");
|
|
810
|
+
lines.push("const app = defineApp({");
|
|
811
|
+
for (const name of appConfig?.exports ?? []) {
|
|
812
|
+
usedAppConfigExports.push(name);
|
|
813
|
+
lines.push(` ${name},`);
|
|
814
|
+
}
|
|
815
|
+
if (capabilities.length > 0) {
|
|
816
|
+
lines.push(" capabilities: {");
|
|
817
|
+
for (const capability of capabilities) lines.push(` ${JSON.stringify(capability.name)}: ${capabilityFileRef(capability.absolutePath)},`);
|
|
818
|
+
lines.push(" },");
|
|
819
|
+
}
|
|
820
|
+
if (appShells.length > 0) {
|
|
821
|
+
lines.push(" shells: {");
|
|
822
|
+
for (const shell of [...appShells].reverse()) {
|
|
823
|
+
const key = /^[A-Za-z_$][\w$]*$/.test(shell.name) ? shell.name : JSON.stringify(shell.name);
|
|
824
|
+
lines.push(` ${key}: ${specialFileRef(shell.absolutePath)},`);
|
|
825
|
+
}
|
|
826
|
+
lines.push(" },");
|
|
827
|
+
}
|
|
828
|
+
if (middlewareFile) {
|
|
829
|
+
lines.push(" middleware: {");
|
|
830
|
+
lines.push(` pages: ${specialFileRef(middlewareFile)},`);
|
|
831
|
+
lines.push(" },");
|
|
832
|
+
}
|
|
833
|
+
lines.push(" routes: [");
|
|
834
|
+
if (groupMetaParts.length > 0) {
|
|
835
|
+
lines.push(` group({ ${groupMetaParts.join(", ")} }, [`);
|
|
836
|
+
lines.push(routeEntries.join(",\n"));
|
|
837
|
+
lines.push(" ]),");
|
|
838
|
+
} else lines.push(routeEntries.join(",\n"));
|
|
839
|
+
lines.push(" ],");
|
|
840
|
+
if (notFoundEntry) lines.push(notFoundEntry);
|
|
841
|
+
lines.push("});");
|
|
842
|
+
if (appConfig && usedAppConfigExports.length > 0) {
|
|
843
|
+
const path = prefix ? `${prefix}/${relative(pagesDir, appConfig.absolutePath).replace(/\\/g, "/")}` : relativeModuleRef(appConfig.absolutePath);
|
|
844
|
+
lines[appConfigImportIndex] = `import { ${usedAppConfigExports.join(", ")} } from ${JSON.stringify(path)};`;
|
|
845
|
+
} else lines.splice(appConfigImportIndex, 1);
|
|
846
|
+
lines.push("");
|
|
847
|
+
return lines.join("\n");
|
|
848
|
+
}
|
|
849
|
+
function buildNotFoundEntry(page, options) {
|
|
850
|
+
const configParts = [`component: ${options.fileRef}`];
|
|
851
|
+
if (options.withShell) configParts.push("shell: \"pages\"");
|
|
852
|
+
if (page.hydrationMode) configParts.push(`hydration: ${JSON.stringify(page.hydrationMode)}`);
|
|
853
|
+
return ` notFound: { ${configParts.join(", ")} },`;
|
|
854
|
+
}
|
|
855
|
+
function scanAllFiles(dir) {
|
|
856
|
+
const results = [];
|
|
857
|
+
let entries;
|
|
858
|
+
try {
|
|
859
|
+
entries = readdirSync(dir);
|
|
860
|
+
} catch {
|
|
861
|
+
return results;
|
|
862
|
+
}
|
|
863
|
+
for (const entry of entries) {
|
|
864
|
+
const abs = join(dir, entry);
|
|
865
|
+
if (statSync(abs).isDirectory()) results.push(...scanAllFiles(abs));
|
|
866
|
+
else results.push(abs);
|
|
867
|
+
}
|
|
868
|
+
return results;
|
|
869
|
+
}
|
|
870
|
+
function scanAllDirectories(dir) {
|
|
871
|
+
const results = [];
|
|
872
|
+
let entries;
|
|
873
|
+
try {
|
|
874
|
+
entries = readdirSync(dir);
|
|
875
|
+
} catch {
|
|
876
|
+
return results;
|
|
877
|
+
}
|
|
878
|
+
for (const entry of entries) {
|
|
879
|
+
const abs = join(dir, entry);
|
|
880
|
+
if (!statSync(abs).isDirectory()) continue;
|
|
881
|
+
results.push(abs, ...scanAllDirectories(abs));
|
|
882
|
+
}
|
|
883
|
+
return results;
|
|
884
|
+
}
|
|
885
|
+
function generateRoutesFile(pagesDir, outputPath, options) {
|
|
886
|
+
const resolvedOutputPath = resolve(outputPath);
|
|
887
|
+
const manifestSource = generatePagesManifestSource(scanPagesDirectory(pagesDir, options.additionalExtensions).filter((page) => resolve(page.absolutePath) !== resolvedOutputPath), {
|
|
888
|
+
...options,
|
|
889
|
+
referenceBaseDir: dirname(outputPath),
|
|
890
|
+
useImportSyntax: true
|
|
891
|
+
}).replace("const app = defineApp(", "export const app = defineApp(");
|
|
892
|
+
writeFileSync(outputPath, [
|
|
893
|
+
`// ${GENERATED_PAGES_MANIFEST_MARKER}`,
|
|
894
|
+
"// Keep this exported marker: the client build uses it to preserve pages-router",
|
|
895
|
+
"// server-only boundaries after ejection without guessing from manifest syntax.",
|
|
896
|
+
`export const ${GENERATED_PAGES_LAYOUT_EXPORT} = true;`,
|
|
897
|
+
"// To use it directly: remove `pagesDir` from the pracht config, set `appFile` to this",
|
|
898
|
+
"// file, and point `routesDir`/`shellsDir`/`middlewareDir` at the pages directory (or",
|
|
899
|
+
"// move the referenced files into the conventional directories). The runtime resolves",
|
|
900
|
+
"// manifest refs through those directory registries.",
|
|
901
|
+
"",
|
|
902
|
+
manifestSource
|
|
903
|
+
].join("\n"), "utf-8");
|
|
904
|
+
}
|
|
905
|
+
//#endregion
|
|
906
|
+
export { withAdditionalExtensions as C, isPrefreshCompatibleId as D, isPrachtClientModuleId as E, stripPrachtClientModuleQuery as O, normalizeAdditionalExtensions as S, getRolldownLang as T, createRouteLoaderHints as _, filePathToRoutePath as a, LEGACY_BARE_ROUTE_EXTENSIONS as b, findPagesAppShellFiles as c, generatePagesManifestSource as d, generateRoutesFile as f, createRouteHints as g, sortRoutes as h, PAGES_APP_CONFIG_EXPORTS$1 as i, toPrachtClientPrefreshId as k, findPagesCapabilityFiles as l, scanPagesDirectory as m, GENERATED_PAGES_MANIFEST_MARKER as n, findOwningPagesShell$1 as o, pagesShellName$1 as p, PAGES_APP_CONFIG_BASENAME as r, findPagesAppConfigFile as s, GENERATED_PAGES_LAYOUT_EXPORT as t, findPagesMiddlewareFile as u, DEFAULT_ROUTE_EXTENSIONS as v, PRACHT_CLIENT_MODULE_QUERY as w, extensionGlob as x, DEFAULT_SHELL_EXTENSIONS as y };
|