@pracht/vite-plugin 0.3.1 → 0.4.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 CHANGED
@@ -13,7 +13,7 @@ npm install @pracht/vite-plugin
13
13
  ```ts
14
14
  // vite.config.ts
15
15
  import { defineConfig } from "vite";
16
- import pracht from "@pracht/vite-plugin";
16
+ import { pracht } from "@pracht/vite-plugin";
17
17
 
18
18
  export default defineConfig({
19
19
  plugins: [pracht()],
@@ -24,9 +24,31 @@ export default defineConfig({
24
24
 
25
25
  - Generates virtual modules (`virtual:pracht/client`, `virtual:pracht/server`) from your route manifest
26
26
  - Builds client and SSR bundles via Vite's multi-environment mode
27
- - Pre-renders SSG and ISG routes at build time
27
+ - Pre-renders SSG and ISG routes at build time (`prerenderConcurrency` controls parallelism)
28
28
  - Provides HMR during development
29
29
 
30
+ ## TSRX (`.tsrx`) Support
31
+
32
+ `.tsrx` modules — TSRX/Ripple-flavoured Preact components — are supported out
33
+ of the box. Bring your own
34
+ [`@tsrx/vite-plugin-preact`](https://github.com/Ripple-TS/ripple) and add it to
35
+ your `plugins` array alongside `pracht()`:
36
+
37
+ ```ts
38
+ // vite.config.ts
39
+ import { defineConfig } from "vite";
40
+ import { pracht } from "@pracht/vite-plugin";
41
+ import { tsrxPreact } from "@tsrx/vite-plugin-preact";
42
+
43
+ export default defineConfig({
44
+ plugins: [tsrxPreact(), pracht()],
45
+ });
46
+ ```
47
+
48
+ The pracht plugin globs `.tsrx` files alongside `.tsx` for routes and shells
49
+ (both manifest- and pages-router modes), and its server-only export stripping
50
+ pass treats them the same way — no separate pracht option is required.
51
+
30
52
  ## Peer Dependencies
31
53
 
32
54
  - `vite@^8.0.0`
package/dist/index.d.mts CHANGED
@@ -30,7 +30,7 @@ interface PrachtAdapter {
30
30
  * Additional Vite plugins the adapter needs (e.g. `@cloudflare/vite-plugin`).
31
31
  * Returned plugins are appended to the plugin array returned by `pracht()`.
32
32
  */
33
- vitePlugins?(): Plugin[] | Promise<Plugin[]>;
33
+ vitePlugins?(): Plugin[];
34
34
  /**
35
35
  * If true, the adapter owns dev-server request handling and the vite-plugin
36
36
  * will not install its own SSR middleware. Used when the adapter contributes
@@ -60,6 +60,10 @@ interface PrachtPluginOptions {
60
60
  pagesDir?: string;
61
61
  /** Default render mode for pages when RENDER_MODE is not exported. Defaults to "ssr". */
62
62
  pagesDefaultRender?: RenderMode$1;
63
+ /** Maximum number of SSG/ISG pages rendered concurrently during `pracht build`. */
64
+ prerenderConcurrency?: number;
65
+ /** Maximum request body size (bytes) accepted by the dev SSR middleware. Defaults to 1 MiB. */
66
+ maxBodySize?: number;
63
67
  }
64
68
  //#endregion
65
69
  //#region src/plugin-codegen.d.ts
@@ -73,6 +77,6 @@ declare function createPrachtServerModuleSource(options?: PrachtPluginOptions, b
73
77
  declare function createPrachtRegistryModuleSource(options?: PrachtPluginOptions): string;
74
78
  //#endregion
75
79
  //#region src/index.d.ts
76
- declare function pracht(options?: PrachtPluginOptions): Promise<Plugin[]>;
80
+ declare function pracht(options?: PrachtPluginOptions): Plugin[];
77
81
  //#endregion
78
82
  export { PRACHT_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, type PrachtAdapter, type PrachtPluginOptions, type RenderMode, createPrachtClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, pracht };
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import preact from "@preact/preset-vite";
3
3
  import { resolve } from "node:path";
4
4
  import { parseAst } from "vite";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
+ import { createNodeServerEntryModule } from "@pracht/adapter-node";
6
7
  //#region src/client-module-query.ts
7
8
  const CLIENT_MODULE_QUERY = "pracht-client";
8
9
  const PRACHT_CLIENT_MODULE_QUERY = `?${CLIENT_MODULE_QUERY}`;
@@ -21,6 +22,7 @@ function stripPrachtClientModuleQuery(id) {
21
22
  function getRolldownLang(id) {
22
23
  const path = stripPrachtClientModuleQuery(id).split("?")[0];
23
24
  if (/\.(c|m)?tsx$/i.test(path)) return "tsx";
25
+ if (/\.tsrx$/i.test(path)) return "tsx";
24
26
  if (/\.(c|m)?ts$/i.test(path)) return "ts";
25
27
  if (/\.(c|m)?jsx$/i.test(path)) return "jsx";
26
28
  if (/\.mdx?$/i.test(path)) return "jsx";
@@ -639,7 +641,8 @@ const SERVER_ONLY_EXPORTS = new Set([
639
641
  "loader",
640
642
  "head",
641
643
  "headers",
642
- "getStaticPaths"
644
+ "getStaticPaths",
645
+ "markdown"
643
646
  ]);
644
647
  function stripServerOnlyExportsForClient(code, id = "pracht-client-route.tsx") {
645
648
  const states = createStatementStates(parseAst(code, { lang: getRolldownLang(id) }));
@@ -944,41 +947,7 @@ function createDefaultNodeAdapter() {
944
947
  id: "node",
945
948
  serverImports: "import { resolveApp, resolveApiRoutes } from \"@pracht/core\";",
946
949
  createServerEntryModule() {
947
- return [
948
- "import { existsSync, readFileSync } from \"node:fs\";",
949
- "import { createServer } from \"node:http\";",
950
- "import { dirname, resolve } from \"node:path\";",
951
- "import { fileURLToPath, pathToFileURL } from \"node:url\";",
952
- "import { createNodeRequestHandler } from \"@pracht/adapter-node\";",
953
- "",
954
- "const serverDir = dirname(fileURLToPath(import.meta.url));",
955
- "const staticDir = resolve(serverDir, \"../client\");",
956
- "const isgManifestPath = resolve(serverDir, \"isg-manifest.json\");",
957
- "const isgManifest = existsSync(isgManifestPath)",
958
- " ? JSON.parse(readFileSync(isgManifestPath, \"utf-8\"))",
959
- " : {};",
960
- "",
961
- "export const handler = createNodeRequestHandler({",
962
- " app: resolvedApp,",
963
- " registry,",
964
- " staticDir,",
965
- " isgManifest,",
966
- " apiRoutes,",
967
- " clientEntryUrl: clientEntryUrl ?? undefined,",
968
- " cssManifest,",
969
- " jsManifest,",
970
- "});",
971
- "",
972
- "const entryHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;",
973
- "if (entryHref && import.meta.url === entryHref) {",
974
- " const server = createServer(handler);",
975
- " const port = Number(process.env.PORT ?? 3000);",
976
- " server.listen(port, () => {",
977
- " console.log(`pracht node server listening on http://localhost:${port}`);",
978
- " });",
979
- "}",
980
- ""
981
- ].join("\n");
950
+ return createNodeServerEntryModule();
982
951
  }
