@pracht/vite-plugin 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -65,6 +65,14 @@ interface PrachtPluginOptions {
65
65
  prerenderConcurrency?: number;
66
66
  /** Maximum request body size (bytes) accepted by the dev SSR middleware. Defaults to 1 MiB. */
67
67
  maxBodySize?: number;
68
+ /**
69
+ * Per-route gzip client-JS budgets evaluated by `pracht build`, e.g.
70
+ * `{ "*": "120kb", "/dashboard": "200kb" }`. `"*"` applies to every route;
71
+ * explicit route paths override it. Values are byte counts or size strings
72
+ * ("120kb", "1mb"). Exceeded budgets fail the build unless
73
+ * `pracht build --no-budget-fail` is used.
74
+ */
75
+ budgets?: Record<string, string | number>;
68
76
  /**
69
77
  * Opt into precompiling safe Preact JSX DOM subtrees for SSR/SSG server bundles.
70
78
  * Client bundles keep the normal Preact JSX transform for hydration.
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-BN3V7ii7.mjs";
2
+ import { createRequire } from "node:module";
2
3
  import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
3
4
  import preact from "@preact/preset-vite";
4
- import { dirname, resolve } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
5
6
  import { parseAst } from "vite";
6
7
  import { existsSync, readFileSync } from "node:fs";
7
8
  import { createNodeServerEntryModule } from "@pracht/adapter-node";
@@ -966,6 +967,7 @@ const DEFAULTS = {
966
967
  pagesDefaultRender: "ssr",
967
968
  prerenderConcurrency: 10,
968
969
  maxBodySize: 1024 * 1024,
970
+ budgets: {},
969
971
  precompileSsrJsx: false
970
972
  };
971
973
  function resolveOptions(options) {
@@ -975,8 +977,17 @@ function resolveOptions(options) {
975
977
  };
976
978
  if (!Number.isInteger(resolved.prerenderConcurrency) || resolved.prerenderConcurrency <= 0) throw new Error("pracht({ prerenderConcurrency }) expects a positive integer.");
977
979
  if (!Number.isInteger(resolved.maxBodySize) || resolved.maxBodySize <= 0) throw new Error("pracht({ maxBodySize }) expects a positive integer number of bytes.");
980
+ validateBudgets(resolved.budgets);
978
981
  return resolved;
979
982
  }
983
+ function validateBudgets(budgets) {
984
+ for (const [key, value] of Object.entries(budgets)) {
985
+ if (key !== "*" && !key.startsWith("/")) throw new Error(`pracht({ budgets }) keys must be "*" or a route path starting with "/", got ${JSON.stringify(key)}.`);
986
+ const isValidNumber = typeof value === "number" && Number.isFinite(value) && value > 0;
987
+ const isValidString = typeof value === "string" && value.trim().length > 0;
988
+ if (!isValidNumber && !isValidString) throw new Error(`pracht({ budgets }) values must be a positive number of bytes or a size string like "120kb", got ${JSON.stringify(value)} for ${JSON.stringify(key)}.`);
989
+ }
990
+ }
980
991
  //#endregion
981
992
  //#region src/plugin-codegen.ts
982
993
  function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
@@ -1076,6 +1087,7 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
1076
1087
  `export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
1077
1088
  `export const jsManifest = ${JSON.stringify(clientBuild.jsManifest)};`,
1078
1089
  `export const prerenderConcurrency = ${JSON.stringify(resolved.prerenderConcurrency)};`,
1090
+ `export const budgets = ${JSON.stringify(resolved.budgets)};`,
1079
1091
  "export { prerenderApp };",
1080
1092
  ""
1081
1093
  ];
@@ -1167,19 +1179,36 @@ function generatePagesAppInlineSource(options, root = process.cwd()) {
1167
1179
  //#region src/plugin-dev-ssr.ts
1168
1180
  const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
1169
1181
  const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
1182
+ const DEVTOOLS_JSON_PATH = "/_pracht.json";
1170
1183
  function createDevSSRMiddleware(server, options = {}) {
1171
1184
  const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
1185
+ let warnedDevtoolsCollision = false;
1172
1186
  return async (req, res, next) => {
1173
1187
  const url = req.url ?? "/";
1174
1188
  const requestUrl = new URL(url, "http://localhost");
1175
1189
  try {
1176
1190
  const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core/server"), server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)]);
1177
- if (shouldBypassDevSSR(requestUrl, req, {
1191
+ const routeMatchers = {
1178
1192
  app: serverMod.resolvedApp,
1179
1193
  apiRoutes: serverMod.apiRoutes,
1180
1194
  matchApiRoute: framework.matchApiRoute,
1181
1195
  matchAppRoute: framework.matchAppRoute
1182
- })) return next();
1196
+ };
1197
+ if (requestUrl.pathname === "/_pracht" || requestUrl.pathname === "/_pracht.json") {
1198
+ if (!warnedDevtoolsCollision && matchesResolvedRoute(requestUrl.pathname, routeMatchers)) {
1199
+ warnedDevtoolsCollision = true;
1200
+ server.config.logger.warn(`[pracht] An app route matches ${requestUrl.pathname}, which is reserved for the pracht devtools page in dev. The devtools page wins during development; the app route is only served in production builds.`);
1201
+ }
1202
+ await serveDevtools(server, res, {
1203
+ apiRoutes: serverMod.apiRoutes ?? [],
1204
+ app: serverMod.resolvedApp,
1205
+ url,
1206
+ wantsJson: requestUrl.pathname === DEVTOOLS_JSON_PATH
1207
+ });
1208
+ return;
1209
+ }
1210
+ if (shouldBypassDevSSR(requestUrl, req, routeMatchers)) return next();
1211
+ if (isDevNotFoundRequest(requestUrl, req, routeMatchers)) return serveDevNotFound(server, res, next, url, requestUrl.pathname, routeMatchers);
1183
1212
  let webRequest;
1184
1213
  try {
1185
1214
  webRequest = await nodeToWebRequest(req, maxBodySize);
@@ -1191,13 +1220,15 @@ function createDevSSRMiddleware(server, options = {}) {
1191
1220
  }
1192
1221
  throw err;
1193
1222
  }
1223
+ const timings = {};
1194
1224
  const response = await framework.handlePrachtRequest({
1195
1225
  app: serverMod.resolvedApp,
1196
1226
  registry: serverMod.registry,
1197
1227
  request: webRequest,
1198
1228
  debugErrors: true,
1199
1229
  clientEntryUrl: CLIENT_BROWSER_PATH,
1200
- apiRoutes: serverMod.apiRoutes
1230
+ apiRoutes: serverMod.apiRoutes,
1231
+ timings
1201
1232
  });
1202
1233
  if (response.status === 404) return next();
1203
1234
  const contentType = response.headers.get("content-type") ?? "text/html";
@@ -1207,12 +1238,38 @@ function createDevSSRMiddleware(server, options = {}) {
1207
1238
  response.headers.forEach((value, key) => {
1208
1239
  res.setHeader(key, value);
1209
1240
  });
1241
+ const serverTiming = framework.formatServerTimingHeader(timings);
1242
+ if (serverTiming) res.setHeader("Server-Timing", serverTiming);
1210
1243
  res.end(body);
1211
1244
  } catch (error) {
1212
1245
  await handleDevError(server, req, res, next, url, error);
1213
1246
  }
1214
1247
  };
1215
1248
  }
1249
+ /**
1250
+ * Serve the dev-only `/_pracht` devtools page (or `/_pracht.json`) built from
1251
+ * the same resolved app graph that `pracht inspect` reports.
1252
+ */
1253
+ async function serveDevtools(server, res, options) {
1254
+ const devtools = await server.ssrLoadModule("@pracht/core/devtools");
1255
+ const graph = await devtools.buildAppGraph({
1256
+ apiRoutes: options.apiRoutes,
1257
+ app: options.app,
1258
+ loadModule: (file) => server.ssrLoadModule(file),
1259
+ readSource: (file) => readFileSync(resolve(server.config.root, `.${file}`), "utf-8")
1260
+ });
1261
+ if (options.wantsJson) {
1262
+ res.statusCode = 200;
1263
+ res.setHeader("content-type", "application/json; charset=utf-8");
1264
+ res.end(JSON.stringify(graph, null, 2));
1265
+ return;
1266
+ }
1267
+ let html = devtools.buildDevtoolsHtml(graph);
1268
+ html = await server.transformIndexHtml(options.url, html);
1269
+ res.statusCode = 200;
1270
+ res.setHeader("content-type", "text/html; charset=utf-8");
1271
+ res.end(html);
1272
+ }
1216
1273
  async function handleDevError(server, req, res, next, url, error) {
1217
1274
  if (error instanceof Error) server.ssrFixStacktrace(error);
1218
1275
  if (req.headers["x-pracht-route-state-request"] === "1") {
@@ -1229,7 +1286,8 @@ async function handleDevError(server, req, res, next, url, error) {
1229
1286
  const { buildErrorOverlayHtml } = await server.ssrLoadModule("@pracht/core/error-overlay");
1230
1287
  let html = buildErrorOverlayHtml({
1231
1288
  message: error instanceof Error ? error.message : String(error),
1232
- stack: error instanceof Error ? error.stack : void 0
1289
+ stack: error instanceof Error ? error.stack : void 0,
1290
+ root: server.config.root
1233
1291
  });
1234
1292
  html = await server.transformIndexHtml(url, html);
1235
1293
  res.statusCode = 500;
@@ -1239,6 +1297,40 @@ async function handleDevError(server, req, res, next, url, error) {
1239
1297
  next(error);
1240
1298
  }
1241
1299
  }
1300
+ /**
1301
+ * True when a GET/HEAD document request matches no page route and no API
1302
+ * route — the dev middleware then serves the rich dev-only 404 page instead
1303
+ * of falling through to Vite. Route-state (JSON) requests and non-document
1304
+ * fetches keep their existing 404 behavior.
1305
+ */
1306
+ function isDevNotFoundRequest(requestUrl, req, options = {}) {
1307
+ const url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
1308
+ if (isRouteStateRequest(url, req)) return false;
1309
+ const method = (req.method ?? "GET").toUpperCase();
1310
+ if (method !== "GET" && method !== "HEAD") return false;
1311
+ const accept = readRequestHeader(req.headers.accept).toLowerCase();
1312
+ if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) return false;
1313
+ return !matchesResolvedRoute(url.pathname, options);
1314
+ }
1315
+ async function serveDevNotFound(server, res, next, url, pathname, options) {
1316
+ try {
1317
+ const { buildDevNotFoundHtml } = await server.ssrLoadModule("@pracht/core/dev-404");
1318
+ let html = buildDevNotFoundHtml({
1319
+ apiRoutes: options.apiRoutes.map((route) => ({ path: route.path })),
1320
+ requestedPath: pathname,
1321
+ routes: options.app.routes.map((route) => ({
1322
+ path: route.path,
1323
+ render: route.render ?? null
1324
+ }))
1325
+ });
1326
+ html = await server.transformIndexHtml(url, html);
1327
+ res.statusCode = 404;
1328
+ res.setHeader("content-type", "text/html; charset=utf-8");
1329
+ res.end(html);
1330
+ } catch {
1331
+ next();
1332
+ }
1333
+ }
1242
1334
  function shouldBypassDevSSR(requestUrl, req, options = {}) {
1243
1335
  const url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
1244
1336
  const pathname = url.pathname;
@@ -1363,10 +1455,16 @@ function pracht(options = {}) {
1363
1455
  const isSSRBuild = env.isSsrBuild;
1364
1456
  return {
1365
1457
  appType: "custom",
1366
- build: { rollupOptions: { output: { manualChunks(id) {
1458
+ ...isSSRBuild ? {} : { build: { rollupOptions: { output: { manualChunks(id) {
1367
1459
  if (id.includes("node_modules/preact") || id.includes("node_modules/preact-suspense")) return "vendor";
1368
- } } } },
1369
- ...isEdge && isSSRBuild ? { ssr: { noExternal: true } } : {}
1460
+ } } } } },
1461
+ ...isEdge && isSSRBuild ? {
1462
+ ssr: {
1463
+ noExternal: true,
1464
+ target: "webworker"
1465
+ },
1466
+ build: { rollupOptions: { external: [/^cloudflare:/] } }
1467
+ } : {}
1370
1468
  };
1371
1469
  },
1372
1470
  configResolved(config) {
@@ -1450,7 +1548,7 @@ function pracht(options = {}) {
1450
1548
  name: "pracht:optimize-deps-entries",
1451
1549
  enforce: "post",
1452
1550
  config(config) {
1453
- return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved));
1551
+ return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved), createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
1454
1552
  }
1455
1553
  };
1456
1554
  const precompilePlugin = resolved.precompileSsrJsx ? preactSsrPrecompile({
@@ -1481,10 +1579,26 @@ function rewriteManifestCoreImports(code) {
1481
1579
  return `import ${typeKeyword ?? ""}{${specifiers}} from ${quote}@pracht/core/manifest${quote}`;
1482
1580
  });
1483
1581
  }
1484
- function withPrachtOptimizeDepsEntries(config, prachtEntries) {
1582
+ const PRACHT_OPTIMIZE_DEPS_INCLUDE = [
1583
+ "@pracht/core",
1584
+ "@pracht/core/client",
1585
+ "@pracht/core/manifest"
1586
+ ];
1587
+ function createPrachtOptimizeDepsInclude(root) {
1588
+ try {
1589
+ if (!toPosixPath(createRequire(join(root, "package.json")).resolve("@pracht/core/package.json")).includes("/node_modules/")) return [];
1590
+ return PRACHT_OPTIMIZE_DEPS_INCLUDE;
1591
+ } catch {
1592
+ return [];
1593
+ }
1594
+ }
1595
+ function withPrachtOptimizeDepsEntries(config, prachtEntries, prachtInclude) {
1485
1596
  const environments = Object.fromEntries(Object.entries(config.environments ?? {}).map(([name, environment]) => [name, { optimizeDeps: { entries: mergeOptimizeDepsEntries(environment.optimizeDeps?.entries, prachtEntries) } }]));
1486
1597
  return {
1487
- optimizeDeps: { entries: mergeOptimizeDepsEntries(config.optimizeDeps?.entries, prachtEntries) },
1598
+ optimizeDeps: {
1599
+ entries: mergeOptimizeDepsEntries(config.optimizeDeps?.entries, prachtEntries),
1600
+ ...prachtInclude.length > 0 ? { include: mergeOptimizeDepsEntries(config.optimizeDeps?.include, prachtInclude) } : {}
1601
+ },
1488
1602
  ...Object.keys(environments).length > 0 ? { environments } : {}
1489
1603
  };
1490
1604
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/vite-plugin",
3
- "version": "0.4.4",
3
+ "version": "0.5.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",
@@ -44,9 +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.4",
48
- "@pracht/core": "0.8.1",
49
- "@pracht/preact-ssr-precompile": "0.1.1"
47
+ "@pracht/adapter-node": "0.2.5",
48
+ "@pracht/core": "0.9.0",
49
+ "@pracht/preact-ssr-precompile": "0.1.2"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "vite": "^8.0.0"