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