983
952
  };
984
953
  }
@@ -993,13 +962,18 @@ const DEFAULTS = {
993
962
  serverDir: "/src/server",
994
963
  adapter: createDefaultNodeAdapter(),
995
964
  pagesDir: "",
996
- pagesDefaultRender: "ssr"
965
+ pagesDefaultRender: "ssr",
966
+ prerenderConcurrency: 10,
967
+ maxBodySize: 1024 * 1024
997
968
  };
998
969
  function resolveOptions(options) {
999
- return {
970
+ const resolved = {
1000
971
  ...DEFAULTS,
1001
972
  ...options
1002
973
  };
974
+ if (!Number.isInteger(resolved.prerenderConcurrency) || resolved.prerenderConcurrency <= 0) throw new Error("pracht({ prerenderConcurrency }) expects a positive integer.");
975
+ if (!Number.isInteger(resolved.maxBodySize) || resolved.maxBodySize <= 0) throw new Error("pracht({ maxBodySize }) expects a positive integer number of bytes.");
976
+ return resolved;
1003
977
  }
1004
978
  //#endregion
1005
979
  //#region src/plugin-codegen.ts
@@ -1007,29 +981,51 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1007
981
  const resolved = resolveOptions(options);
1008
982
  const isPagesMode = !!resolved.pagesDir;
1009
983
  const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
1010
- const routeGlob = isPagesMode ? `${resolved.pagesDir}/**/*.{ts,tsx,js,jsx,md,mdx}` : `${resolved.routesDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
984
+ const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
985
+ const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
986
+ const routeTsrxGlob = `${dirPrefix}/**/*.tsrx`;
1011
987
  const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
988
+ const shellTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.tsrx` : `${resolved.shellsDir}/**/*.tsrx`;
1012
989
  return [
1013
990
  "import { resolveApp, initClientRouter, readHydrationState } from \"@pracht/core\";",
1014
991
  appImport,
1015
992
  "",
1016
- `const routeModules = import.meta.glob(${JSON.stringify(routeGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} });`,
1017
- `const shellModules = import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} });`,
993
+ `const routeModules = {`,
994
+ ` ...import.meta.glob(${JSON.stringify(routeGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
995
+ ` ...import.meta.glob(${JSON.stringify(routeTsrxGlob)}),`,
996
+ `};`,
997
+ `const shellModules = {`,
998
+ ` ...import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
999
+ ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
1000
+ `};`,
1018
1001
  "",
1019
1002
  "const resolvedApp = resolveApp(app);",
1020
1003
  "",
1021
1004
  "function normalizeModuleKey(key) {",
1022
- " return key.split(\"?\")[0];",
1005
+ " return key.split(\"?\")[0].replace(/^\\.?\\//, \"\");",
1023
1006
  "}",
1024
1007
  "",
1025
- "function findModuleKey(modules, file) {",
1026
- " if (file in modules) return file;",
1027
- " const suffix = file.replace(/^\\.\\//,\"\");",
1008
+ "const moduleKeyIndexes = new WeakMap();",
1009
+ "function getModuleKeyIndex(modules) {",
1010
+ " let index = moduleKeyIndexes.get(modules);",
1011
+ " if (index) return index;",
1012
+ " index = new Map();",
1028
1013
  " for (const key of Object.keys(modules)) {",
1029
- " const normalizedKey = normalizeModuleKey(key);",
1030
- " if (normalizedKey.endsWith(\"/\" + suffix) || normalizedKey.endsWith(suffix)) return key;",
1014
+ " const normalized = normalizeModuleKey(key);",
1015
+ " if (!normalized) continue;",
1016
+ " if (!index.has(normalized)) index.set(normalized, key);",
1017
+ " for (let i = normalized.indexOf(\"/\"); i !== -1; i = normalized.indexOf(\"/\", i + 1)) {",
1018
+ " const suffix = normalized.slice(i + 1);",
1019
+ " if (suffix && !index.has(suffix)) index.set(suffix, key);",
1020
+ " }",
1031
1021
  " }",
1032
- " return null;",
1022
+ " moduleKeyIndexes.set(modules, index);",
1023
+ " return index;",
1024
+ "}",
1025
+ "",
1026
+ "function findModuleKey(modules, file) {",
1027
+ " if (file in modules) return file;",
1028
+ " return getModuleKeyIndex(modules).get(normalizeModuleKey(file)) ?? null;",
1033
1029
  "}",
1034
1030
  "",
1035
1031
  "const state = readHydrationState();",
@@ -1069,6 +1065,7 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
1069
1065
  `export const clientEntryUrl = ${JSON.stringify(clientBuild.clientEntryUrl ?? "/@pracht/client.js")};`,
1070
1066
  `export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
1071
1067
  `export const jsManifest = ${JSON.stringify(clientBuild.jsManifest)};`,
1068
+ `export const prerenderConcurrency = ${JSON.stringify(resolved.prerenderConcurrency)};`,
1072
1069
  "export { prerenderApp };",
1073
1070
  ""
1074
1071
  ];
@@ -1079,10 +1076,18 @@ function createPrachtRegistryModuleSource(options = {}) {
1079
1076
  const resolved = resolveOptions(options);
1080
1077
  const isPagesMode = !!resolved.pagesDir;
1081
1078
  const routeGlob = isPagesMode ? `${resolved.pagesDir}/**/*.{ts,tsx,js,jsx,md,mdx}` : `${resolved.routesDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
1079
+ const routeTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/*.tsrx` : `${resolved.routesDir}/**/*.tsrx`;
1082
1080
  const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
1081
+ const shellTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.tsrx` : `${resolved.shellsDir}/**/*.tsrx`;
1083
1082
  return [
1084
- `export const routeModules = import.meta.glob(${JSON.stringify(routeGlob)});`,
1085
- `export const shellModules = import.meta.glob(${JSON.stringify(shellGlob)});`,
1083
+ `export const routeModules = {`,
1084
+ ` ...import.meta.glob(${JSON.stringify(routeGlob)}),`,
1085
+ ` ...import.meta.glob(${JSON.stringify(routeTsrxGlob)}),`,
1086
+ `};`,
1087
+ `export const shellModules = {`,
1088
+ ` ...import.meta.glob(${JSON.stringify(shellGlob)}),`,
1089
+ ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
1090
+ `};`,
1086
1091
  `export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)});`,
1087
1092
  `export const apiModules = import.meta.glob(${JSON.stringify(`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`)});`,
1088
1093
  `export const dataModules = import.meta.glob(${JSON.stringify(`${resolved.serverDir}/**/*.{ts,js,tsx,jsx}`)});`,
@@ -1096,19 +1101,33 @@ function createPrachtRegistryModuleSource(options = {}) {
1096
1101
  "};"
1097
1102
  ].join("\n");
1098
1103
  }
1104
+ const pagesAppSourceCache = /* @__PURE__ */ new Map();
1105
+ function clearPagesAppSourceCache() {
1106
+ pagesAppSourceCache.clear();
1107
+ }
1099
1108
  function generatePagesAppInlineSource(options, root = process.cwd()) {
1100
1109
  const absPagesDir = resolve(root, options.pagesDir.slice(1));
1101
- return generatePagesManifestSource(scanPagesDirectory(absPagesDir), {
1110
+ const cacheKey = JSON.stringify({
1111
+ absPagesDir,
1112
+ pagesDefaultRender: options.pagesDefaultRender,
1113
+ pagesDirPrefix: options.pagesDir
1114
+ });
1115
+ const cached = pagesAppSourceCache.get(cacheKey);
1116
+ if (cached) return cached;
1117
+ const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir), {
1102
1118
  pagesDir: absPagesDir,
1103
1119
  pagesDefaultRender: options.pagesDefaultRender,
1104
1120
  pagesDirPrefix: options.pagesDir
1105
1121
  });
1122
+ pagesAppSourceCache.set(cacheKey, source);
1123
+ return source;
1106
1124
  }
1107
1125
  //#endregion
1108
1126
  //#region src/plugin-dev-ssr.ts
1109
1127
  const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
1110
- const MAX_BODY_SIZE = 1024 * 1024;
1111
- function createDevSSRMiddleware(server) {
1128
+ const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
1129
+ function createDevSSRMiddleware(server, options = {}) {
1130
+ const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
1112
1131
  return async (req, res, next) => {
1113
1132
  const url = req.url ?? "/";
1114
1133
  const pathname = new URL(url, "http://localhost").pathname;
@@ -1117,7 +1136,7 @@ function createDevSSRMiddleware(server) {
1117
1136
  const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core"), server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)]);
1118
1137
  let webRequest;
1119
1138
  try {
1120
- webRequest = await nodeToWebRequest(req);
1139
+ webRequest = await nodeToWebRequest(req, maxBodySize);
1121
1140
  } catch (err) {
1122
1141
  if (err instanceof Error && err.message === "Request body too large") {
1123
1142
  res.statusCode = 413;
@@ -1161,7 +1180,7 @@ async function handleDevError(server, req, res, next, url, error) {
1161
1180
  return;
1162
1181
  }
1163
1182
  try {
1164
- const { buildErrorOverlayHtml } = await server.ssrLoadModule("pracht/error-overlay");
1183
+ const { buildErrorOverlayHtml } = await server.ssrLoadModule("@pracht/core/error-overlay");
1165
1184
  let html = buildErrorOverlayHtml({
1166
1185
  message: error instanceof Error ? error.message : String(error),
1167
1186
  stack: error instanceof Error ? error.stack : void 0
@@ -1174,7 +1193,7 @@ async function handleDevError(server, req, res, next, url, error) {
1174
1193
  next(error);
1175
1194
  }
1176
1195
  }
1177
- async function nodeToWebRequest(req) {
1196
+ async function nodeToWebRequest(req, maxBodySize) {
1178
1197
  const protocol = "http";
1179
1198
  const host = req.headers.host ?? "localhost";
1180
1199
  const url = new URL(req.url ?? "/", `${protocol}://${host}`);
@@ -1195,10 +1214,7 @@ async function nodeToWebRequest(req) {
1195
1214
  for await (const chunk of req) {
1196
1215
  const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1197
1216
  totalSize += buf.byteLength;
1198
- if (totalSize > MAX_BODY_SIZE) {
1199
- req.destroy();
1200
- throw new Error("Request body too large");
1201
- }
1217
+ if (totalSize > maxBodySize) throw new Error("Request body too large");
1202
1218
  chunks.push(buf);
1203
1219
  }
1204
1220
  const body = Buffer.concat(chunks);
@@ -1208,7 +1224,7 @@ async function nodeToWebRequest(req) {
1208
1224
  }
1209
1225
  //#endregion
1210
1226
  //#region src/index.ts
1211
- async function pracht(options = {}) {
1227
+ function pracht(options = {}) {
1212
1228
  const resolved = resolveOptions(options);
1213
1229
  const isPagesMode = !!resolved.pagesDir;
1214
1230
  let root = process.cwd();
@@ -1248,7 +1264,8 @@ async function pracht(options = {}) {
1248
1264
  return null;
1249
1265
  },
1250
1266
  transform(code, id) {
1251
- if (id !== resolve(root, resolved.appFile.slice(1))) return null;
1267
+ const appFileAbs = resolveConfigPath(root, resolved.appFile);
1268
+ if (toPosixPath(id.split("?")[0]) !== appFileAbs) return null;
1252
1269
  const transformed = code.replace(/\(\)\s*=>\s*import\(\s*(['"])([^'"]+)\1\s*\)/g, "$1$2$1");
1253
1270
  if (transformed === code) return null;
1254
1271
  return {
@@ -1260,13 +1277,15 @@ async function pracht(options = {}) {
1260
1277
  if (isPagesMode) watchPagesDirectory(server, resolved, root);
1261
1278
  if (resolved.adapter.ownsDevServer) return;
1262
1279
  return () => {
1263
- server.middlewares.use(createDevSSRMiddleware(server));
1280
+ server.middlewares.use(createDevSSRMiddleware(server, { maxBodySize: resolved.maxBodySize }));
1264
1281
  };
1265
1282
  },
1266
1283
  handleHotUpdate({ file, server }) {
1267
- const serverRoot = server.config.root;
1268
- const relative = file.startsWith(serverRoot) ? file.slice(serverRoot.length) : file;
1284
+ const serverRoot = toPosixPath(server.config.root);
1285
+ const normalizedFile = toPosixPath(file);
1286
+ const relative = normalizedFile.startsWith(serverRoot) ? normalizedFile.slice(serverRoot.length) : normalizedFile;
1269
1287
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
1288
+ clearPagesAppSourceCache();
1270
1289
  invalidateVirtualModules(server);
1271
1290
  return;
1272
1291
  }
@@ -1299,22 +1318,67 @@ async function pracht(options = {}) {
1299
1318
  };
1300
1319
  }
1301
1320
  };
1321
+ const optimizeDepsEntriesPlugin = {
1322
+ name: "pracht:optimize-deps-entries",
1323
+ enforce: "post",
1324
+ config(config) {
1325
+ return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved));
1326
+ }
1327
+ };
1302
1328
  const plugins = [
1303
1329
  ...preact(),
1304
1330
  prachtPlugin,
1305
1331
  clientModuleTransformPlugin
1306
1332
  ];
1307
- const adapterPlugins = await resolved.adapter.vitePlugins?.();
1333
+ const adapterPlugins = resolved.adapter.vitePlugins?.();
1308
1334
  if (adapterPlugins?.length) plugins.push(...adapterPlugins);
1335
+ plugins.push(optimizeDepsEntriesPlugin);
1309
1336
  return plugins;
1310
1337
  }
1338
+ function withPrachtOptimizeDepsEntries(config, prachtEntries) {
1339
+ const environments = Object.fromEntries(Object.entries(config.environments ?? {}).map(([name, environment]) => [name, { optimizeDeps: { entries: mergeOptimizeDepsEntries(environment.optimizeDeps?.entries, prachtEntries) } }]));
1340
+ return {
1341
+ optimizeDeps: { entries: mergeOptimizeDepsEntries(config.optimizeDeps?.entries, prachtEntries) },
1342
+ ...Object.keys(environments).length > 0 ? { environments } : {}
1343
+ };
1344
+ }
1345
+ function createPrachtOptimizeDepsEntries(resolved) {
1346
+ const scriptExtensions = "{ts,tsx,js,jsx}";
1347
+ const routeExtensions = "{ts,tsx,js,jsx,md,mdx,tsrx}";
1348
+ const entries = resolved.pagesDir ? [
1349
+ `${toOptimizeDepsEntry(resolved.pagesDir)}/**/*.${routeExtensions}`,
1350
+ `${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
1351
+ `${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
1352
+ `${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`
1353
+ ] : [
1354
+ toOptimizeDepsEntry(resolved.appFile),
1355
+ `${toOptimizeDepsEntry(resolved.routesDir)}/**/*.${routeExtensions}`,
1356
+ `${toOptimizeDepsEntry(resolved.shellsDir)}/**/*.${routeExtensions}`,
1357
+ `${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
1358
+ `${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
1359
+ `${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`
1360
+ ];
1361
+ return [...new Set(entries.filter(Boolean))];
1362
+ }
1363
+ function mergeOptimizeDepsEntries(userEntries, prachtEntries) {
1364
+ return [...new Set([...Array.isArray(userEntries) ? userEntries : userEntries ? [userEntries] : [], ...prachtEntries])];
1365
+ }
1366
+ function toOptimizeDepsEntry(path) {
1367
+ return toPosixPath(path).replace(/^\.\//, "").replace(/^\//, "").replace(/\/$/, "");
1368
+ }
1311
1369
  function watchPagesDirectory(server, resolved, root) {
1312
- const abs = resolve(root, resolved.pagesDir.slice(1));
1370
+ const abs = resolveConfigPath(root, resolved.pagesDir);
1313
1371
  server.watcher.on("add", (f) => {
1314
- if (f.startsWith(abs)) server.restart();
1372
+ if (toPosixPath(f).startsWith(toPosixPath(abs))) {
1373
+ clearPagesAppSourceCache();
1374
+ server.restart();
1375
+ }
1315
1376
  });
1316
1377
  server.watcher.on("unlink", (f) => {
1317
- if (f.startsWith(abs)) server.restart();
1378
+ if (toPosixPath(f).startsWith(toPosixPath(abs))) {
1379
+ clearPagesAppSourceCache();
1380
+ server.restart();
1381
+ }
1318
1382
  });
1319
1383
  }
1320
1384
  function invalidateVirtualModules(server) {
@@ -1329,10 +1393,11 @@ const ROUTE_FILE_EXTENSIONS = new Set([
1329
1393
  ".js",
1330
1394
  ".jsx",
1331
1395
  ".md",
1332
- ".mdx"
1396
+ ".mdx",
1397
+ ".tsrx"
1333
1398
  ]);
1334
1399
  function computeRouteFileDirs(root, resolved) {
1335
- return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => toPosixPath(resolve(root, dir.replace(/^\//, "")))).map(withTrailingSep);
1400
+ return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => resolveConfigPath(root, dir)).map(withTrailingSep);
1336
1401
  }
1337
1402
  function isRouteOrShellFile(id, dirs) {
1338
1403
  if (dirs.length === 0) return false;
@@ -1346,6 +1411,12 @@ function isRouteOrShellFile(id, dirs) {
1346
1411
  const normalized = toPosixPath(path);
1347
1412
  return dirs.some((dir) => normalized.startsWith(dir));
1348
1413
  }
1414
+ function resolveConfigPath(root, configPath) {
1415
+ const normalizedRoot = toPosixPath(root).replace(/\/$/, "");
1416
+ const relativePath = configPath.replace(/^\//, "");
1417
+ if (normalizedRoot.startsWith("/") && !/^[A-Za-z]:\//.test(normalizedRoot)) return `${normalizedRoot}/${relativePath}`;
1418
+ return toPosixPath(resolve(root, relativePath));
1419
+ }
1349
1420
  function toPosixPath(p) {
1350
1421
  return p.replace(/\\/g, "/");
1351
1422
  }
@@ -3,6 +3,7 @@ import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  //#region src/pages-router.ts
4
4
  const PAGE_EXTENSIONS = new Set([
5
5
  ".tsx",
6
+ ".tsrx",
6
7
  ".ts",
7
8
  ".jsx",
8
9
  ".js",
@@ -11,6 +12,7 @@ const PAGE_EXTENSIONS = new Set([
11
12
  ]);
12
13
  const SHELL_EXTENSIONS = new Set([
13
14
  ".tsx",
15
+ ".tsrx",
14
16
  ".ts",
15
17
  ".jsx",
16
18
  ".js"
@@ -45,14 +47,14 @@ function scan(dir, root, pages) {
45
47
  relativePath: rel,
46
48
  routePath,
47
49
  isIndex: name === "index",
48
- isCatchAll: name.startsWith("[..."),
49
- isDynamic: name.startsWith("[") && !name.startsWith("[..."),
50
+ isCatchAll: routePath.split("/").includes("*"),
51
+ isDynamic: routePath.split("/").some((segment) => segment.startsWith(":")),
50
52
  renderMode
51
53
  });
52
54
  }
53
55
  }
54
56
  function filePathToRoutePath(relativePath) {
55
- let route = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, "");
57
+ let route = relativePath.replace(/\.(tsx?|tsrx|jsx?|mdx?)$/, "");
56
58
  route = route.replace(/\\/g, "/");
57
59
  if (route === "_app" || route.endsWith("/_app")) return "__shell__";
58
60
  if (route === "index") return "/";
@@ -62,13 +64,31 @@ function filePathToRoutePath(relativePath) {
62
64
  return `/${route}`;
63
65
  }
64
66
  function sortRoutes(pages) {
65
- return [...pages].filter((p) => p.routePath !== "__shell__").sort((a, b) => {
66
- if (a.isCatchAll && !b.isCatchAll) return 1;
67
- if (!a.isCatchAll && b.isCatchAll) return -1;
68
- if (a.isDynamic && !b.isDynamic) return 1;
69
- if (!a.isDynamic && b.isDynamic) return -1;
70
- return a.routePath.localeCompare(b.routePath);
71
- });
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;
72
92
  }
73
93
  const RENDER_MODE_RE = /export\s+const\s+RENDER_MODE\s*=\s*["'](\w+)["']/;
74
94
  function extractRenderMode(source) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/vite-plugin",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/vite-plugin",
6
6
  "bugs": {
@@ -31,8 +31,8 @@
31
31
  "dependencies": {
32
32
  "@preact/preset-vite": "^2.10.5",
33
33
  "@prefresh/vite": "^2.0.0",
34
- "@pracht/adapter-node": "0.1.10",
35
- "@pracht/core": "0.4.0"
34
+ "@pracht/adapter-node": "0.2.0",
35
+ "@pracht/core": "0.6.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "vite": "^8.0.0"