@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 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, scanPagesDirectory } from "./pages-router.mjs";
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 pathname = new URL(url, "http://localhost").pathname;
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 };
@@ -7,6 +7,7 @@ interface ScannedPage {
7
7
  isCatchAll: boolean;
8
8
  isDynamic: boolean;
9
9
  renderMode?: string;
10
+ hasLoader?: boolean;
10
11
  }
11
12
  interface PagesRouterOptions {
12
13
  pagesDir: string;
@@ -1,162 +1,2 @@
1
- import { basename, extname, join, relative } from "node:path";
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.1",
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.1",
48
- "@pracht/core": "0.6.1"
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"