@pracht/vite-plugin 0.4.3 → 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 +8 -0
- package/dist/index.mjs +125 -11
- package/package.json +4 -4
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
|
-
|
|
1191
|
+
const routeMatchers = {
|
|
1178
1192
|
app: serverMod.resolvedApp,
|
|
1179
1193
|
apiRoutes: serverMod.apiRoutes,
|
|
1180
1194
|
matchApiRoute: framework.matchApiRoute,
|
|
1181
1195
|
matchAppRoute: framework.matchAppRoute
|
|
1182
|
-
}
|
|
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 ? {
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
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.
|
|
48
|
-
"@pracht/core": "0.
|
|
49
|
-
"@pracht/preact-ssr-precompile": "0.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"
|