@pracht/vite-plugin 0.11.1 → 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 +8 -5
- package/virtual.d.ts +2 -2
- package/dist/pages-router-MA9rOl88.mjs +0 -618
|
@@ -1,618 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { basename, extname, join, relative } from "node:path";
|
|
3
|
-
import { maskCommentsAndStrings } from "@pracht/capabilities/static";
|
|
4
|
-
import { parse } from "@babel/parser";
|
|
5
|
-
import { initSync, parse as parse$1 } from "es-module-lexer";
|
|
6
|
-
//#region src/route-extensions.ts
|
|
7
|
-
const BUILT_IN_ROUTE_EXTENSIONS = [
|
|
8
|
-
".ts",
|
|
9
|
-
".tsx",
|
|
10
|
-
".js",
|
|
11
|
-
".jsx",
|
|
12
|
-
".md",
|
|
13
|
-
".mdx"
|
|
14
|
-
];
|
|
15
|
-
const LEGACY_BARE_ROUTE_EXTENSIONS = [".tsrx"];
|
|
16
|
-
const DEFAULT_ROUTE_EXTENSIONS = [...BUILT_IN_ROUTE_EXTENSIONS, ...LEGACY_BARE_ROUTE_EXTENSIONS];
|
|
17
|
-
const DEFAULT_SHELL_EXTENSIONS = [
|
|
18
|
-
".ts",
|
|
19
|
-
".tsx",
|
|
20
|
-
".js",
|
|
21
|
-
".jsx",
|
|
22
|
-
...LEGACY_BARE_ROUTE_EXTENSIONS
|
|
23
|
-
];
|
|
24
|
-
const EXTENSION_RE = /^\.[a-z0-9][a-z0-9_-]*$/i;
|
|
25
|
-
function normalizeAdditionalExtensions(extensions) {
|
|
26
|
-
if (extensions === void 0) return [];
|
|
27
|
-
if (!Array.isArray(extensions)) throw new Error("pracht({ additionalExtensions }) expects an array of dot-prefixed extensions.");
|
|
28
|
-
const normalized = extensions.map((extension) => {
|
|
29
|
-
if (typeof extension !== "string" || !EXTENSION_RE.test(extension)) throw new Error(`pracht({ additionalExtensions }) expects dot-prefixed extensions such as ".vue", got ${JSON.stringify(extension)}.`);
|
|
30
|
-
return extension.toLowerCase();
|
|
31
|
-
});
|
|
32
|
-
const defaults = new Set(BUILT_IN_ROUTE_EXTENSIONS);
|
|
33
|
-
return [...new Set(normalized)].filter((extension) => !defaults.has(extension));
|
|
34
|
-
}
|
|
35
|
-
function extensionGlob(extensions) {
|
|
36
|
-
const names = extensions.map((extension) => extension.slice(1));
|
|
37
|
-
return names.length === 1 ? names[0] : `{${names.join(",")}}`;
|
|
38
|
-
}
|
|
39
|
-
function withAdditionalExtensions(defaults, additionalExtensions) {
|
|
40
|
-
return new Set([...defaults, ...additionalExtensions]);
|
|
41
|
-
}
|
|
42
|
-
//#endregion
|
|
43
|
-
//#region src/route-loader-hints.ts
|
|
44
|
-
initSync();
|
|
45
|
-
function namedDeclarationRe(exportName) {
|
|
46
|
-
return new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+${exportName}\\b`);
|
|
47
|
-
}
|
|
48
|
-
const HEAD_DECLARATION_RE = namedDeclarationRe("head");
|
|
49
|
-
const HEADERS_DECLARATION_RE = namedDeclarationRe("headers");
|
|
50
|
-
const STATIC_PATHS_DECLARATION_RE = namedDeclarationRe("getStaticPaths");
|
|
51
|
-
const EXPORT_BLOCK_RE = /export\s*\{([^}]*)\}\s*(?:from\s*["'][^"']+["'])?/g;
|
|
52
|
-
const EXPORT_ALL_RE = /export\s+\*\s+from\b/;
|
|
53
|
-
const EXPORT_VARIABLE_DECLARATION_RE = /export\s+(?:const|let|var)\b/g;
|
|
54
|
-
function isExportAllStatement(source) {
|
|
55
|
-
const withoutComments = source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, " ");
|
|
56
|
-
return /^\s*export\s*\*/.test(withoutComments);
|
|
57
|
-
}
|
|
58
|
-
function exportedVariableDeclarationIncludesLoader(source) {
|
|
59
|
-
for (const declaration of source.matchAll(/\bexport\s+(?:const|let|var)\b/g)) {
|
|
60
|
-
let index = (declaration.index ?? 0) + declaration[0].length;
|
|
61
|
-
while (index < source.length) {
|
|
62
|
-
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
63
|
-
const bindingStart = index;
|
|
64
|
-
const opening = source[index];
|
|
65
|
-
if (opening === "{" || opening === "[") {
|
|
66
|
-
const closing = opening === "{" ? "}" : "]";
|
|
67
|
-
let depth = 0;
|
|
68
|
-
do {
|
|
69
|
-
const char = source[index++];
|
|
70
|
-
if (char === opening) depth += 1;
|
|
71
|
-
if (char === closing) depth -= 1;
|
|
72
|
-
} while (index < source.length && depth > 0);
|
|
73
|
-
if (/\bloader\b/.test(source.slice(bindingStart, index))) return true;
|
|
74
|
-
} else {
|
|
75
|
-
const binding = /^[A-Za-z_$][\w$]*/.exec(source.slice(index));
|
|
76
|
-
if (!binding) break;
|
|
77
|
-
if (binding[0] === "loader") return true;
|
|
78
|
-
index += binding[0].length;
|
|
79
|
-
}
|
|
80
|
-
let parentheses = 0;
|
|
81
|
-
let brackets = 0;
|
|
82
|
-
let braces = 0;
|
|
83
|
-
for (; index < source.length; index += 1) {
|
|
84
|
-
const char = source[index];
|
|
85
|
-
if (char === "(") parentheses += 1;
|
|
86
|
-
else if (char === ")") parentheses = Math.max(0, parentheses - 1);
|
|
87
|
-
else if (char === "[") brackets += 1;
|
|
88
|
-
else if (char === "]") brackets = Math.max(0, brackets - 1);
|
|
89
|
-
else if (char === "{") braces += 1;
|
|
90
|
-
else if (char === "}") braces = Math.max(0, braces - 1);
|
|
91
|
-
if (parentheses === 0 && brackets === 0 && braces === 0) {
|
|
92
|
-
if (char === ";") break;
|
|
93
|
-
if (char === ",") {
|
|
94
|
-
index += 1;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (source[index] === ";" || index >= source.length) break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
function detectLoaderExportFallback(source) {
|
|
105
|
-
const masked = maskCommentsAndStrings(source);
|
|
106
|
-
if (/\bexport\s+(?:async\s+)?function\s+loader\b/.test(masked)) return true;
|
|
107
|
-
if (exportedVariableDeclarationIncludesLoader(masked)) return true;
|
|
108
|
-
if (/\bexport\s*\*/.test(masked)) return true;
|
|
109
|
-
for (const match of masked.matchAll(/\bexport\s*\{([^}]*)\}/g)) if (match[1].split(",").map((specifier) => specifier.trim()).filter(Boolean).some((specifier) => {
|
|
110
|
-
const names = /^(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier);
|
|
111
|
-
if (!names || specifier.startsWith("type ")) return false;
|
|
112
|
-
return (names[2] ?? names[1]) === "loader";
|
|
113
|
-
})) return true;
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
function topLevelAssignmentIndex(source) {
|
|
117
|
-
let parentheses = 0;
|
|
118
|
-
let brackets = 0;
|
|
119
|
-
let braces = 0;
|
|
120
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
121
|
-
const char = source[index];
|
|
122
|
-
if (char === "(") parentheses += 1;
|
|
123
|
-
else if (char === ")") parentheses = Math.max(0, parentheses - 1);
|
|
124
|
-
else if (char === "[") brackets += 1;
|
|
125
|
-
else if (char === "]") brackets = Math.max(0, brackets - 1);
|
|
126
|
-
else if (char === "{") braces += 1;
|
|
127
|
-
else if (char === "}") braces = Math.max(0, braces - 1);
|
|
128
|
-
else if (char === "=" && parentheses === 0 && brackets === 0 && braces === 0) return index;
|
|
129
|
-
}
|
|
130
|
-
return -1;
|
|
131
|
-
}
|
|
132
|
-
function bindingExportsName(source, exportName) {
|
|
133
|
-
const assignmentIndex = topLevelAssignmentIndex(source);
|
|
134
|
-
const binding = assignmentIndex === -1 ? source : source.slice(0, assignmentIndex);
|
|
135
|
-
return new RegExp(`\\b${exportName}\\b`).test(binding);
|
|
136
|
-
}
|
|
137
|
-
function variableDeclarationExports(source, exportName) {
|
|
138
|
-
for (const match of source.matchAll(EXPORT_VARIABLE_DECLARATION_RE)) {
|
|
139
|
-
let declarationStart = (match.index ?? 0) + match[0].length;
|
|
140
|
-
let parentheses = 0;
|
|
141
|
-
let brackets = 0;
|
|
142
|
-
let braces = 0;
|
|
143
|
-
for (let index = declarationStart; index <= source.length; index += 1) {
|
|
144
|
-
const char = source[index];
|
|
145
|
-
if (char === "(") parentheses += 1;
|
|
146
|
-
else if (char === ")") parentheses = Math.max(0, parentheses - 1);
|
|
147
|
-
else if (char === "[") brackets += 1;
|
|
148
|
-
else if (char === "]") brackets = Math.max(0, brackets - 1);
|
|
149
|
-
else if (char === "{") braces += 1;
|
|
150
|
-
else if (char === "}") braces = Math.max(0, braces - 1);
|
|
151
|
-
if (parentheses === 0 && brackets === 0 && braces === 0 && (char === "," || char === ";" || char === void 0)) {
|
|
152
|
-
if (bindingExportsName(source.slice(declarationStart, index), exportName)) return true;
|
|
153
|
-
if (char !== ",") break;
|
|
154
|
-
declarationStart = index + 1;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
function exportSpecifiersInclude(specifiers, exportName) {
|
|
161
|
-
return specifiers.split(",").map((specifier) => specifier.trim()).filter(Boolean).some((specifier) => {
|
|
162
|
-
const match = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier);
|
|
163
|
-
if (!match) return false;
|
|
164
|
-
const [, localName, exportedName] = match;
|
|
165
|
-
return (exportedName ?? localName) === exportName;
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Whether `source` exports `exportName`, via a declaration, an export block,
|
|
170
|
-
* or an `export *` re-export (which could expose anything, so it counts).
|
|
171
|
-
*
|
|
172
|
-
* Ordinary TS/JS is parsed exactly, including string-literal export names.
|
|
173
|
-
* Custom syntaxes fall back to masked lexical detection so prose or a string
|
|
174
|
-
* literal mentioning the name cannot produce a false positive.
|
|
175
|
-
*/
|
|
176
|
-
function detectNamedExport(source, exportName, declarationRe) {
|
|
177
|
-
const parsedResult = inspectParsedModule(source, exportName);
|
|
178
|
-
if (parsedResult !== void 0) return parsedResult;
|
|
179
|
-
const analysisSource = maskCommentsAndStrings(source);
|
|
180
|
-
if (declarationRe.test(analysisSource) || variableDeclarationExports(analysisSource, exportName)) return true;
|
|
181
|
-
for (const match of analysisSource.matchAll(EXPORT_BLOCK_RE)) if (exportSpecifiersInclude(match[1], exportName)) return true;
|
|
182
|
-
return EXPORT_ALL_RE.test(analysisSource);
|
|
183
|
-
}
|
|
184
|
-
function detectHeadExport(source) {
|
|
185
|
-
return detectNamedExport(source, "head", HEAD_DECLARATION_RE);
|
|
186
|
-
}
|
|
187
|
-
/** Whether the route or shell module exports document response headers. */
|
|
188
|
-
function detectHeadersExport(source) {
|
|
189
|
-
return detectNamedExport(source, "headers", HEADERS_DECLARATION_RE);
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Whether the route module exports `getStaticPaths()`.
|
|
193
|
-
*
|
|
194
|
-
* Only a static export consumes this: it decides whether a dynamic route has
|
|
195
|
-
* any prerendered path at all, and therefore whether the client should ever
|
|
196
|
-
* request a route-state file for it. Unknown answers must stay `true` — the
|
|
197
|
-
* cost of a wrong `true` is the request the client already makes today, while
|
|
198
|
-
* a wrong `false` would drop state the build did write.
|
|
199
|
-
*/
|
|
200
|
-
function detectStaticPathsExport(source) {
|
|
201
|
-
return detectNamedExport(source, "getStaticPaths", STATIC_PATHS_DECLARATION_RE);
|
|
202
|
-
}
|
|
203
|
-
function isSyntaxNode(value) {
|
|
204
|
-
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
205
|
-
}
|
|
206
|
-
function bindingIncludesName(node, exportName) {
|
|
207
|
-
if (!isSyntaxNode(node)) return false;
|
|
208
|
-
if (node.type === "Identifier") return node.name === exportName;
|
|
209
|
-
if (node.type === "AssignmentPattern") return bindingIncludesName(node.left, exportName);
|
|
210
|
-
if (node.type === "RestElement") return bindingIncludesName(node.argument, exportName);
|
|
211
|
-
if (node.type === "ArrayPattern") return Array.isArray(node.elements) && node.elements.some((element) => bindingIncludesName(element, exportName));
|
|
212
|
-
if (node.type === "ObjectPattern") return Array.isArray(node.properties) && node.properties.some((property) => {
|
|
213
|
-
if (!isSyntaxNode(property)) return false;
|
|
214
|
-
return property.type === "RestElement" ? bindingIncludesName(property.argument, exportName) : bindingIncludesName(property.value, exportName);
|
|
215
|
-
});
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
function exportedNameMatches(node, exportName) {
|
|
219
|
-
if (!isSyntaxNode(node)) return false;
|
|
220
|
-
if (node.type === "Identifier") return node.name === exportName;
|
|
221
|
-
if (node.type === "StringLiteral") return node.value === exportName;
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
function inspectParsedModule(source, exportName) {
|
|
225
|
-
for (const plugins of [["typescript", "jsx"], ["typescript"]]) {
|
|
226
|
-
let body;
|
|
227
|
-
try {
|
|
228
|
-
body = parse(source, {
|
|
229
|
-
plugins: [...plugins],
|
|
230
|
-
sourceType: "module"
|
|
231
|
-
}).program.body;
|
|
232
|
-
} catch {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
for (const statement of body) {
|
|
236
|
-
if (statement.type === "ExportAllDeclaration") {
|
|
237
|
-
if (statement.exportKind !== "type") return true;
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
|
|
241
|
-
if (Array.isArray(statement.specifiers) && statement.specifiers.some((specifier) => isSyntaxNode(specifier) && specifier.exportKind !== "type" && exportedNameMatches(specifier.exported, exportName))) return true;
|
|
242
|
-
const declaration = statement.declaration;
|
|
243
|
-
if (!isSyntaxNode(declaration)) continue;
|
|
244
|
-
if (declaration.declare === true || declaration.type.startsWith("TS")) continue;
|
|
245
|
-
if (declaration.type === "VariableDeclaration") {
|
|
246
|
-
if (Array.isArray(declaration.declarations) && declaration.declarations.some((declarator) => isSyntaxNode(declarator) && bindingIncludesName(declarator.id, exportName))) return true;
|
|
247
|
-
} else if (bindingIncludesName(declaration.id, exportName)) return true;
|
|
248
|
-
}
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
function detectLoaderExport(source) {
|
|
253
|
-
const parsedResult = inspectParsedModule(source, "loader");
|
|
254
|
-
if (parsedResult !== void 0) return parsedResult;
|
|
255
|
-
try {
|
|
256
|
-
const [imports, exports] = parse$1(source);
|
|
257
|
-
if (exports.some((entry) => entry.n === "loader")) return true;
|
|
258
|
-
for (const entry of imports) if (entry.d === -1 && isExportAllStatement(source.slice(entry.ss, entry.se))) return true;
|
|
259
|
-
} catch {}
|
|
260
|
-
return detectLoaderExportFallback(source);
|
|
261
|
-
}
|
|
262
|
-
function scanRouteFiles(dir, files, extensions) {
|
|
263
|
-
let entries;
|
|
264
|
-
try {
|
|
265
|
-
entries = readdirSync(dir);
|
|
266
|
-
} catch {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
for (const entry of entries) {
|
|
270
|
-
const abs = join(dir, entry);
|
|
271
|
-
if (statSync(abs).isDirectory()) {
|
|
272
|
-
scanRouteFiles(abs, files, extensions);
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (extensions.has(extname(entry))) files.push(abs);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function toPosixPath(path) {
|
|
279
|
-
return path.replace(/\\/g, "/");
|
|
280
|
-
}
|
|
281
|
-
function createRouteLoaderHints(routesDir, options = {}) {
|
|
282
|
-
const files = [];
|
|
283
|
-
const hints = {};
|
|
284
|
-
scanRouteFiles(routesDir, files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, normalizeAdditionalExtensions(options.additionalExtensions)));
|
|
285
|
-
for (const file of files) {
|
|
286
|
-
const hasLoader = detectLoaderExport(readFileSync(file, "utf-8"));
|
|
287
|
-
const relativeToRoutesDir = toPosixPath(relative(routesDir, file));
|
|
288
|
-
const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
|
|
289
|
-
const appFileDir = options.appFileDir;
|
|
290
|
-
const keys = /* @__PURE__ */ new Set();
|
|
291
|
-
if (appFileDir) {
|
|
292
|
-
const relativeToAppFile = toPosixPath(relative(appFileDir, file));
|
|
293
|
-
keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
|
|
294
|
-
}
|
|
295
|
-
if (routeRootPrefix) keys.add(`${routeRootPrefix}/${relativeToRoutesDir}`);
|
|
296
|
-
for (const key of keys) hints[key] = hasLoader;
|
|
297
|
-
}
|
|
298
|
-
return hints;
|
|
299
|
-
}
|
|
300
|
-
function createRouteHeadHints(routesDir, options = {}) {
|
|
301
|
-
const files = [];
|
|
302
|
-
const hints = {};
|
|
303
|
-
const additionalExtensions = normalizeAdditionalExtensions(options.additionalExtensions);
|
|
304
|
-
scanRouteFiles(routesDir, files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, additionalExtensions));
|
|
305
|
-
for (const file of files) {
|
|
306
|
-
const extension = extname(file);
|
|
307
|
-
const hasHead = extension === ".md" || extension === ".mdx" || additionalExtensions.includes(extension) || detectHeadExport(readFileSync(file, "utf-8"));
|
|
308
|
-
const relativeToRoutesDir = toPosixPath(relative(routesDir, file));
|
|
309
|
-
const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
|
|
310
|
-
const keys = /* @__PURE__ */ new Set();
|
|
311
|
-
if (options.appFileDir) {
|
|
312
|
-
const relativeToAppFile = toPosixPath(relative(options.appFileDir, file));
|
|
313
|
-
keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
|
|
314
|
-
}
|
|
315
|
-
if (routeRootPrefix) keys.add(`${routeRootPrefix}/${relativeToRoutesDir}`);
|
|
316
|
-
for (const key of keys) hints[key] = hasHead;
|
|
317
|
-
}
|
|
318
|
-
return hints;
|
|
319
|
-
}
|
|
320
|
-
function createRouteHeadersHints(routesDir, options = {}) {
|
|
321
|
-
const files = [];
|
|
322
|
-
const hints = {};
|
|
323
|
-
const additionalExtensions = normalizeAdditionalExtensions(options.additionalExtensions);
|
|
324
|
-
scanRouteFiles(routesDir, files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, additionalExtensions));
|
|
325
|
-
for (const file of files) {
|
|
326
|
-
const extension = extname(file);
|
|
327
|
-
const hasHeaders = extension === ".md" || extension === ".mdx" || additionalExtensions.includes(extension) || detectHeadersExport(readFileSync(file, "utf-8"));
|
|
328
|
-
const relativeToRoutesDir = toPosixPath(relative(routesDir, file));
|
|
329
|
-
const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
|
|
330
|
-
const keys = /* @__PURE__ */ new Set();
|
|
331
|
-
if (options.appFileDir) {
|
|
332
|
-
const relativeToAppFile = toPosixPath(relative(options.appFileDir, file));
|
|
333
|
-
keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
|
|
334
|
-
}
|
|
335
|
-
if (routeRootPrefix) keys.add(`${routeRootPrefix}/${relativeToRoutesDir}`);
|
|
336
|
-
for (const key of keys) hints[key] = hasHeaders;
|
|
337
|
-
}
|
|
338
|
-
return hints;
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Per-route-file `getStaticPaths()` presence, keyed the same way as the loader
|
|
342
|
-
* and head hints.
|
|
343
|
-
*
|
|
344
|
-
* Formats compiled by a companion Vite plugin are reported as `true`: raw
|
|
345
|
-
* source scanning cannot prove such a module has no `getStaticPaths`, and the
|
|
346
|
-
* conservative answer keeps today's behavior.
|
|
347
|
-
*/
|
|
348
|
-
function createRouteStaticPathsHints(routesDir, options = {}) {
|
|
349
|
-
const files = [];
|
|
350
|
-
const hints = {};
|
|
351
|
-
const additionalExtensions = normalizeAdditionalExtensions(options.additionalExtensions);
|
|
352
|
-
scanRouteFiles(routesDir, files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, additionalExtensions));
|
|
353
|
-
for (const file of files) {
|
|
354
|
-
const extension = extname(file);
|
|
355
|
-
const hasStaticPaths = additionalExtensions.includes(extension) || detectStaticPathsExport(readFileSync(file, "utf-8"));
|
|
356
|
-
const relativeToRoutesDir = toPosixPath(relative(routesDir, file));
|
|
357
|
-
const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
|
|
358
|
-
const keys = /* @__PURE__ */ new Set();
|
|
359
|
-
if (options.appFileDir) {
|
|
360
|
-
const relativeToAppFile = toPosixPath(relative(options.appFileDir, file));
|
|
361
|
-
keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
|
|
362
|
-
}
|
|
363
|
-
if (routeRootPrefix) keys.add(`${routeRootPrefix}/${relativeToRoutesDir}`);
|
|
364
|
-
for (const key of keys) hints[key] = hasStaticPaths;
|
|
365
|
-
}
|
|
366
|
-
return hints;
|
|
367
|
-
}
|
|
368
|
-
//#endregion
|
|
369
|
-
//#region src/pages-router.ts
|
|
370
|
-
function scanPagesDirectory(pagesDir, additionalExtensions = []) {
|
|
371
|
-
const normalizedExtensions = normalizeAdditionalExtensions(additionalExtensions);
|
|
372
|
-
const pageExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, normalizedExtensions);
|
|
373
|
-
const shellExtensions = withAdditionalExtensions(DEFAULT_SHELL_EXTENSIONS, normalizedExtensions);
|
|
374
|
-
const pages = [];
|
|
375
|
-
scan(pagesDir, pagesDir, pages, pageExtensions, shellExtensions, new Set(normalizedExtensions));
|
|
376
|
-
const appShell = pages.find((page) => page.routePath === "__shell__");
|
|
377
|
-
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.`);
|
|
378
|
-
return sortRoutes(pages);
|
|
379
|
-
}
|
|
380
|
-
function scan(dir, root, pages, pageExtensions, shellExtensions, additionalExtensions) {
|
|
381
|
-
let entries;
|
|
382
|
-
try {
|
|
383
|
-
entries = readdirSync(dir);
|
|
384
|
-
} catch {
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
for (const entry of entries) {
|
|
388
|
-
const abs = join(dir, entry);
|
|
389
|
-
if (statSync(abs).isDirectory()) {
|
|
390
|
-
scan(abs, root, pages, pageExtensions, shellExtensions, additionalExtensions);
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
const ext = extname(entry);
|
|
394
|
-
if (!pageExtensions.has(ext)) continue;
|
|
395
|
-
const name = basename(entry, ext);
|
|
396
|
-
if (name === "_app" && !shellExtensions.has(ext)) continue;
|
|
397
|
-
if (name.startsWith("_") && name !== "_app") continue;
|
|
398
|
-
const rel = relative(root, abs);
|
|
399
|
-
const routePath = filePathToRoutePath(rel);
|
|
400
|
-
const analysisSource = maskMarkdownFences(readFileSync(abs, "utf-8"), rel);
|
|
401
|
-
const renderMode = extractQuotedPageExport(analysisSource, "RENDER_MODE", rel);
|
|
402
|
-
const hydrationMode = extractQuotedPageExport(analysisSource, "HYDRATION", rel);
|
|
403
|
-
const revalidate = extractRevalidateSeconds(analysisSource, rel);
|
|
404
|
-
const hasLoader = detectLoaderExport(analysisSource);
|
|
405
|
-
const hasHead = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadExport(analysisSource);
|
|
406
|
-
const hasHeaders = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadersExport(analysisSource);
|
|
407
|
-
pages.push({
|
|
408
|
-
absolutePath: abs,
|
|
409
|
-
relativePath: rel,
|
|
410
|
-
routePath,
|
|
411
|
-
isIndex: name === "index",
|
|
412
|
-
isCatchAll: routePath.split("/").includes("*"),
|
|
413
|
-
isDynamic: routePath.split("/").some((segment) => segment.startsWith(":")),
|
|
414
|
-
renderMode,
|
|
415
|
-
hydrationMode,
|
|
416
|
-
revalidateSeconds: revalidate.seconds,
|
|
417
|
-
hasRevalidateExport: revalidate.present,
|
|
418
|
-
hasLoader,
|
|
419
|
-
hasHead,
|
|
420
|
-
hasHeaders
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
function filePathToRoutePath(relativePath) {
|
|
425
|
-
const extension = extname(relativePath);
|
|
426
|
-
let route = extension ? relativePath.slice(0, -extension.length) : relativePath;
|
|
427
|
-
route = route.replace(/\\/g, "/");
|
|
428
|
-
if (route === "_app" || route.endsWith("/_app")) return "__shell__";
|
|
429
|
-
if (route === "index") return "/";
|
|
430
|
-
route = route.replace(/\/index$/, "");
|
|
431
|
-
route = route.replace(/\[([^\].]+)\]/g, ":$1");
|
|
432
|
-
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*");
|
|
433
|
-
return `/${route}`;
|
|
434
|
-
}
|
|
435
|
-
function sortRoutes(pages) {
|
|
436
|
-
return [...pages].filter((p) => p.routePath !== "__shell__").sort(comparePagesBySpecificity);
|
|
437
|
-
}
|
|
438
|
-
function comparePagesBySpecificity(left, right) {
|
|
439
|
-
const leftSegments = splitRoutePath(left.routePath);
|
|
440
|
-
const rightSegments = splitRoutePath(right.routePath);
|
|
441
|
-
const length = Math.max(leftSegments.length, rightSegments.length);
|
|
442
|
-
for (let index = 0; index < length; index += 1) {
|
|
443
|
-
const leftSegment = leftSegments[index];
|
|
444
|
-
const rightSegment = rightSegments[index];
|
|
445
|
-
if (!leftSegment) return -1;
|
|
446
|
-
if (!rightSegment) return 1;
|
|
447
|
-
const leftScore = getRouteSegmentSpecificity(leftSegment);
|
|
448
|
-
const rightScore = getRouteSegmentSpecificity(rightSegment);
|
|
449
|
-
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
450
|
-
if (leftScore === 3 && leftSegment !== rightSegment) return leftSegment.localeCompare(rightSegment);
|
|
451
|
-
}
|
|
452
|
-
return left.routePath.localeCompare(right.routePath);
|
|
453
|
-
}
|
|
454
|
-
function splitRoutePath(routePath) {
|
|
455
|
-
return routePath.split("/").filter(Boolean);
|
|
456
|
-
}
|
|
457
|
-
function getRouteSegmentSpecificity(segment) {
|
|
458
|
-
if (segment === "*") return 1;
|
|
459
|
-
if (segment.startsWith(":")) return 2;
|
|
460
|
-
return 3;
|
|
461
|
-
}
|
|
462
|
-
function extractQuotedPageExport(source, name, relativePath) {
|
|
463
|
-
const declarations = [...maskCommentsAndStrings(source).matchAll(new RegExp(`export\\s+const\\s+${name}\\s*=`, "g"))];
|
|
464
|
-
if (declarations.length === 0) return void 0;
|
|
465
|
-
if (declarations.length > 1) throw new Error(`[pracht] Pages route ${JSON.stringify(relativePath)} exports ${name} more than once.`);
|
|
466
|
-
const declaration = declarations[0];
|
|
467
|
-
const valueStart = (declaration.index ?? 0) + declaration[0].length;
|
|
468
|
-
return source.slice(valueStart).trimStart().match(/^["'](\w+)["']/)?.[1];
|
|
469
|
-
}
|
|
470
|
-
const REVALIDATE_RE = /export\s+const\s+REVALIDATE\s*=\s*([^;\n]+)/;
|
|
471
|
-
function extractRevalidateSeconds(source, relativePath) {
|
|
472
|
-
const matches = [...maskCommentsAndStrings(source).matchAll(new RegExp(REVALIDATE_RE, "g"))];
|
|
473
|
-
if (matches.length === 0) return { present: false };
|
|
474
|
-
if (matches.length > 1) throw new Error(`[pracht] Pages route ${JSON.stringify(relativePath)} exports REVALIDATE more than once.`);
|
|
475
|
-
const expression = matches[0][1].trim().replace(/\s+as\s+const$/, "");
|
|
476
|
-
if (!/^\d(?:_?\d)*$/.test(expression)) throw new Error(`[pracht] Pages route ${JSON.stringify(relativePath)} must export REVALIDATE as a positive integer literal number of seconds (for example, \`export const REVALIDATE = 60\`).`);
|
|
477
|
-
const seconds = Number(expression.replaceAll("_", ""));
|
|
478
|
-
if (!Number.isSafeInteger(seconds) || seconds <= 0) throw new Error(`[pracht] Pages route ${JSON.stringify(relativePath)} must export REVALIDATE as a positive integer literal number of seconds within JavaScript's safe integer range.`);
|
|
479
|
-
return {
|
|
480
|
-
present: true,
|
|
481
|
-
seconds
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
/** Mask Markdown fenced examples while preserving source offsets and top-level MDX exports. */
|
|
485
|
-
function maskMarkdownFences(source, relativePath) {
|
|
486
|
-
if (!/\.mdx?$/.test(relativePath)) return source;
|
|
487
|
-
const chars = source.split("");
|
|
488
|
-
let activeFence = null;
|
|
489
|
-
for (const line of source.matchAll(/.*(?:\r?\n|$)/g)) {
|
|
490
|
-
if (line[0] === "") continue;
|
|
491
|
-
const lineStart = line.index ?? 0;
|
|
492
|
-
const stripped = stripMarkdownContainerPrefix(line[0].replace(/\r?\n$/, ""));
|
|
493
|
-
const fenceContent = activeFence && stripped.content.startsWith(" ".repeat(activeFence.continuationIndent)) ? stripped.content.slice(activeFence.continuationIndent) : stripped.content;
|
|
494
|
-
const opening = activeFence ? null : /^ {0,3}(`{3,}|~{3,})/.exec(fenceContent);
|
|
495
|
-
const closing = activeFence ? new RegExp(`^ {0,3}\\${activeFence.character}{${activeFence.length},}[ \\t]*$`).test(fenceContent) : false;
|
|
496
|
-
if (activeFence || opening) for (let offset = 0; offset < line[0].length; offset += 1) {
|
|
497
|
-
const index = lineStart + offset;
|
|
498
|
-
if (chars[index] !== "\n" && chars[index] !== "\r") chars[index] = " ";
|
|
499
|
-
}
|
|
500
|
-
if (closing) activeFence = null;
|
|
501
|
-
else if (opening) activeFence = {
|
|
502
|
-
character: opening[1][0],
|
|
503
|
-
continuationIndent: stripped.continuationIndent,
|
|
504
|
-
length: opening[1].length
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
return chars.join("");
|
|
508
|
-
}
|
|
509
|
-
function stripMarkdownContainerPrefix(line) {
|
|
510
|
-
let content = line;
|
|
511
|
-
let continuationIndent = 0;
|
|
512
|
-
while (true) {
|
|
513
|
-
const quote = /^ {0,3}> ?/.exec(content);
|
|
514
|
-
if (quote) {
|
|
515
|
-
content = content.slice(quote[0].length);
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
const list = /^ {0,3}(?:[-+*]|\d{1,9}[.)])[ \t]+/.exec(content);
|
|
519
|
-
if (!list) return {
|
|
520
|
-
content,
|
|
521
|
-
continuationIndent
|
|
522
|
-
};
|
|
523
|
-
continuationIndent += list[0].length;
|
|
524
|
-
content = content.slice(list[0].length);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function generatePagesManifestSource(pages, options) {
|
|
528
|
-
const pagesDir = options.pagesDir;
|
|
529
|
-
const defaultRender = options.pagesDefaultRender ?? "ssr";
|
|
530
|
-
const prefix = options.pagesDirPrefix;
|
|
531
|
-
const useImport = options.useImportSyntax ?? false;
|
|
532
|
-
const shellExtensions = withAdditionalExtensions(DEFAULT_SHELL_EXTENSIONS, normalizeAdditionalExtensions(options.additionalExtensions));
|
|
533
|
-
const appFile = scanAllFiles(pagesDir).find((f) => basename(f, extname(f)) === "_app" && shellExtensions.has(extname(f)));
|
|
534
|
-
const lines = [`import { ${pages.some((page) => page.revalidateSeconds !== void 0) ? "defineApp, group, route, timeRevalidate" : "defineApp, group, route"} } from "@pracht/core/manifest";`, ""];
|
|
535
|
-
const routeEntries = [];
|
|
536
|
-
const notFoundPage = pages.find((page) => page.routePath === "/404");
|
|
537
|
-
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.`);
|
|
538
|
-
for (const page of pages) {
|
|
539
|
-
if (page === notFoundPage) continue;
|
|
540
|
-
const render = page.renderMode ?? defaultRender;
|
|
541
|
-
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.`);
|
|
542
|
-
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"\`).`);
|
|
543
|
-
const filePath = prefix ? `${prefix}/${page.relativePath.replace(/\\/g, "/")}` : `./${page.relativePath.replace(/\\/g, "/")}`;
|
|
544
|
-
const fileRef = useImport ? `() => import(${JSON.stringify(filePath)})` : JSON.stringify(filePath);
|
|
545
|
-
const metaParts = [
|
|
546
|
-
`render: ${JSON.stringify(render)}`,
|
|
547
|
-
`hasLoader: ${page.hasLoader ? "true" : "false"}`,
|
|
548
|
-
`hasHead: ${page.hasHead ? "true" : "false"}`
|
|
549
|
-
];
|
|
550
|
-
if (page.hydrationMode) metaParts.push(`hydration: ${JSON.stringify(page.hydrationMode)}`);
|
|
551
|
-
if (page.revalidateSeconds !== void 0) metaParts.push(`revalidate: timeRevalidate(${page.revalidateSeconds})`);
|
|
552
|
-
routeEntries.push(` route(${JSON.stringify(page.routePath)}, ${fileRef}, { ${metaParts.join(", ")} })`);
|
|
553
|
-
}
|
|
554
|
-
const notFoundEntry = notFoundPage ? buildNotFoundEntry(notFoundPage, {
|
|
555
|
-
prefix,
|
|
556
|
-
useImport,
|
|
557
|
-
withShell: !!appFile
|
|
558
|
-
}) : null;
|
|
559
|
-
if (appFile) {
|
|
560
|
-
const appPath = prefix ? `${prefix}/_app.${extname(appFile).slice(1)}` : `./${relative(join(pagesDir, ".."), appFile).replace(/\\/g, "/")}`;
|
|
561
|
-
const shellRef = useImport ? `() => import(${JSON.stringify(appPath)})` : JSON.stringify(appPath);
|
|
562
|
-
lines.push("const app = defineApp({");
|
|
563
|
-
lines.push(" shells: {");
|
|
564
|
-
lines.push(` pages: ${shellRef},`);
|
|
565
|
-
lines.push(" },");
|
|
566
|
-
lines.push(" routes: [");
|
|
567
|
-
lines.push(` group({ shell: "pages" }, [`);
|
|
568
|
-
lines.push(routeEntries.join(",\n"));
|
|
569
|
-
lines.push(" ]),");
|
|
570
|
-
lines.push(" ],");
|
|
571
|
-
if (notFoundEntry) lines.push(notFoundEntry);
|
|
572
|
-
lines.push("});");
|
|
573
|
-
} else {
|
|
574
|
-
lines.push("const app = defineApp({");
|
|
575
|
-
lines.push(" routes: [");
|
|
576
|
-
lines.push(routeEntries.join(",\n"));
|
|
577
|
-
lines.push(" ],");
|
|
578
|
-
if (notFoundEntry) lines.push(notFoundEntry);
|
|
579
|
-
lines.push("});");
|
|
580
|
-
}
|
|
581
|
-
lines.push("");
|
|
582
|
-
return lines.join("\n");
|
|
583
|
-
}
|
|
584
|
-
function buildNotFoundEntry(page, options) {
|
|
585
|
-
const filePath = options.prefix ? `${options.prefix}/${page.relativePath.replace(/\\/g, "/")}` : `./${page.relativePath.replace(/\\/g, "/")}`;
|
|
586
|
-
const configParts = [`component: ${options.useImport ? `() => import(${JSON.stringify(filePath)})` : JSON.stringify(filePath)}`];
|
|
587
|
-
if (options.withShell) configParts.push("shell: \"pages\"");
|
|
588
|
-
if (page.hydrationMode) configParts.push(`hydration: ${JSON.stringify(page.hydrationMode)}`);
|
|
589
|
-
return ` notFound: { ${configParts.join(", ")} },`;
|
|
590
|
-
}
|
|
591
|
-
function scanAllFiles(dir) {
|
|
592
|
-
const results = [];
|
|
593
|
-
let entries;
|
|
594
|
-
try {
|
|
595
|
-
entries = readdirSync(dir);
|
|
596
|
-
} catch {
|
|
597
|
-
return results;
|
|
598
|
-
}
|
|
599
|
-
for (const entry of entries) {
|
|
600
|
-
const abs = join(dir, entry);
|
|
601
|
-
if (statSync(abs).isDirectory()) results.push(...scanAllFiles(abs));
|
|
602
|
-
else results.push(abs);
|
|
603
|
-
}
|
|
604
|
-
return results;
|
|
605
|
-
}
|
|
606
|
-
function generateRoutesFile(pagesDir, outputPath, options) {
|
|
607
|
-
writeFileSync(outputPath, [
|
|
608
|
-
"// Auto-generated from pages/ directory by @pracht/vite-plugin.",
|
|
609
|
-
"// Customize this file and remove `pagesDir` from pracht config to use it directly.",
|
|
610
|
-
"",
|
|
611
|
-
generatePagesManifestSource(scanPagesDirectory(pagesDir, options.additionalExtensions), {
|
|
612
|
-
...options,
|
|
613
|
-
useImportSyntax: true
|
|
614
|
-
}).replace("const app = defineApp(", "export const app = defineApp(")
|
|
615
|
-
].join("\n"), "utf-8");
|
|
616
|
-
}
|
|
617
|
-
//#endregion
|
|
618
|
-
export { sortRoutes as a, createRouteLoaderHints as c, LEGACY_BARE_ROUTE_EXTENSIONS as d, extensionGlob as f, scanPagesDirectory as i, createRouteStaticPathsHints as l, withAdditionalExtensions as m, generatePagesManifestSource as n, createRouteHeadHints as o, normalizeAdditionalExtensions as p, generateRoutesFile as r, createRouteHeadersHints as s, filePathToRoutePath as t, DEFAULT_ROUTE_EXTENSIONS as u };
|