@pracht/vite-plugin 0.4.1 → 0.4.2
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 +29 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +138 -5
- package/dist/pages-router-DxofUxnH.mjs +230 -0
- package/dist/pages-router.d.mts +1 -0
- package/dist/pages-router.mjs +1 -161
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -27,6 +27,35 @@ export default defineConfig({
|
|
|
27
27
|
- Pre-renders SSG and ISG routes at build time (`prerenderConcurrency` controls parallelism)
|
|
28
28
|
- Provides HMR during development
|
|
29
29
|
|
|
30
|
+
## Optional Preact SSR JSX precompile
|
|
31
|
+
|
|
32
|
+
Pracht can opt into the experimental `@pracht/preact-ssr-precompile` transform for
|
|
33
|
+
SSR and SSG server bundles:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export default defineConfig({
|
|
37
|
+
plugins: [pracht({ precompileSsrJsx: true })],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The transform turns safe native HTML JSX subtrees into `jsxTemplate()` calls that
|
|
42
|
+
`preact-render-to-string` can concatenate directly. Client bundles still use the
|
|
43
|
+
normal Preact JSX transform so hydration receives a normal VNode tree.
|
|
44
|
+
|
|
45
|
+
Pass an options object to tune the transform:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
pracht({
|
|
49
|
+
precompileSsrJsx: {
|
|
50
|
+
skipElements: ["canvas"],
|
|
51
|
+
dynamicProps: ["data-client"],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Keep it opt-in for now: it is best suited to SSR-heavy pages with large static
|
|
57
|
+
DOM subtrees and should be benchmarked against your app before enabling broadly.
|
|
58
|
+
|
|
30
59
|
## TSRX (`.tsrx`) Support
|
|
31
60
|
|
|
32
61
|
`.tsrx` modules — TSRX/Ripple-flavoured Preact components — are supported out
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PreactSsrPrecompileOptions } from "@pracht/preact-ssr-precompile";
|
|
1
2
|
import { Plugin } from "vite";
|
|
2
3
|
import { RenderMode, RenderMode as RenderMode$1 } from "@pracht/core";
|
|
3
4
|
|
|
@@ -64,6 +65,11 @@ interface PrachtPluginOptions {
|
|
|
64
65
|
prerenderConcurrency?: number;
|
|
65
66
|
/** Maximum request body size (bytes) accepted by the dev SSR middleware. Defaults to 1 MiB. */
|
|
66
67
|
maxBodySize?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Opt into precompiling safe Preact JSX DOM subtrees for SSR/SSG server bundles.
|
|
70
|
+
* Client bundles keep the normal Preact JSX transform for hydration.
|
|
71
|
+
*/
|
|
72
|
+
precompileSsrJsx?: boolean | PreactSsrPrecompileOptions;
|
|
67
73
|
}
|
|
68
74
|
//#endregion
|
|
69
75
|
//#region src/plugin-codegen.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { generatePagesManifestSource,
|
|
1
|
+
import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-DxofUxnH.mjs";
|
|
2
|
+
import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
|
|
2
3
|
import preact from "@preact/preset-vite";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
4
5
|
import { parseAst } from "vite";
|
|
5
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import { createNodeServerEntryModule } from "@pracht/adapter-node";
|
|
@@ -964,7 +965,8 @@ const DEFAULTS = {
|
|
|
964
965
|
pagesDir: "",
|
|
965
966
|
pagesDefaultRender: "ssr",
|
|
966
967
|
prerenderConcurrency: 10,
|
|
967
|
-
maxBodySize: 1024 * 1024
|
|
968
|
+
maxBodySize: 1024 * 1024,
|
|
969
|
+
precompileSsrJsx: false
|
|
968
970
|
};
|
|
969
971
|
function resolveOptions(options) {
|
|
970
972
|
const resolved = {
|
|
@@ -980,6 +982,7 @@ function resolveOptions(options) {
|
|
|
980
982
|
function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
981
983
|
const resolved = resolveOptions(options);
|
|
982
984
|
const isPagesMode = !!resolved.pagesDir;
|
|
985
|
+
const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
|
|
983
986
|
const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
|
|
984
987
|
const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
|
|
985
988
|
const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
|
|
@@ -990,6 +993,7 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
990
993
|
"import { resolveApp, initClientRouter, readHydrationState } from \"@pracht/core\";",
|
|
991
994
|
appImport,
|
|
992
995
|
"",
|
|
996
|
+
`const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
|
|
993
997
|
`const routeModules = {`,
|
|
994
998
|
` ...import.meta.glob(${JSON.stringify(routeGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
|
|
995
999
|
` ...import.meta.glob(${JSON.stringify(routeTsrxGlob)}),`,
|
|
@@ -1000,7 +1004,9 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1000
1004
|
`};`,
|
|
1001
1005
|
"",
|
|
1002
1006
|
"const resolvedApp = resolveApp(app);",
|
|
1007
|
+
"applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
|
|
1003
1008
|
"",
|
|
1009
|
+
...createApplyRouteLoaderHintsSource(),
|
|
1004
1010
|
"function normalizeModuleKey(key) {",
|
|
1005
1011
|
" return key.split(\"?\")[0].replace(/^\\.?\\//, \"\");",
|
|
1006
1012
|
"}",
|
|
@@ -1047,6 +1053,7 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
|
1047
1053
|
const resolved = resolveOptions(options);
|
|
1048
1054
|
const isPagesMode = !!resolved.pagesDir;
|
|
1049
1055
|
const registrySource = createPrachtRegistryModuleSource(resolved);
|
|
1056
|
+
const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
|
|
1050
1057
|
const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root) : {
|
|
1051
1058
|
clientEntryUrl: null,
|
|
1052
1059
|
cssManifest: {},
|
|
@@ -1057,9 +1064,12 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
|
1057
1064
|
adapter?.serverImports ? adapter.serverImports + "\nimport { prerenderApp } from \"@pracht/core\";" : "import { resolveApp, resolveApiRoutes, prerenderApp } from \"@pracht/core\";",
|
|
1058
1065
|
isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`,
|
|
1059
1066
|
"",
|
|
1067
|
+
`const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
|
|
1068
|
+
...createApplyRouteLoaderHintsSource(),
|
|
1060
1069
|
registrySource,
|
|
1061
1070
|
"",
|
|
1062
1071
|
"export const resolvedApp = resolveApp(app);",
|
|
1072
|
+
"applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
|
|
1063
1073
|
`export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
|
|
1064
1074
|
`export const buildTarget = ${JSON.stringify(adapter?.id ?? "node")};`,
|
|
1065
1075
|
`export const clientEntryUrl = ${JSON.stringify(clientBuild.clientEntryUrl ?? "/@pracht/client.js")};`,
|
|
@@ -1072,6 +1082,37 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
|
1072
1082
|
if (adapter) source.push(adapter.createServerEntryModule());
|
|
1073
1083
|
return source.join("\n");
|
|
1074
1084
|
}
|
|
1085
|
+
function createApplyRouteLoaderHintsSource() {
|
|
1086
|
+
return [
|
|
1087
|
+
"function applyRouteLoaderHints(resolvedApp, routeLoaderHints) {",
|
|
1088
|
+
" for (const route of resolvedApp.routes) {",
|
|
1089
|
+
" const hint = routeLoaderHints[route.file];",
|
|
1090
|
+
" if (hint === true) {",
|
|
1091
|
+
" route.hasLoader = true;",
|
|
1092
|
+
" } else if (typeof route.hasLoader === 'undefined' && typeof hint === 'boolean') {",
|
|
1093
|
+
" route.hasLoader = hint;",
|
|
1094
|
+
" }",
|
|
1095
|
+
" }",
|
|
1096
|
+
"}",
|
|
1097
|
+
""
|
|
1098
|
+
];
|
|
1099
|
+
}
|
|
1100
|
+
function createRouteLoaderHintsForVirtualModules(options, root = process.cwd()) {
|
|
1101
|
+
if (options.pagesDir) {
|
|
1102
|
+
const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)));
|
|
1103
|
+
const hints = {};
|
|
1104
|
+
for (const page of pages) {
|
|
1105
|
+
const key = `${options.pagesDir}/${page.relativePath.replace(/\\/g, "/")}`;
|
|
1106
|
+
hints[key] = !!page.hasLoader;
|
|
1107
|
+
}
|
|
1108
|
+
return hints;
|
|
1109
|
+
}
|
|
1110
|
+
const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
|
|
1111
|
+
return createRouteLoaderHints(resolve(root, options.routesDir.slice(1)), {
|
|
1112
|
+
appFileDir,
|
|
1113
|
+
rootRelativePrefix: options.routesDir
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1075
1116
|
function createPrachtRegistryModuleSource(options = {}) {
|
|
1076
1117
|
const resolved = resolveOptions(options);
|
|
1077
1118
|
const isPagesMode = !!resolved.pagesDir;
|
|
@@ -1130,10 +1171,15 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
1130
1171
|
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
1131
1172
|
return async (req, res, next) => {
|
|
1132
1173
|
const url = req.url ?? "/";
|
|
1133
|
-
const
|
|
1134
|
-
if (pathname.includes(".") || pathname.startsWith("/node_modules/")) return next();
|
|
1174
|
+
const requestUrl = new URL(url, "http://localhost");
|
|
1135
1175
|
try {
|
|
1136
1176
|
const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core"), server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)]);
|
|
1177
|
+
if (shouldBypassDevSSR(requestUrl, req, {
|
|
1178
|
+
app: serverMod.resolvedApp,
|
|
1179
|
+
apiRoutes: serverMod.apiRoutes,
|
|
1180
|
+
matchApiRoute: framework.matchApiRoute,
|
|
1181
|
+
matchAppRoute: framework.matchAppRoute
|
|
1182
|
+
})) return next();
|
|
1137
1183
|
let webRequest;
|
|
1138
1184
|
try {
|
|
1139
1185
|
webRequest = await nodeToWebRequest(req, maxBodySize);
|
|
@@ -1193,6 +1239,84 @@ async function handleDevError(server, req, res, next, url, error) {
|
|
|
1193
1239
|
next(error);
|
|
1194
1240
|
}
|
|
1195
1241
|
}
|
|
1242
|
+
function shouldBypassDevSSR(requestUrl, req, options = {}) {
|
|
1243
|
+
const url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
|
|
1244
|
+
const pathname = url.pathname;
|
|
1245
|
+
if (isReservedDevPath(pathname)) return true;
|
|
1246
|
+
if (isRouteStateRequest(url, req)) return false;
|
|
1247
|
+
if (pathname === "/api" || pathname.startsWith("/api/")) return false;
|
|
1248
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
1249
|
+
if (method !== "GET" && method !== "HEAD") return false;
|
|
1250
|
+
const fetchDest = readRequestHeader(req.headers["sec-fetch-dest"]).toLowerCase();
|
|
1251
|
+
if (matchesResolvedRoute(pathname, options) && !NON_DOCUMENT_FETCH_DESTINATIONS.has(fetchDest)) return false;
|
|
1252
|
+
if (NON_DOCUMENT_FETCH_DESTINATIONS.has(fetchDest)) return true;
|
|
1253
|
+
const accept = readRequestHeader(req.headers.accept).toLowerCase();
|
|
1254
|
+
if (accept.includes("text/html") || accept.includes("application/xhtml+xml")) return false;
|
|
1255
|
+
return hasKnownAssetExtension(pathname);
|
|
1256
|
+
}
|
|
1257
|
+
function matchesResolvedRoute(pathname, options) {
|
|
1258
|
+
if (options.app && options.matchAppRoute && options.matchAppRoute(options.app, pathname)) return true;
|
|
1259
|
+
if (options.apiRoutes?.length && options.matchApiRoute && options.matchApiRoute(options.apiRoutes, pathname)) return true;
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
function isRouteStateRequest(url, req) {
|
|
1263
|
+
return req.headers["x-pracht-route-state-request"] === "1" || url.searchParams.get("_data") === "1";
|
|
1264
|
+
}
|
|
1265
|
+
function readRequestHeader(value) {
|
|
1266
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
1267
|
+
return value ?? "";
|
|
1268
|
+
}
|
|
1269
|
+
function hasKnownAssetExtension(pathname) {
|
|
1270
|
+
const fileName = pathname.split("/").pop() ?? "";
|
|
1271
|
+
const extensionIndex = fileName.lastIndexOf(".");
|
|
1272
|
+
if (extensionIndex <= 0) return false;
|
|
1273
|
+
const extension = fileName.slice(extensionIndex).toLowerCase();
|
|
1274
|
+
return DEV_ASSET_EXTENSIONS.has(extension);
|
|
1275
|
+
}
|
|
1276
|
+
function isReservedDevPath(pathname) {
|
|
1277
|
+
return pathname === "/@pracht/client.js" || pathname === "/@vite/client" || pathname === "/@react-refresh" || pathname.startsWith("/@vite/") || pathname.startsWith("/@id/") || pathname.startsWith("/@fs/") || pathname.startsWith("/__vite_");
|
|
1278
|
+
}
|
|
1279
|
+
const NON_DOCUMENT_FETCH_DESTINATIONS = new Set([
|
|
1280
|
+
"audio",
|
|
1281
|
+
"embed",
|
|
1282
|
+
"font",
|
|
1283
|
+
"image",
|
|
1284
|
+
"manifest",
|
|
1285
|
+
"object",
|
|
1286
|
+
"paintworklet",
|
|
1287
|
+
"report",
|
|
1288
|
+
"script",
|
|
1289
|
+
"serviceworker",
|
|
1290
|
+
"sharedworker",
|
|
1291
|
+
"style",
|
|
1292
|
+
"track",
|
|
1293
|
+
"video",
|
|
1294
|
+
"worker"
|
|
1295
|
+
]);
|
|
1296
|
+
const DEV_ASSET_EXTENSIONS = new Set([
|
|
1297
|
+
".avif",
|
|
1298
|
+
".bmp",
|
|
1299
|
+
".cjs",
|
|
1300
|
+
".css",
|
|
1301
|
+
".gif",
|
|
1302
|
+
".ico",
|
|
1303
|
+
".jpeg",
|
|
1304
|
+
".jpg",
|
|
1305
|
+
".js",
|
|
1306
|
+
".json",
|
|
1307
|
+
".map",
|
|
1308
|
+
".mjs",
|
|
1309
|
+
".pdf",
|
|
1310
|
+
".png",
|
|
1311
|
+
".svg",
|
|
1312
|
+
".txt",
|
|
1313
|
+
".wasm",
|
|
1314
|
+
".webmanifest",
|
|
1315
|
+
".webp",
|
|
1316
|
+
".woff",
|
|
1317
|
+
".woff2",
|
|
1318
|
+
".xml"
|
|
1319
|
+
]);
|
|
1196
1320
|
async function nodeToWebRequest(req, maxBodySize) {
|
|
1197
1321
|
const protocol = "http";
|
|
1198
1322
|
const host = req.headers.host ?? "localhost";
|
|
@@ -1302,6 +1426,10 @@ function pracht(options = {}) {
|
|
|
1302
1426
|
].some((dir) => relative.startsWith(dir))) {
|
|
1303
1427
|
const serverMod = server.moduleGraph.getModuleById(PRACHT_SERVER_MODULE_ID);
|
|
1304
1428
|
if (serverMod) server.moduleGraph.invalidateModule(serverMod);
|
|
1429
|
+
if (relative.startsWith(resolved.routesDir)) {
|
|
1430
|
+
const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
|
|
1431
|
+
if (clientMod) server.moduleGraph.invalidateModule(clientMod);
|
|
1432
|
+
}
|
|
1305
1433
|
}
|
|
1306
1434
|
}
|
|
1307
1435
|
};
|
|
@@ -1325,7 +1453,12 @@ function pracht(options = {}) {
|
|
|
1325
1453
|
return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved));
|
|
1326
1454
|
}
|
|
1327
1455
|
};
|
|
1456
|
+
const precompilePlugin = resolved.precompileSsrJsx ? preactSsrPrecompile({
|
|
1457
|
+
...resolved.precompileSsrJsx === true ? {} : resolved.precompileSsrJsx,
|
|
1458
|
+
ssrOnly: true
|
|
1459
|
+
}) : null;
|
|
1328
1460
|
const plugins = [
|
|
1461
|
+
...precompilePlugin ? [precompilePlugin] : [],
|
|
1329
1462
|
...preact(),
|
|
1330
1463
|
prachtPlugin,
|
|
1331
1464
|
clientModuleTransformPlugin
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { basename, extname, join, relative } from "node:path";
|
|
2
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
//#region src/route-loader-hints.ts
|
|
4
|
+
const ROUTE_EXTENSIONS = new Set([
|
|
5
|
+
".tsx",
|
|
6
|
+
".ts",
|
|
7
|
+
".jsx",
|
|
8
|
+
".js",
|
|
9
|
+
".md",
|
|
10
|
+
".mdx"
|
|
11
|
+
]);
|
|
12
|
+
const LOADER_DECLARATION_RE = /export\s+(?:async\s+)?(?:function|const|let|var)\s+loader\b/;
|
|
13
|
+
const EXPORT_BLOCK_RE = /export\s*\{([^}]*)\}\s*(?:from\s*["'][^"']+["'])?/g;
|
|
14
|
+
const EXPORT_ALL_RE = /export\s+\*\s+from\s*["'][^"']+["']/;
|
|
15
|
+
function exportSpecifiersIncludeLoader(specifiers) {
|
|
16
|
+
return specifiers.split(",").map((specifier) => specifier.trim()).filter(Boolean).some((specifier) => {
|
|
17
|
+
const match = /^(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier);
|
|
18
|
+
if (!match) return false;
|
|
19
|
+
const [, localName, exportedName] = match;
|
|
20
|
+
return (exportedName ?? localName) === "loader";
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function detectLoaderExport(source) {
|
|
24
|
+
if (LOADER_DECLARATION_RE.test(source)) return true;
|
|
25
|
+
for (const match of source.matchAll(EXPORT_BLOCK_RE)) if (exportSpecifiersIncludeLoader(match[1])) return true;
|
|
26
|
+
return EXPORT_ALL_RE.test(source);
|
|
27
|
+
}
|
|
28
|
+
function scanRouteFiles(dir, files) {
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = readdirSync(dir);
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const abs = join(dir, entry);
|
|
37
|
+
if (statSync(abs).isDirectory()) {
|
|
38
|
+
scanRouteFiles(abs, files);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ROUTE_EXTENSIONS.has(extname(entry))) files.push(abs);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function toPosixPath(path) {
|
|
45
|
+
return path.replace(/\\/g, "/");
|
|
46
|
+
}
|
|
47
|
+
function createRouteLoaderHints(routesDir, options = {}) {
|
|
48
|
+
const files = [];
|
|
49
|
+
const hints = {};
|
|
50
|
+
scanRouteFiles(routesDir, files);
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const hasLoader = detectLoaderExport(readFileSync(file, "utf-8"));
|
|
53
|
+
const relativeToRoutesDir = toPosixPath(relative(routesDir, file));
|
|
54
|
+
const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
|
|
55
|
+
const appFileDir = options.appFileDir;
|
|
56
|
+
const keys = /* @__PURE__ */ new Set();
|
|
57
|
+
if (appFileDir) {
|
|
58
|
+
const relativeToAppFile = toPosixPath(relative(appFileDir, file));
|
|
59
|
+
keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
|
|
60
|
+
}
|
|
61
|
+
if (routeRootPrefix) keys.add(`${routeRootPrefix}/${relativeToRoutesDir}`);
|
|
62
|
+
for (const key of keys) hints[key] = hasLoader;
|
|
63
|
+
}
|
|
64
|
+
return hints;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/pages-router.ts
|
|
68
|
+
const PAGE_EXTENSIONS = new Set([
|
|
69
|
+
".tsx",
|
|
70
|
+
".tsrx",
|
|
71
|
+
".ts",
|
|
72
|
+
".jsx",
|
|
73
|
+
".js",
|
|
74
|
+
".md",
|
|
75
|
+
".mdx"
|
|
76
|
+
]);
|
|
77
|
+
const SHELL_EXTENSIONS = new Set([
|
|
78
|
+
".tsx",
|
|
79
|
+
".tsrx",
|
|
80
|
+
".ts",
|
|
81
|
+
".jsx",
|
|
82
|
+
".js"
|
|
83
|
+
]);
|
|
84
|
+
function scanPagesDirectory(pagesDir) {
|
|
85
|
+
const pages = [];
|
|
86
|
+
scan(pagesDir, pagesDir, pages);
|
|
87
|
+
return sortRoutes(pages);
|
|
88
|
+
}
|
|
89
|
+
function scan(dir, root, pages) {
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = readdirSync(dir);
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const abs = join(dir, entry);
|
|
98
|
+
if (statSync(abs).isDirectory()) {
|
|
99
|
+
scan(abs, root, pages);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const ext = extname(entry);
|
|
103
|
+
if (!PAGE_EXTENSIONS.has(ext)) continue;
|
|
104
|
+
const name = basename(entry, ext);
|
|
105
|
+
if (name.startsWith("_") && name !== "_app") continue;
|
|
106
|
+
const rel = relative(root, abs);
|
|
107
|
+
const routePath = filePathToRoutePath(rel);
|
|
108
|
+
const source = readFileSync(abs, "utf-8");
|
|
109
|
+
const renderMode = extractRenderMode(source);
|
|
110
|
+
const hasLoader = detectLoaderExport(source);
|
|
111
|
+
pages.push({
|
|
112
|
+
absolutePath: abs,
|
|
113
|
+
relativePath: rel,
|
|
114
|
+
routePath,
|
|
115
|
+
isIndex: name === "index",
|
|
116
|
+
isCatchAll: routePath.split("/").includes("*"),
|
|
117
|
+
isDynamic: routePath.split("/").some((segment) => segment.startsWith(":")),
|
|
118
|
+
renderMode,
|
|
119
|
+
hasLoader
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function filePathToRoutePath(relativePath) {
|
|
124
|
+
let route = relativePath.replace(/\.(tsx?|tsrx|jsx?|mdx?)$/, "");
|
|
125
|
+
route = route.replace(/\\/g, "/");
|
|
126
|
+
if (route === "_app" || route.endsWith("/_app")) return "__shell__";
|
|
127
|
+
if (route === "index") return "/";
|
|
128
|
+
route = route.replace(/\/index$/, "");
|
|
129
|
+
route = route.replace(/\[([^\].]+)\]/g, ":$1");
|
|
130
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*");
|
|
131
|
+
return `/${route}`;
|
|
132
|
+
}
|
|
133
|
+
function sortRoutes(pages) {
|
|
134
|
+
return [...pages].filter((p) => p.routePath !== "__shell__").sort(comparePagesBySpecificity);
|
|
135
|
+
}
|
|
136
|
+
function comparePagesBySpecificity(left, right) {
|
|
137
|
+
const leftSegments = splitRoutePath(left.routePath);
|
|
138
|
+
const rightSegments = splitRoutePath(right.routePath);
|
|
139
|
+
const length = Math.max(leftSegments.length, rightSegments.length);
|
|
140
|
+
for (let index = 0; index < length; index += 1) {
|
|
141
|
+
const leftSegment = leftSegments[index];
|
|
142
|
+
const rightSegment = rightSegments[index];
|
|
143
|
+
if (!leftSegment) return -1;
|
|
144
|
+
if (!rightSegment) return 1;
|
|
145
|
+
const leftScore = getRouteSegmentSpecificity(leftSegment);
|
|
146
|
+
const rightScore = getRouteSegmentSpecificity(rightSegment);
|
|
147
|
+
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
148
|
+
if (leftScore === 3 && leftSegment !== rightSegment) return leftSegment.localeCompare(rightSegment);
|
|
149
|
+
}
|
|
150
|
+
return left.routePath.localeCompare(right.routePath);
|
|
151
|
+
}
|
|
152
|
+
function splitRoutePath(routePath) {
|
|
153
|
+
return routePath.split("/").filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
function getRouteSegmentSpecificity(segment) {
|
|
156
|
+
if (segment === "*") return 1;
|
|
157
|
+
if (segment.startsWith(":")) return 2;
|
|
158
|
+
return 3;
|
|
159
|
+
}
|
|
160
|
+
const RENDER_MODE_RE = /export\s+const\s+RENDER_MODE\s*=\s*["'](\w+)["']/;
|
|
161
|
+
function extractRenderMode(source) {
|
|
162
|
+
const match = RENDER_MODE_RE.exec(source);
|
|
163
|
+
return match ? match[1] : void 0;
|
|
164
|
+
}
|
|
165
|
+
function generatePagesManifestSource(pages, options) {
|
|
166
|
+
const pagesDir = options.pagesDir;
|
|
167
|
+
const defaultRender = options.pagesDefaultRender ?? "ssr";
|
|
168
|
+
const prefix = options.pagesDirPrefix;
|
|
169
|
+
const useImport = options.useImportSyntax ?? false;
|
|
170
|
+
const appFile = scanAllFiles(pagesDir).find((f) => basename(f, extname(f)) === "_app" && SHELL_EXTENSIONS.has(extname(f)));
|
|
171
|
+
const lines = ["import { defineApp, group, route } from \"@pracht/core\";", ""];
|
|
172
|
+
const routeEntries = [];
|
|
173
|
+
for (const page of pages) {
|
|
174
|
+
const render = page.renderMode ?? defaultRender;
|
|
175
|
+
const filePath = prefix ? `${prefix}/${page.relativePath.replace(/\\/g, "/")}` : `./${page.relativePath.replace(/\\/g, "/")}`;
|
|
176
|
+
const fileRef = useImport ? `() => import(${JSON.stringify(filePath)})` : JSON.stringify(filePath);
|
|
177
|
+
const metaParts = [`render: ${JSON.stringify(render)}`, `hasLoader: ${page.hasLoader ? "true" : "false"}`];
|
|
178
|
+
routeEntries.push(` route(${JSON.stringify(page.routePath)}, ${fileRef}, { ${metaParts.join(", ")} })`);
|
|
179
|
+
}
|
|
180
|
+
if (appFile) {
|
|
181
|
+
const appPath = prefix ? `${prefix}/_app.${extname(appFile).slice(1)}` : `./${relative(join(pagesDir, ".."), appFile).replace(/\\/g, "/")}`;
|
|
182
|
+
const shellRef = useImport ? `() => import(${JSON.stringify(appPath)})` : JSON.stringify(appPath);
|
|
183
|
+
lines.push("const app = defineApp({");
|
|
184
|
+
lines.push(" shells: {");
|
|
185
|
+
lines.push(` pages: ${shellRef},`);
|
|
186
|
+
lines.push(" },");
|
|
187
|
+
lines.push(" routes: [");
|
|
188
|
+
lines.push(` group({ shell: "pages" }, [`);
|
|
189
|
+
lines.push(routeEntries.join(",\n"));
|
|
190
|
+
lines.push(" ]),");
|
|
191
|
+
lines.push(" ],");
|
|
192
|
+
lines.push("});");
|
|
193
|
+
} else {
|
|
194
|
+
lines.push("const app = defineApp({");
|
|
195
|
+
lines.push(" routes: [");
|
|
196
|
+
lines.push(routeEntries.join(",\n"));
|
|
197
|
+
lines.push(" ],");
|
|
198
|
+
lines.push("});");
|
|
199
|
+
}
|
|
200
|
+
lines.push("");
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
function scanAllFiles(dir) {
|
|
204
|
+
const results = [];
|
|
205
|
+
let entries;
|
|
206
|
+
try {
|
|
207
|
+
entries = readdirSync(dir);
|
|
208
|
+
} catch {
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const abs = join(dir, entry);
|
|
213
|
+
if (statSync(abs).isDirectory()) results.push(...scanAllFiles(abs));
|
|
214
|
+
else results.push(abs);
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
function generateRoutesFile(pagesDir, outputPath, options) {
|
|
219
|
+
writeFileSync(outputPath, [
|
|
220
|
+
"// Auto-generated from pages/ directory by @pracht/vite-plugin.",
|
|
221
|
+
"// Customize this file and remove `pagesDir` from pracht config to use it directly.",
|
|
222
|
+
"",
|
|
223
|
+
generatePagesManifestSource(scanPagesDirectory(pagesDir), {
|
|
224
|
+
...options,
|
|
225
|
+
useImportSyntax: true
|
|
226
|
+
}).replace("const app = defineApp(", "export const app = defineApp(")
|
|
227
|
+
].join("\n"), "utf-8");
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
export { sortRoutes as a, scanPagesDirectory as i, generatePagesManifestSource as n, createRouteLoaderHints as o, generateRoutesFile as r, filePathToRoutePath as t };
|
package/dist/pages-router.d.mts
CHANGED
package/dist/pages-router.mjs
CHANGED
|
@@ -1,162 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
//#region src/pages-router.ts
|
|
4
|
-
const PAGE_EXTENSIONS = new Set([
|
|
5
|
-
".tsx",
|
|
6
|
-
".tsrx",
|
|
7
|
-
".ts",
|
|
8
|
-
".jsx",
|
|
9
|
-
".js",
|
|
10
|
-
".md",
|
|
11
|
-
".mdx"
|
|
12
|
-
]);
|
|
13
|
-
const SHELL_EXTENSIONS = new Set([
|
|
14
|
-
".tsx",
|
|
15
|
-
".tsrx",
|
|
16
|
-
".ts",
|
|
17
|
-
".jsx",
|
|
18
|
-
".js"
|
|
19
|
-
]);
|
|
20
|
-
function scanPagesDirectory(pagesDir) {
|
|
21
|
-
const pages = [];
|
|
22
|
-
scan(pagesDir, pagesDir, pages);
|
|
23
|
-
return sortRoutes(pages);
|
|
24
|
-
}
|
|
25
|
-
function scan(dir, root, pages) {
|
|
26
|
-
let entries;
|
|
27
|
-
try {
|
|
28
|
-
entries = readdirSync(dir);
|
|
29
|
-
} catch {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
for (const entry of entries) {
|
|
33
|
-
const abs = join(dir, entry);
|
|
34
|
-
if (statSync(abs).isDirectory()) {
|
|
35
|
-
scan(abs, root, pages);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
const ext = extname(entry);
|
|
39
|
-
if (!PAGE_EXTENSIONS.has(ext)) continue;
|
|
40
|
-
const name = basename(entry, ext);
|
|
41
|
-
if (name.startsWith("_") && name !== "_app") continue;
|
|
42
|
-
const rel = relative(root, abs);
|
|
43
|
-
const routePath = filePathToRoutePath(rel);
|
|
44
|
-
const renderMode = extractRenderMode(readFileSync(abs, "utf-8"));
|
|
45
|
-
pages.push({
|
|
46
|
-
absolutePath: abs,
|
|
47
|
-
relativePath: rel,
|
|
48
|
-
routePath,
|
|
49
|
-
isIndex: name === "index",
|
|
50
|
-
isCatchAll: routePath.split("/").includes("*"),
|
|
51
|
-
isDynamic: routePath.split("/").some((segment) => segment.startsWith(":")),
|
|
52
|
-
renderMode
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function filePathToRoutePath(relativePath) {
|
|
57
|
-
let route = relativePath.replace(/\.(tsx?|tsrx|jsx?|mdx?)$/, "");
|
|
58
|
-
route = route.replace(/\\/g, "/");
|
|
59
|
-
if (route === "_app" || route.endsWith("/_app")) return "__shell__";
|
|
60
|
-
if (route === "index") return "/";
|
|
61
|
-
route = route.replace(/\/index$/, "");
|
|
62
|
-
route = route.replace(/\[([^\].]+)\]/g, ":$1");
|
|
63
|
-
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*");
|
|
64
|
-
return `/${route}`;
|
|
65
|
-
}
|
|
66
|
-
function sortRoutes(pages) {
|
|
67
|
-
return [...pages].filter((p) => p.routePath !== "__shell__").sort(comparePagesBySpecificity);
|
|
68
|
-
}
|
|
69
|
-
function comparePagesBySpecificity(left, right) {
|
|
70
|
-
const leftSegments = splitRoutePath(left.routePath);
|
|
71
|
-
const rightSegments = splitRoutePath(right.routePath);
|
|
72
|
-
const length = Math.max(leftSegments.length, rightSegments.length);
|
|
73
|
-
for (let index = 0; index < length; index += 1) {
|
|
74
|
-
const leftSegment = leftSegments[index];
|
|
75
|
-
const rightSegment = rightSegments[index];
|
|
76
|
-
if (!leftSegment) return -1;
|
|
77
|
-
if (!rightSegment) return 1;
|
|
78
|
-
const leftScore = getRouteSegmentSpecificity(leftSegment);
|
|
79
|
-
const rightScore = getRouteSegmentSpecificity(rightSegment);
|
|
80
|
-
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
81
|
-
if (leftScore === 3 && leftSegment !== rightSegment) return leftSegment.localeCompare(rightSegment);
|
|
82
|
-
}
|
|
83
|
-
return left.routePath.localeCompare(right.routePath);
|
|
84
|
-
}
|
|
85
|
-
function splitRoutePath(routePath) {
|
|
86
|
-
return routePath.split("/").filter(Boolean);
|
|
87
|
-
}
|
|
88
|
-
function getRouteSegmentSpecificity(segment) {
|
|
89
|
-
if (segment === "*") return 1;
|
|
90
|
-
if (segment.startsWith(":")) return 2;
|
|
91
|
-
return 3;
|
|
92
|
-
}
|
|
93
|
-
const RENDER_MODE_RE = /export\s+const\s+RENDER_MODE\s*=\s*["'](\w+)["']/;
|
|
94
|
-
function extractRenderMode(source) {
|
|
95
|
-
const match = RENDER_MODE_RE.exec(source);
|
|
96
|
-
return match ? match[1] : void 0;
|
|
97
|
-
}
|
|
98
|
-
function generatePagesManifestSource(pages, options) {
|
|
99
|
-
const pagesDir = options.pagesDir;
|
|
100
|
-
const defaultRender = options.pagesDefaultRender ?? "ssr";
|
|
101
|
-
const prefix = options.pagesDirPrefix;
|
|
102
|
-
const useImport = options.useImportSyntax ?? false;
|
|
103
|
-
const appFile = scanAllFiles(pagesDir).find((f) => basename(f, extname(f)) === "_app" && SHELL_EXTENSIONS.has(extname(f)));
|
|
104
|
-
const lines = ["import { defineApp, group, route } from \"@pracht/core\";", ""];
|
|
105
|
-
const routeEntries = [];
|
|
106
|
-
for (const page of pages) {
|
|
107
|
-
const render = page.renderMode ?? defaultRender;
|
|
108
|
-
const filePath = prefix ? `${prefix}/${page.relativePath.replace(/\\/g, "/")}` : `./${page.relativePath.replace(/\\/g, "/")}`;
|
|
109
|
-
const fileRef = useImport ? `() => import(${JSON.stringify(filePath)})` : JSON.stringify(filePath);
|
|
110
|
-
routeEntries.push(` route(${JSON.stringify(page.routePath)}, ${fileRef}, { render: ${JSON.stringify(render)} })`);
|
|
111
|
-
}
|
|
112
|
-
if (appFile) {
|
|
113
|
-
const appPath = prefix ? `${prefix}/_app.${extname(appFile).slice(1)}` : `./${relative(join(pagesDir, ".."), appFile).replace(/\\/g, "/")}`;
|
|
114
|
-
const shellRef = useImport ? `() => import(${JSON.stringify(appPath)})` : JSON.stringify(appPath);
|
|
115
|
-
lines.push("const app = defineApp({");
|
|
116
|
-
lines.push(" shells: {");
|
|
117
|
-
lines.push(` pages: ${shellRef},`);
|
|
118
|
-
lines.push(" },");
|
|
119
|
-
lines.push(" routes: [");
|
|
120
|
-
lines.push(` group({ shell: "pages" }, [`);
|
|
121
|
-
lines.push(routeEntries.join(",\n"));
|
|
122
|
-
lines.push(" ]),");
|
|
123
|
-
lines.push(" ],");
|
|
124
|
-
lines.push("});");
|
|
125
|
-
} else {
|
|
126
|
-
lines.push("const app = defineApp({");
|
|
127
|
-
lines.push(" routes: [");
|
|
128
|
-
lines.push(routeEntries.join(",\n"));
|
|
129
|
-
lines.push(" ],");
|
|
130
|
-
lines.push("});");
|
|
131
|
-
}
|
|
132
|
-
lines.push("");
|
|
133
|
-
return lines.join("\n");
|
|
134
|
-
}
|
|
135
|
-
function scanAllFiles(dir) {
|
|
136
|
-
const results = [];
|
|
137
|
-
let entries;
|
|
138
|
-
try {
|
|
139
|
-
entries = readdirSync(dir);
|
|
140
|
-
} catch {
|
|
141
|
-
return results;
|
|
142
|
-
}
|
|
143
|
-
for (const entry of entries) {
|
|
144
|
-
const abs = join(dir, entry);
|
|
145
|
-
if (statSync(abs).isDirectory()) results.push(...scanAllFiles(abs));
|
|
146
|
-
else results.push(abs);
|
|
147
|
-
}
|
|
148
|
-
return results;
|
|
149
|
-
}
|
|
150
|
-
function generateRoutesFile(pagesDir, outputPath, options) {
|
|
151
|
-
writeFileSync(outputPath, [
|
|
152
|
-
"// Auto-generated from pages/ directory by @pracht/vite-plugin.",
|
|
153
|
-
"// Customize this file and remove `pagesDir` from pracht config to use it directly.",
|
|
154
|
-
"",
|
|
155
|
-
generatePagesManifestSource(scanPagesDirectory(pagesDir), {
|
|
156
|
-
...options,
|
|
157
|
-
useImportSyntax: true
|
|
158
|
-
}).replace("const app = defineApp(", "export const app = defineApp(")
|
|
159
|
-
].join("\n"), "utf-8");
|
|
160
|
-
}
|
|
161
|
-
//#endregion
|
|
1
|
+
import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-DxofUxnH.mjs";
|
|
162
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.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@preact/preset-vite": "^2.10.5",
|
|
46
46
|
"@prefresh/vite": "^2.0.0",
|
|
47
|
-
"@pracht/adapter-node": "0.2.
|
|
48
|
-
"@pracht/core": "0.
|
|
47
|
+
"@pracht/adapter-node": "0.2.2",
|
|
48
|
+
"@pracht/core": "0.7.0",
|
|
49
|
+
"@pracht/preact-ssr-precompile": "0.1.1"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
52
|
"vite": "^8.0.0"
|