@pracht/vite-plugin 0.4.4 → 0.6.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
|
@@ -5,6 +5,41 @@ import { RenderMode, RenderMode as RenderMode$1 } from "@pracht/core";
|
|
|
5
5
|
//#region src/plugin-assets.d.ts
|
|
6
6
|
declare const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
|
|
7
7
|
declare const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
|
|
8
|
+
declare const PRACHT_ISLANDS_CLIENT_MODULE_ID = "virtual:pracht/islands-client";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/env-safety.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* Env vars Vite defines on `import.meta.env` in every bundle, plus NODE_ENV
|
|
13
|
+
* which Vite's define pass statically replaces at build time (so it can never
|
|
14
|
+
* leak and is referenced by countless dependencies).
|
|
15
|
+
*/
|
|
16
|
+
declare const VITE_BUILTIN_ENV_VARS: Set<string>;
|
|
17
|
+
/** Prefix that marks an env var as intentionally public. */
|
|
18
|
+
declare const PUBLIC_ENV_PREFIX = "PRACHT_PUBLIC_";
|
|
19
|
+
interface EnvSafetyOptions {
|
|
20
|
+
/** Env var names allowed to appear in client bundles despite not being public. */
|
|
21
|
+
allow?: string[];
|
|
22
|
+
}
|
|
23
|
+
interface EnvLeakReference {
|
|
24
|
+
accessor: "process.env" | "import.meta.env";
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Scans JavaScript source for references to environment variables that are
|
|
29
|
+
* neither public-prefixed, Vite built-ins, nor explicitly allowed.
|
|
30
|
+
*/
|
|
31
|
+
declare function scanCodeForEnvLeaks(code: string, allow?: ReadonlySet<string>): EnvLeakReference[];
|
|
32
|
+
interface EnvLeakProblem extends EnvLeakReference {
|
|
33
|
+
chunk: string;
|
|
34
|
+
sources: string[];
|
|
35
|
+
}
|
|
36
|
+
declare function formatEnvLeakError(problems: EnvLeakProblem[]): string;
|
|
37
|
+
/**
|
|
38
|
+
* Build-time leak detection: scans rendered client chunks for references to
|
|
39
|
+
* non-public env vars and fails the build with the variable, chunk, and the
|
|
40
|
+
* likely source module.
|
|
41
|
+
*/
|
|
42
|
+
declare function createEnvSafetyPlugin(envSafety: false | EnvSafetyOptions): Plugin;
|
|
8
43
|
//#endregion
|
|
9
44
|
//#region src/plugin-adapter.d.ts
|
|
10
45
|
/**
|
|
@@ -56,6 +91,11 @@ interface PrachtPluginOptions {
|
|
|
56
91
|
middlewareDir?: string;
|
|
57
92
|
apiDir?: string;
|
|
58
93
|
serverDir?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Directory containing island components hydrated on
|
|
96
|
+
* `hydration: "islands"` routes. Defaults to "/src/islands".
|
|
97
|
+
*/
|
|
98
|
+
islandsDir?: string;
|
|
59
99
|
adapter?: PrachtAdapter;
|
|
60
100
|
/** Enable file-system pages routing by pointing to the pages directory (e.g. "/src/pages"). */
|
|
61
101
|
pagesDir?: string;
|
|
@@ -65,17 +105,39 @@ interface PrachtPluginOptions {
|
|
|
65
105
|
prerenderConcurrency?: number;
|
|
66
106
|
/** Maximum request body size (bytes) accepted by the dev SSR middleware. Defaults to 1 MiB. */
|
|
67
107
|
maxBodySize?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Per-route gzip client-JS budgets evaluated by `pracht build`, e.g.
|
|
110
|
+
* `{ "*": "120kb", "/dashboard": "200kb" }`. `"*"` applies to every route;
|
|
111
|
+
* explicit route paths override it. Values are byte counts or size strings
|
|
112
|
+
* ("120kb", "1mb"). Exceeded budgets fail the build unless
|
|
113
|
+
* `pracht build --no-budget-fail` is used.
|
|
114
|
+
*/
|
|
115
|
+
budgets?: Record<string, string | number>;
|
|
68
116
|
/**
|
|
69
117
|
* Opt into precompiling safe Preact JSX DOM subtrees for SSR/SSG server bundles.
|
|
70
118
|
* Client bundles keep the normal Preact JSX transform for hydration.
|
|
71
119
|
*/
|
|
72
120
|
precompileSsrJsx?: boolean | PreactSsrPrecompileOptions;
|
|
121
|
+
/**
|
|
122
|
+
* Client-bundle env leak detection. Enabled by default: production client
|
|
123
|
+
* chunks referencing `process.env.X` / `import.meta.env.X` for a non-public
|
|
124
|
+
* variable fail the build. Pass `{ allow: ["NAME"] }` to permit specific
|
|
125
|
+
* variables, or `false` to disable the check entirely.
|
|
126
|
+
*/
|
|
127
|
+
envSafety?: false | EnvSafetyOptions;
|
|
73
128
|
}
|
|
74
129
|
//#endregion
|
|
75
130
|
//#region src/plugin-codegen.d.ts
|
|
76
131
|
declare function createPrachtClientModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
77
132
|
root?: string;
|
|
78
133
|
}): string;
|
|
134
|
+
/**
|
|
135
|
+
* Source of `virtual:pracht/islands-client` — the tiny bootstrap loaded by
|
|
136
|
+
* `hydration: "islands"` routes. It deliberately does NOT import the app
|
|
137
|
+
* manifest, the router, or the full client runtime: it only scans the DOM
|
|
138
|
+
* for island markers and hydrates the islands present on the page.
|
|
139
|
+
*/
|
|
140
|
+
declare function createPrachtIslandsClientModuleSource(options?: PrachtPluginOptions): string;
|
|
79
141
|
declare function createPrachtServerModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
80
142
|
root?: string;
|
|
81
143
|
isBuild?: boolean;
|
|
@@ -85,4 +147,4 @@ declare function createPrachtRegistryModuleSource(options?: PrachtPluginOptions)
|
|
|
85
147
|
//#region src/index.d.ts
|
|
86
148
|
declare function pracht(options?: PrachtPluginOptions): Plugin[];
|
|
87
149
|
//#endregion
|
|
88
|
-
export { PRACHT_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, type PrachtAdapter, type PrachtPluginOptions, type RenderMode, createPrachtClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, pracht };
|
|
150
|
+
export { type EnvLeakReference, type EnvSafetyOptions, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PUBLIC_ENV_PREFIX, type PrachtAdapter, type PrachtPluginOptions, type RenderMode, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-
|
|
1
|
+
import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-BsVlzz-e.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, extname, join, resolve } from "node:path";
|
|
5
6
|
import { parseAst } from "vite";
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
7
8
|
import { createNodeServerEntryModule } from "@pracht/adapter-node";
|
|
8
9
|
//#region src/client-module-query.ts
|
|
9
10
|
const CLIENT_MODULE_QUERY = "pracht-client";
|
|
@@ -887,20 +888,323 @@ function enqueueDependencies(target, dependencies) {
|
|
|
887
888
|
for (const name of dependencies) target.add(name);
|
|
888
889
|
}
|
|
889
890
|
//#endregion
|
|
891
|
+
//#region src/env-safety.ts
|
|
892
|
+
/**
|
|
893
|
+
* Env vars Vite defines on `import.meta.env` in every bundle, plus NODE_ENV
|
|
894
|
+
* which Vite's define pass statically replaces at build time (so it can never
|
|
895
|
+
* leak and is referenced by countless dependencies).
|
|
896
|
+
*/
|
|
897
|
+
const VITE_BUILTIN_ENV_VARS = new Set([
|
|
898
|
+
"MODE",
|
|
899
|
+
"DEV",
|
|
900
|
+
"PROD",
|
|
901
|
+
"SSR",
|
|
902
|
+
"BASE_URL",
|
|
903
|
+
"NODE_ENV"
|
|
904
|
+
]);
|
|
905
|
+
/** Prefix that marks an env var as intentionally public. */
|
|
906
|
+
const PUBLIC_ENV_PREFIX = "PRACHT_PUBLIC_";
|
|
907
|
+
/** Server-only core entry that must never resolve into client bundles. */
|
|
908
|
+
const SERVER_ENV_MODULE_ID = "@pracht/core/env/server";
|
|
909
|
+
const ENV_REFERENCE_RE = /\b(process\.env|import\.meta\.env)(?:\.([A-Za-z_$][A-Za-z0-9_$]*)|\[\s*(["'])([A-Za-z_$][A-Za-z0-9_$]*)\3\s*\])/g;
|
|
910
|
+
/**
|
|
911
|
+
* Scans JavaScript source for references to environment variables that are
|
|
912
|
+
* neither public-prefixed, Vite built-ins, nor explicitly allowed.
|
|
913
|
+
*/
|
|
914
|
+
function scanCodeForEnvLeaks(code, allow = /* @__PURE__ */ new Set()) {
|
|
915
|
+
const findings = [];
|
|
916
|
+
const seen = /* @__PURE__ */ new Set();
|
|
917
|
+
const codePositions = getCodePositionMask(code);
|
|
918
|
+
for (const match of code.matchAll(ENV_REFERENCE_RE)) {
|
|
919
|
+
if (!codePositions[match.index ?? -1]) continue;
|
|
920
|
+
const accessor = match[1];
|
|
921
|
+
const name = match[2] ?? match[4];
|
|
922
|
+
if (!name) continue;
|
|
923
|
+
if (name.startsWith("PRACHT_PUBLIC_")) continue;
|
|
924
|
+
if (VITE_BUILTIN_ENV_VARS.has(name)) continue;
|
|
925
|
+
if (allow.has(name)) continue;
|
|
926
|
+
const key = `${accessor}.${name}`;
|
|
927
|
+
if (seen.has(key)) continue;
|
|
928
|
+
seen.add(key);
|
|
929
|
+
findings.push({
|
|
930
|
+
accessor,
|
|
931
|
+
name
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return findings;
|
|
935
|
+
}
|
|
936
|
+
function getCodePositionMask(code) {
|
|
937
|
+
const mask = new Uint8Array(code.length);
|
|
938
|
+
const templateExpressionDepths = [];
|
|
939
|
+
let mode = "code";
|
|
940
|
+
let regexCharClass = false;
|
|
941
|
+
let i = 0;
|
|
942
|
+
while (i < code.length) {
|
|
943
|
+
const char = code[i];
|
|
944
|
+
const next = code[i + 1];
|
|
945
|
+
if (mode === "line-comment") {
|
|
946
|
+
if (char === "\n" || char === "\r") {
|
|
947
|
+
mode = "code";
|
|
948
|
+
mask[i] = 1;
|
|
949
|
+
}
|
|
950
|
+
i++;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (mode === "block-comment") {
|
|
954
|
+
if (char === "*" && next === "/") {
|
|
955
|
+
mode = "code";
|
|
956
|
+
i += 2;
|
|
957
|
+
} else i++;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (mode === "single" || mode === "double") {
|
|
961
|
+
const quote = mode === "single" ? "'" : "\"";
|
|
962
|
+
if (char === "\\") {
|
|
963
|
+
i += 2;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (char === quote || char === "\n" || char === "\r") mode = "code";
|
|
967
|
+
i++;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (mode === "regex") {
|
|
971
|
+
if (char === "\\") {
|
|
972
|
+
i += 2;
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (char === "[") {
|
|
976
|
+
regexCharClass = true;
|
|
977
|
+
i++;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (char === "]") {
|
|
981
|
+
regexCharClass = false;
|
|
982
|
+
i++;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (char === "/" && !regexCharClass) {
|
|
986
|
+
regexCharClass = false;
|
|
987
|
+
i++;
|
|
988
|
+
while (i < code.length && isIdentifierChar(code[i])) i++;
|
|
989
|
+
mode = "code";
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (char === "\n" || char === "\r") {
|
|
993
|
+
regexCharClass = false;
|
|
994
|
+
mode = "code";
|
|
995
|
+
}
|
|
996
|
+
i++;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (mode === "template") {
|
|
1000
|
+
if (char === "\\") {
|
|
1001
|
+
i += 2;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (char === "`") {
|
|
1005
|
+
mode = "code";
|
|
1006
|
+
i++;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (char === "$" && next === "{") {
|
|
1010
|
+
mask[i] = 1;
|
|
1011
|
+
mask[i + 1] = 1;
|
|
1012
|
+
templateExpressionDepths.push(1);
|
|
1013
|
+
mode = "code";
|
|
1014
|
+
i += 2;
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
i++;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
mask[i] = 1;
|
|
1021
|
+
if (char === "/" && next === "/") {
|
|
1022
|
+
mask[i + 1] = 1;
|
|
1023
|
+
mode = "line-comment";
|
|
1024
|
+
i += 2;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (char === "/" && next === "*") {
|
|
1028
|
+
mask[i + 1] = 1;
|
|
1029
|
+
mode = "block-comment";
|
|
1030
|
+
i += 2;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (char === "/" && isRegexLiteralStart(code, i)) {
|
|
1034
|
+
mode = "regex";
|
|
1035
|
+
regexCharClass = false;
|
|
1036
|
+
i++;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (char === "'") {
|
|
1040
|
+
mode = "single";
|
|
1041
|
+
i++;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (char === "\"") {
|
|
1045
|
+
mode = "double";
|
|
1046
|
+
i++;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (char === "`") {
|
|
1050
|
+
mode = "template";
|
|
1051
|
+
i++;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (templateExpressionDepths.length > 0) {
|
|
1055
|
+
const top = templateExpressionDepths.length - 1;
|
|
1056
|
+
if (char === "{") templateExpressionDepths[top]++;
|
|
1057
|
+
else if (char === "}") {
|
|
1058
|
+
templateExpressionDepths[top]--;
|
|
1059
|
+
if (templateExpressionDepths[top] === 0) {
|
|
1060
|
+
templateExpressionDepths.pop();
|
|
1061
|
+
mode = "template";
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
i++;
|
|
1066
|
+
}
|
|
1067
|
+
return mask;
|
|
1068
|
+
}
|
|
1069
|
+
function isRegexLiteralStart(code, slashIndex) {
|
|
1070
|
+
let i = slashIndex - 1;
|
|
1071
|
+
while (i >= 0 && /\s/.test(code[i])) i--;
|
|
1072
|
+
if (i < 0) return true;
|
|
1073
|
+
const previous = code[i];
|
|
1074
|
+
if (previous === ">" && code[i - 1] === "=") return true;
|
|
1075
|
+
if ("([{=,:;!?&|^~<>*%+-".includes(previous)) return true;
|
|
1076
|
+
if (isIdentifierChar(previous)) {
|
|
1077
|
+
let start = i;
|
|
1078
|
+
while (start >= 0 && isIdentifierChar(code[start])) start--;
|
|
1079
|
+
const word = code.slice(start + 1, i + 1);
|
|
1080
|
+
return new Set([
|
|
1081
|
+
"await",
|
|
1082
|
+
"case",
|
|
1083
|
+
"delete",
|
|
1084
|
+
"do",
|
|
1085
|
+
"else",
|
|
1086
|
+
"in",
|
|
1087
|
+
"instanceof",
|
|
1088
|
+
"new",
|
|
1089
|
+
"of",
|
|
1090
|
+
"return",
|
|
1091
|
+
"throw",
|
|
1092
|
+
"typeof",
|
|
1093
|
+
"void",
|
|
1094
|
+
"yield"
|
|
1095
|
+
]).has(word);
|
|
1096
|
+
}
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
function isIdentifierChar(char) {
|
|
1100
|
+
return !!char && /[A-Za-z0-9_$]/.test(char);
|
|
1101
|
+
}
|
|
1102
|
+
function formatEnvLeakError(problems) {
|
|
1103
|
+
return [
|
|
1104
|
+
"[pracht] Environment variable leak detected in the client bundle:",
|
|
1105
|
+
...problems.map((problem) => {
|
|
1106
|
+
const source = problem.sources.length > 0 ? ` (likely from ${problem.sources.map((file) => JSON.stringify(file)).join(", ")})` : "";
|
|
1107
|
+
return ` - ${problem.accessor}.${problem.name} in chunk "${problem.chunk}"${source}`;
|
|
1108
|
+
}),
|
|
1109
|
+
"",
|
|
1110
|
+
`Only PRACHT_PUBLIC_-prefixed variables may be referenced in client code (prefer publicEnv from "@pracht/core" for typed public values).`,
|
|
1111
|
+
`Move server-only reads into loaders/API routes and access them via serverEnv from "@pracht/core/env/server",`,
|
|
1112
|
+
"or allowlist intentionally-safe names with pracht({ envSafety: { allow: [...] } })."
|
|
1113
|
+
].join("\n");
|
|
1114
|
+
}
|
|
1115
|
+
function stripIdQuery(id) {
|
|
1116
|
+
const queryStart = id.indexOf("?");
|
|
1117
|
+
return queryStart === -1 ? id : id.slice(0, queryStart);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Build-time leak detection: scans rendered client chunks for references to
|
|
1121
|
+
* non-public env vars and fails the build with the variable, chunk, and the
|
|
1122
|
+
* likely source module.
|
|
1123
|
+
*/
|
|
1124
|
+
function createEnvSafetyPlugin(envSafety) {
|
|
1125
|
+
const allow = new Set(envSafety === false ? [] : envSafety.allow ?? []);
|
|
1126
|
+
const moduleEnvReferences = /* @__PURE__ */ new Map();
|
|
1127
|
+
let isSsrBuild = false;
|
|
1128
|
+
return {
|
|
1129
|
+
name: "pracht:env-safety",
|
|
1130
|
+
apply: "build",
|
|
1131
|
+
enforce: "post",
|
|
1132
|
+
configResolved(config) {
|
|
1133
|
+
isSsrBuild = !!config.build.ssr;
|
|
1134
|
+
},
|
|
1135
|
+
transform(code, id, transformOptions) {
|
|
1136
|
+
if (envSafety === false) return null;
|
|
1137
|
+
if (transformOptions?.ssr) return null;
|
|
1138
|
+
const moduleId = stripIdQuery(id);
|
|
1139
|
+
if (moduleId.includes("node_modules")) return null;
|
|
1140
|
+
const findings = scanCodeForEnvLeaks(code, allow);
|
|
1141
|
+
if (findings.length > 0) moduleEnvReferences.set(moduleId, findings);
|
|
1142
|
+
return null;
|
|
1143
|
+
},
|
|
1144
|
+
generateBundle(_options, bundle) {
|
|
1145
|
+
if (envSafety === false) return;
|
|
1146
|
+
const consumer = this.environment?.config?.consumer;
|
|
1147
|
+
if (!(consumer ? consumer === "client" : !isSsrBuild)) return;
|
|
1148
|
+
const problems = [];
|
|
1149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1150
|
+
const addProblem = (problem) => {
|
|
1151
|
+
const key = `${problem.chunk}:${problem.accessor}.${problem.name}`;
|
|
1152
|
+
if (seen.has(key)) return;
|
|
1153
|
+
seen.add(key);
|
|
1154
|
+
problems.push(problem);
|
|
1155
|
+
};
|
|
1156
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
1157
|
+
if (output.type !== "chunk") continue;
|
|
1158
|
+
const moduleIds = (output.moduleIds ?? Object.keys(output.modules ?? {})).map(stripIdQuery);
|
|
1159
|
+
for (const moduleId of moduleIds) {
|
|
1160
|
+
const references = moduleEnvReferences.get(moduleId);
|
|
1161
|
+
if (!references) continue;
|
|
1162
|
+
for (const reference of references) addProblem({
|
|
1163
|
+
...reference,
|
|
1164
|
+
chunk: fileName,
|
|
1165
|
+
sources: [moduleId]
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
for (const finding of scanCodeForEnvLeaks(output.code, allow)) {
|
|
1169
|
+
const sources = moduleIds.filter((moduleId) => moduleEnvReferences.get(moduleId)?.some((reference) => reference.name === finding.name));
|
|
1170
|
+
addProblem({
|
|
1171
|
+
...finding,
|
|
1172
|
+
chunk: fileName,
|
|
1173
|
+
sources
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (problems.length > 0) this.error(formatEnvLeakError(problems));
|
|
1178
|
+
this.emitFile({
|
|
1179
|
+
fileName: "_pracht/env-safety.json",
|
|
1180
|
+
source: JSON.stringify({
|
|
1181
|
+
findings: problems,
|
|
1182
|
+
version: 1
|
|
1183
|
+
}, null, 2),
|
|
1184
|
+
type: "asset"
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
//#endregion
|
|
890
1190
|
//#region src/plugin-assets.ts
|
|
891
1191
|
const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
|
|
892
1192
|
const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
|
|
1193
|
+
const PRACHT_ISLANDS_CLIENT_MODULE_ID = "virtual:pracht/islands-client";
|
|
893
1194
|
const CLIENT_BROWSER_PATH = "/@pracht/client.js";
|
|
1195
|
+
const ISLANDS_CLIENT_BROWSER_PATH = "/@pracht/islands.js";
|
|
894
1196
|
function readClientBuildAssets(root = process.cwd()) {
|
|
895
1197
|
const manifestPath = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"].map((candidate) => resolve(root, candidate)).find((candidate) => existsSync(candidate));
|
|
896
1198
|
if (!manifestPath) return {
|
|
897
1199
|
clientEntryUrl: null,
|
|
1200
|
+
islandsEntryUrl: null,
|
|
898
1201
|
cssManifest: {},
|
|
899
1202
|
jsManifest: {}
|
|
900
1203
|
};
|
|
901
1204
|
const rawManifest = readFileSync(manifestPath, "utf-8");
|
|
902
1205
|
const manifest = JSON.parse(rawManifest);
|
|
903
1206
|
const clientEntry = manifest[PRACHT_CLIENT_MODULE_ID];
|
|
1207
|
+
const islandsEntry = manifest[PRACHT_ISLANDS_CLIENT_MODULE_ID];
|
|
904
1208
|
const cssManifest = {};
|
|
905
1209
|
const jsManifest = {};
|
|
906
1210
|
for (const [key, entry] of Object.entries(manifest)) {
|
|
@@ -910,12 +1214,20 @@ function readClientBuildAssets(root = process.cwd()) {
|
|
|
910
1214
|
if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((f) => `/${f}`);
|
|
911
1215
|
if (deps.js.length > 0) jsManifest[manifestKey] = deps.js.map((f) => `/${f}`);
|
|
912
1216
|
}
|
|
1217
|
+
addEntryDeps(manifest, jsManifest, PRACHT_CLIENT_MODULE_ID, clientEntry);
|
|
1218
|
+
addEntryDeps(manifest, jsManifest, PRACHT_ISLANDS_CLIENT_MODULE_ID, islandsEntry);
|
|
913
1219
|
return {
|
|
914
1220
|
clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
|
|
1221
|
+
islandsEntryUrl: islandsEntry ? `/${islandsEntry.file}` : null,
|
|
915
1222
|
cssManifest,
|
|
916
1223
|
jsManifest
|
|
917
1224
|
};
|
|
918
1225
|
}
|
|
1226
|
+
function addEntryDeps(manifest, jsManifest, entryKey, entry) {
|
|
1227
|
+
if (!entry) return;
|
|
1228
|
+
const deps = collectTransitiveDeps(manifest, entryKey).js.filter((file) => file !== entry.file);
|
|
1229
|
+
if (deps.length > 0) jsManifest[entryKey] = deps.map((file) => `/${file}`);
|
|
1230
|
+
}
|
|
919
1231
|
function collectTransitiveDeps(manifest, key) {
|
|
920
1232
|
const css = /* @__PURE__ */ new Set();
|
|
921
1233
|
const js = /* @__PURE__ */ new Set();
|
|
@@ -941,6 +1253,9 @@ function isClientModule(id) {
|
|
|
941
1253
|
function isServerModule(id) {
|
|
942
1254
|
return id === "virtual:pracht/server" || id.endsWith("virtual:pracht/server");
|
|
943
1255
|
}
|
|
1256
|
+
function isIslandsClientModule(id) {
|
|
1257
|
+
return id === "virtual:pracht/islands-client" || id === "/@pracht/islands.js" || id.endsWith("virtual:pracht/islands-client");
|
|
1258
|
+
}
|
|
944
1259
|
//#endregion
|
|
945
1260
|
//#region src/plugin-adapter.ts
|
|
946
1261
|
function createDefaultNodeAdapter() {
|
|
@@ -961,12 +1276,15 @@ const DEFAULTS = {
|
|
|
961
1276
|
shellsDir: "/src/shells",
|
|
962
1277
|
apiDir: "/src/api",
|
|
963
1278
|
serverDir: "/src/server",
|
|
1279
|
+
islandsDir: "/src/islands",
|
|
964
1280
|
adapter: createDefaultNodeAdapter(),
|
|
965
1281
|
pagesDir: "",
|
|
966
1282
|
pagesDefaultRender: "ssr",
|
|
967
1283
|
prerenderConcurrency: 10,
|
|
968
1284
|
maxBodySize: 1024 * 1024,
|
|
969
|
-
|
|
1285
|
+
budgets: {},
|
|
1286
|
+
precompileSsrJsx: false,
|
|
1287
|
+
envSafety: {}
|
|
970
1288
|
};
|
|
971
1289
|
function resolveOptions(options) {
|
|
972
1290
|
const resolved = {
|
|
@@ -975,10 +1293,115 @@ function resolveOptions(options) {
|
|
|
975
1293
|
};
|
|
976
1294
|
if (!Number.isInteger(resolved.prerenderConcurrency) || resolved.prerenderConcurrency <= 0) throw new Error("pracht({ prerenderConcurrency }) expects a positive integer.");
|
|
977
1295
|
if (!Number.isInteger(resolved.maxBodySize) || resolved.maxBodySize <= 0) throw new Error("pracht({ maxBodySize }) expects a positive integer number of bytes.");
|
|
1296
|
+
validateBudgets(resolved.budgets);
|
|
978
1297
|
return resolved;
|
|
979
1298
|
}
|
|
1299
|
+
function validateBudgets(budgets) {
|
|
1300
|
+
for (const [key, value] of Object.entries(budgets)) {
|
|
1301
|
+
if (key !== "*" && !key.startsWith("/")) throw new Error(`pracht({ budgets }) keys must be "*" or a route path starting with "/", got ${JSON.stringify(key)}.`);
|
|
1302
|
+
const isValidNumber = typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
1303
|
+
const isValidString = typeof value === "string" && value.trim().length > 0;
|
|
1304
|
+
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)}.`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
980
1307
|
//#endregion
|
|
981
1308
|
//#region src/plugin-codegen.ts
|
|
1309
|
+
const ROUTE_MODULE_EXTENSIONS = new Set([
|
|
1310
|
+
".ts",
|
|
1311
|
+
".tsx",
|
|
1312
|
+
".js",
|
|
1313
|
+
".jsx",
|
|
1314
|
+
".md",
|
|
1315
|
+
".mdx",
|
|
1316
|
+
".tsrx"
|
|
1317
|
+
]);
|
|
1318
|
+
const NON_FULL_HYDRATION_RE = /hydration\s*:\s*["'](?:islands|none)["']/;
|
|
1319
|
+
const FULL_HYDRATION_RE = /hydration\s*:\s*["']full["']/;
|
|
1320
|
+
const PAGES_NON_FULL_HYDRATION_RE = /export\s+const\s+HYDRATION\s*=\s*["'](?:islands|none)["']/;
|
|
1321
|
+
function toPosixPath$1(path) {
|
|
1322
|
+
return path.replace(/\\/g, "/");
|
|
1323
|
+
}
|
|
1324
|
+
function findMatching(source, start, open, close) {
|
|
1325
|
+
let depth = 0;
|
|
1326
|
+
for (let i = start; i < source.length; i++) {
|
|
1327
|
+
const ch = source[i];
|
|
1328
|
+
if (ch === open) depth++;
|
|
1329
|
+
if (ch === close) {
|
|
1330
|
+
depth--;
|
|
1331
|
+
if (depth === 0) return i;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return -1;
|
|
1335
|
+
}
|
|
1336
|
+
function scanFiles(dir, files) {
|
|
1337
|
+
let entries;
|
|
1338
|
+
try {
|
|
1339
|
+
entries = readdirSync(dir);
|
|
1340
|
+
} catch {
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
for (const entry of entries) {
|
|
1344
|
+
const abs = join(dir, entry);
|
|
1345
|
+
let stat;
|
|
1346
|
+
try {
|
|
1347
|
+
stat = statSync(abs);
|
|
1348
|
+
} catch {
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (stat.isDirectory()) scanFiles(abs, files);
|
|
1352
|
+
else if (ROUTE_MODULE_EXTENSIONS.has(extname(entry))) files.push(abs);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
function createNonFullHydrationExcludes(resolved, root = process.cwd()) {
|
|
1356
|
+
const excludes = /* @__PURE__ */ new Set();
|
|
1357
|
+
if (resolved.pagesDir) {
|
|
1358
|
+
const files = [];
|
|
1359
|
+
scanFiles(resolve(root, resolved.pagesDir.replace(/^\//, "")), files);
|
|
1360
|
+
for (const file of files) try {
|
|
1361
|
+
if (PAGES_NON_FULL_HYDRATION_RE.test(readFileSync(file, "utf-8"))) excludes.add(`!/${toPosixPath$1(file).replace(toPosixPath$1(root).replace(/\/$/, "") + "/", "")}`);
|
|
1362
|
+
} catch {}
|
|
1363
|
+
return [...excludes];
|
|
1364
|
+
}
|
|
1365
|
+
const appFile = resolve(root, resolved.appFile.replace(/^\//, ""));
|
|
1366
|
+
let source;
|
|
1367
|
+
try {
|
|
1368
|
+
source = readFileSync(appFile, "utf-8");
|
|
1369
|
+
} catch {
|
|
1370
|
+
return [];
|
|
1371
|
+
}
|
|
1372
|
+
const groups = [];
|
|
1373
|
+
for (const match of source.matchAll(/\bgroup\s*\(/g)) {
|
|
1374
|
+
const parenStart = match.index + match[0].lastIndexOf("(");
|
|
1375
|
+
const parenEnd = findMatching(source, parenStart, "(", ")");
|
|
1376
|
+
if (parenEnd === -1) continue;
|
|
1377
|
+
const args = source.slice(parenStart + 1, parenEnd);
|
|
1378
|
+
const arrayStart = source.indexOf("[", parenStart);
|
|
1379
|
+
if (arrayStart === -1 || arrayStart > parenEnd) continue;
|
|
1380
|
+
const arrayEnd = findMatching(source, arrayStart, "[", "]");
|
|
1381
|
+
if (arrayEnd === -1) continue;
|
|
1382
|
+
groups.push({
|
|
1383
|
+
start: arrayStart,
|
|
1384
|
+
end: arrayEnd,
|
|
1385
|
+
nonFull: NON_FULL_HYDRATION_RE.test(args.split("[")[0] ?? "")
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
const appDir = dirname(appFile);
|
|
1389
|
+
for (const match of source.matchAll(/\broute\s*\(\s*[^,]+,\s*(?:(?:\(\s*\)\s*=>\s*import\s*\(\s*)?["']([^"']+)["']\s*\)?|["']([^"']+)["'])/g)) {
|
|
1390
|
+
const fileRef = match[1] ?? match[2];
|
|
1391
|
+
const callStart = match.index;
|
|
1392
|
+
const parenStart = source.indexOf("(", callStart);
|
|
1393
|
+
const parenEnd = findMatching(source, parenStart, "(", ")");
|
|
1394
|
+
if (parenEnd === -1) continue;
|
|
1395
|
+
const callSource = source.slice(parenStart, parenEnd);
|
|
1396
|
+
const ownNonFull = NON_FULL_HYDRATION_RE.test(callSource);
|
|
1397
|
+
const ownFull = FULL_HYDRATION_RE.test(callSource);
|
|
1398
|
+
const inheritedNonFull = groups.filter((group) => group.start < callStart && callStart < group.end).sort((a, b) => b.start - a.start)[0]?.nonFull;
|
|
1399
|
+
if (ownFull || !ownNonFull && inheritedNonFull !== true) continue;
|
|
1400
|
+
const abs = resolve(appDir, fileRef);
|
|
1401
|
+
excludes.add(`!/${toPosixPath$1(abs).replace(toPosixPath$1(root).replace(/\/$/, "") + "/", "")}`);
|
|
1402
|
+
}
|
|
1403
|
+
return [...excludes];
|
|
1404
|
+
}
|
|
982
1405
|
function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
983
1406
|
const resolved = resolveOptions(options);
|
|
984
1407
|
const isPagesMode = !!resolved.pagesDir;
|
|
@@ -987,16 +1410,21 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
987
1410
|
const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
|
|
988
1411
|
const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
|
|
989
1412
|
const routeTsrxGlob = `${dirPrefix}/**/*.tsrx`;
|
|
1413
|
+
const routeExcludes = createNonFullHydrationExcludes(resolved, buildOptions.root);
|
|
1414
|
+
const routeGlobPattern = routeExcludes.length > 0 ? [routeGlob, ...routeExcludes] : routeGlob;
|
|
1415
|
+
const routeTsrxGlobPattern = routeExcludes.length > 0 ? [routeTsrxGlob, ...routeExcludes] : routeTsrxGlob;
|
|
990
1416
|
const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
|
|
991
1417
|
const shellTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.tsrx` : `${resolved.shellsDir}/**/*.tsrx`;
|
|
1418
|
+
const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1419
|
+
const appDir = (appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`).replace(/\/[^/]*$/, "") || "/";
|
|
992
1420
|
return [
|
|
993
1421
|
"import { resolveApp, initClientRouter, readHydrationState } from \"@pracht/core/client\";",
|
|
994
1422
|
appImport,
|
|
995
1423
|
"",
|
|
996
1424
|
`const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
|
|
997
1425
|
`const routeModules = {`,
|
|
998
|
-
` ...import.meta.glob(${JSON.stringify(
|
|
999
|
-
` ...import.meta.glob(${JSON.stringify(
|
|
1426
|
+
` ...import.meta.glob(${JSON.stringify(routeGlobPattern)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
|
|
1427
|
+
` ...import.meta.glob(${JSON.stringify(routeTsrxGlobPattern)}),`,
|
|
1000
1428
|
`};`,
|
|
1001
1429
|
`const shellModules = {`,
|
|
1002
1430
|
` ...import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
|
|
@@ -1007,8 +1435,22 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1007
1435
|
"applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
|
|
1008
1436
|
"",
|
|
1009
1437
|
...createApplyRouteLoaderHintsSource(),
|
|
1010
|
-
|
|
1011
|
-
"
|
|
1438
|
+
`const APP_DIR = ${JSON.stringify(appDir)};`,
|
|
1439
|
+
"",
|
|
1440
|
+
"// Manifest refs are written relative to the app manifest file",
|
|
1441
|
+
"// (\"./routes/home.tsx\") while import.meta.glob keys are root-absolute",
|
|
1442
|
+
"// (\"/src/routes/home.tsx\"). Both sides canonicalize against APP_DIR —",
|
|
1443
|
+
"// known at build time — replacing the previous runtime suffix index.",
|
|
1444
|
+
"function canonicalModuleKey(path) {",
|
|
1445
|
+
" const raw = path.split(\"?\")[0];",
|
|
1446
|
+
" const joined = raw.startsWith(\"/\") ? raw : APP_DIR + \"/\" + raw;",
|
|
1447
|
+
" const parts = [];",
|
|
1448
|
+
" for (const segment of joined.split(\"/\")) {",
|
|
1449
|
+
" if (!segment || segment === \".\") continue;",
|
|
1450
|
+
" if (segment === \"..\") parts.pop();",
|
|
1451
|
+
" else parts.push(segment);",
|
|
1452
|
+
" }",
|
|
1453
|
+
" return \"/\" + parts.join(\"/\");",
|
|
1012
1454
|
"}",
|
|
1013
1455
|
"",
|
|
1014
1456
|
"const moduleKeyIndexes = new WeakMap();",
|
|
@@ -1016,22 +1458,34 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1016
1458
|
" let index = moduleKeyIndexes.get(modules);",
|
|
1017
1459
|
" if (index) return index;",
|
|
1018
1460
|
" index = new Map();",
|
|
1019
|
-
" for (const key of Object.keys(modules))
|
|
1020
|
-
" const normalized = normalizeModuleKey(key);",
|
|
1021
|
-
" if (!normalized) continue;",
|
|
1022
|
-
" if (!index.has(normalized)) index.set(normalized, key);",
|
|
1023
|
-
" for (let i = normalized.indexOf(\"/\"); i !== -1; i = normalized.indexOf(\"/\", i + 1)) {",
|
|
1024
|
-
" const suffix = normalized.slice(i + 1);",
|
|
1025
|
-
" if (suffix && !index.has(suffix)) index.set(suffix, key);",
|
|
1026
|
-
" }",
|
|
1027
|
-
" }",
|
|
1461
|
+
" for (const key of Object.keys(modules)) index.set(canonicalModuleKey(key), key);",
|
|
1028
1462
|
" moduleKeyIndexes.set(modules, index);",
|
|
1029
1463
|
" return index;",
|
|
1030
1464
|
"}",
|
|
1031
1465
|
"",
|
|
1032
1466
|
"function findModuleKey(modules, file) {",
|
|
1033
1467
|
" if (file in modules) return file;",
|
|
1034
|
-
"
|
|
1468
|
+
" const key = getModuleKeyIndex(modules).get(canonicalModuleKey(file));",
|
|
1469
|
+
" if (key != null) return key;",
|
|
1470
|
+
" if (import.meta.env?.DEV) {",
|
|
1471
|
+
" // Dev-only lenient fallback so refs that never canonicalize (written",
|
|
1472
|
+
" // relative to a file other than the app manifest) keep working while",
|
|
1473
|
+
" // the console error tells the author to fix them — production builds",
|
|
1474
|
+
" // resolve strictly and drop this branch.",
|
|
1475
|
+
" const suffix = \"/\" + file.split(\"?\")[0].replace(/^\\.?\\//, \"\");",
|
|
1476
|
+
" for (const candidate of Object.keys(modules)) {",
|
|
1477
|
+
" if (canonicalModuleKey(candidate).endsWith(suffix)) {",
|
|
1478
|
+
" console.error(",
|
|
1479
|
+
" `[pracht] Module ref ${JSON.stringify(file)} only resolved by suffix matching ` +",
|
|
1480
|
+
" `against ${JSON.stringify(candidate)}. Write manifest refs relative to the app ` +",
|
|
1481
|
+
" `manifest file (e.g. \"./routes/home.tsx\") — suffix matching is disabled in ` +",
|
|
1482
|
+
" `production builds.`,",
|
|
1483
|
+
" );",
|
|
1484
|
+
" return candidate;",
|
|
1485
|
+
" }",
|
|
1486
|
+
" }",
|
|
1487
|
+
" }",
|
|
1488
|
+
" return null;",
|
|
1035
1489
|
"}",
|
|
1036
1490
|
"",
|
|
1037
1491
|
"const state = readHydrationState();",
|
|
@@ -1049,6 +1503,23 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1049
1503
|
""
|
|
1050
1504
|
].join("\n");
|
|
1051
1505
|
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Source of `virtual:pracht/islands-client` — the tiny bootstrap loaded by
|
|
1508
|
+
* `hydration: "islands"` routes. It deliberately does NOT import the app
|
|
1509
|
+
* manifest, the router, or the full client runtime: it only scans the DOM
|
|
1510
|
+
* for island markers and hydrates the islands present on the page.
|
|
1511
|
+
*/
|
|
1512
|
+
function createPrachtIslandsClientModuleSource(options = {}) {
|
|
1513
|
+
const islandsGlob = `${resolveOptions(options).islandsDir}/**/*.{ts,tsx,js,jsx}`;
|
|
1514
|
+
return [
|
|
1515
|
+
"import { hydrateIslands } from \"@pracht/core/islands-client\";",
|
|
1516
|
+
"",
|
|
1517
|
+
`const islandModules = import.meta.glob(${JSON.stringify(islandsGlob)});`,
|
|
1518
|
+
"",
|
|
1519
|
+
"hydrateIslands({ modules: islandModules });",
|
|
1520
|
+
""
|
|
1521
|
+
].join("\n");
|
|
1522
|
+
}
|
|
1052
1523
|
function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
1053
1524
|
const resolved = resolveOptions(options);
|
|
1054
1525
|
const isPagesMode = !!resolved.pagesDir;
|
|
@@ -1056,26 +1527,41 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
|
1056
1527
|
const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
|
|
1057
1528
|
const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root) : {
|
|
1058
1529
|
clientEntryUrl: null,
|
|
1530
|
+
islandsEntryUrl: null,
|
|
1059
1531
|
cssManifest: {},
|
|
1060
1532
|
jsManifest: {}
|
|
1061
1533
|
};
|
|
1062
1534
|
const adapter = resolved.adapter;
|
|
1535
|
+
const prachtImports = adapter?.serverImports ? adapter.serverImports + "\nimport { prerenderApp } from \"@pracht/core/server\";" : "import { resolveApp, resolveApiRoutes, prerenderApp } from \"@pracht/core/server\";";
|
|
1536
|
+
const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
|
|
1537
|
+
const islandsEntryUrl = buildOptions.isBuild ? clientBuild.islandsEntryUrl : ISLANDS_CLIENT_BROWSER_PATH;
|
|
1538
|
+
const islandsGlob = `${resolved.islandsDir}/**/*.{ts,tsx,js,jsx}`;
|
|
1063
1539
|
const source = [
|
|
1064
|
-
|
|
1065
|
-
|
|
1540
|
+
prachtImports,
|
|
1541
|
+
"import { registerServerIslands, setIslandsClientEntryUrl } from \"@pracht/core/server\";",
|
|
1542
|
+
appImport,
|
|
1066
1543
|
"",
|
|
1067
1544
|
`const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
|
|
1068
1545
|
...createApplyRouteLoaderHintsSource(),
|
|
1069
1546
|
registrySource,
|
|
1070
1547
|
"",
|
|
1548
|
+
"// Islands are registered eagerly so the server renderer can detect their",
|
|
1549
|
+
"// vnodes during islands-mode renders.",
|
|
1550
|
+
`const islandModules = import.meta.glob(${JSON.stringify(islandsGlob)}, { eager: true });`,
|
|
1551
|
+
"registerServerIslands(islandModules);",
|
|
1552
|
+
`setIslandsClientEntryUrl(${JSON.stringify(islandsEntryUrl ?? void 0)});`,
|
|
1553
|
+
"export const islandFiles = Object.keys(islandModules);",
|
|
1554
|
+
"",
|
|
1071
1555
|
"export const resolvedApp = resolveApp(app);",
|
|
1072
1556
|
"applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
|
|
1073
1557
|
`export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
|
|
1074
1558
|
`export const buildTarget = ${JSON.stringify(adapter?.id ?? "node")};`,
|
|
1075
1559
|
`export const clientEntryUrl = ${JSON.stringify(clientBuild.clientEntryUrl ?? "/@pracht/client.js")};`,
|
|
1560
|
+
`export const islandsEntryUrl = ${JSON.stringify(islandsEntryUrl ?? null)};`,
|
|
1076
1561
|
`export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
|
|
1077
1562
|
`export const jsManifest = ${JSON.stringify(clientBuild.jsManifest)};`,
|
|
1078
1563
|
`export const prerenderConcurrency = ${JSON.stringify(resolved.prerenderConcurrency)};`,
|
|
1564
|
+
`export const budgets = ${JSON.stringify(resolved.budgets)};`,
|
|
1079
1565
|
"export { prerenderApp };",
|
|
1080
1566
|
""
|
|
1081
1567
|
];
|
|
@@ -1167,19 +1653,36 @@ function generatePagesAppInlineSource(options, root = process.cwd()) {
|
|
|
1167
1653
|
//#region src/plugin-dev-ssr.ts
|
|
1168
1654
|
const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
|
|
1169
1655
|
const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
|
|
1656
|
+
const DEVTOOLS_JSON_PATH = "/_pracht.json";
|
|
1170
1657
|
function createDevSSRMiddleware(server, options = {}) {
|
|
1171
1658
|
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
1659
|
+
let warnedDevtoolsCollision = false;
|
|
1172
1660
|
return async (req, res, next) => {
|
|
1173
1661
|
const url = req.url ?? "/";
|
|
1174
1662
|
const requestUrl = new URL(url, "http://localhost");
|
|
1175
1663
|
try {
|
|
1176
1664
|
const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core/server"), server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)]);
|
|
1177
|
-
|
|
1665
|
+
const routeMatchers = {
|
|
1178
1666
|
app: serverMod.resolvedApp,
|
|
1179
1667
|
apiRoutes: serverMod.apiRoutes,
|
|
1180
1668
|
matchApiRoute: framework.matchApiRoute,
|
|
1181
1669
|
matchAppRoute: framework.matchAppRoute
|
|
1182
|
-
}
|
|
1670
|
+
};
|
|
1671
|
+
if (requestUrl.pathname === "/_pracht" || requestUrl.pathname === "/_pracht.json") {
|
|
1672
|
+
if (!warnedDevtoolsCollision && matchesResolvedRoute(requestUrl.pathname, routeMatchers)) {
|
|
1673
|
+
warnedDevtoolsCollision = true;
|
|
1674
|
+
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.`);
|
|
1675
|
+
}
|
|
1676
|
+
await serveDevtools(server, res, {
|
|
1677
|
+
apiRoutes: serverMod.apiRoutes ?? [],
|
|
1678
|
+
app: serverMod.resolvedApp,
|
|
1679
|
+
url,
|
|
1680
|
+
wantsJson: requestUrl.pathname === DEVTOOLS_JSON_PATH
|
|
1681
|
+
});
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (shouldBypassDevSSR(requestUrl, req, routeMatchers)) return next();
|
|
1685
|
+
if (isDevNotFoundRequest(requestUrl, req, routeMatchers)) return serveDevNotFound(server, res, next, url, requestUrl.pathname, routeMatchers);
|
|
1183
1686
|
let webRequest;
|
|
1184
1687
|
try {
|
|
1185
1688
|
webRequest = await nodeToWebRequest(req, maxBodySize);
|
|
@@ -1191,15 +1694,17 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
1191
1694
|
}
|
|
1192
1695
|
throw err;
|
|
1193
1696
|
}
|
|
1697
|
+
const timings = {};
|
|
1194
1698
|
const response = await framework.handlePrachtRequest({
|
|
1195
1699
|
app: serverMod.resolvedApp,
|
|
1196
1700
|
registry: serverMod.registry,
|
|
1197
1701
|
request: webRequest,
|
|
1198
1702
|
debugErrors: true,
|
|
1199
1703
|
clientEntryUrl: CLIENT_BROWSER_PATH,
|
|
1200
|
-
apiRoutes: serverMod.apiRoutes
|
|
1704
|
+
apiRoutes: serverMod.apiRoutes,
|
|
1705
|
+
timings
|
|
1201
1706
|
});
|
|
1202
|
-
if (response.status === 404) return next();
|
|
1707
|
+
if (response.status === 404 && !routeMatchers.app?.notFound) return next();
|
|
1203
1708
|
const contentType = response.headers.get("content-type") ?? "text/html";
|
|
1204
1709
|
let body = await response.text();
|
|
1205
1710
|
if (contentType.includes("text/html")) body = await server.transformIndexHtml(url, body);
|
|
@@ -1207,12 +1712,38 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
1207
1712
|
response.headers.forEach((value, key) => {
|
|
1208
1713
|
res.setHeader(key, value);
|
|
1209
1714
|
});
|
|
1715
|
+
const serverTiming = framework.formatServerTimingHeader(timings);
|
|
1716
|
+
if (serverTiming) res.setHeader("Server-Timing", serverTiming);
|
|
1210
1717
|
res.end(body);
|
|
1211
1718
|
} catch (error) {
|
|
1212
1719
|
await handleDevError(server, req, res, next, url, error);
|
|
1213
1720
|
}
|
|
1214
1721
|
};
|
|
1215
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Serve the dev-only `/_pracht` devtools page (or `/_pracht.json`) built from
|
|
1725
|
+
* the same resolved app graph that `pracht inspect` reports.
|
|
1726
|
+
*/
|
|
1727
|
+
async function serveDevtools(server, res, options) {
|
|
1728
|
+
const devtools = await server.ssrLoadModule("@pracht/core/devtools");
|
|
1729
|
+
const graph = await devtools.buildAppGraph({
|
|
1730
|
+
apiRoutes: options.apiRoutes,
|
|
1731
|
+
app: options.app,
|
|
1732
|
+
loadModule: (file) => server.ssrLoadModule(file),
|
|
1733
|
+
readSource: (file) => readFileSync(resolve(server.config.root, `.${file}`), "utf-8")
|
|
1734
|
+
});
|
|
1735
|
+
if (options.wantsJson) {
|
|
1736
|
+
res.statusCode = 200;
|
|
1737
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1738
|
+
res.end(JSON.stringify(graph, null, 2));
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
let html = devtools.buildDevtoolsHtml(graph);
|
|
1742
|
+
html = await server.transformIndexHtml(options.url, html);
|
|
1743
|
+
res.statusCode = 200;
|
|
1744
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
1745
|
+
res.end(html);
|
|
1746
|
+
}
|
|
1216
1747
|
async function handleDevError(server, req, res, next, url, error) {
|
|
1217
1748
|
if (error instanceof Error) server.ssrFixStacktrace(error);
|
|
1218
1749
|
if (req.headers["x-pracht-route-state-request"] === "1") {
|
|
@@ -1229,7 +1760,8 @@ async function handleDevError(server, req, res, next, url, error) {
|
|
|
1229
1760
|
const { buildErrorOverlayHtml } = await server.ssrLoadModule("@pracht/core/error-overlay");
|
|
1230
1761
|
let html = buildErrorOverlayHtml({
|
|
1231
1762
|
message: error instanceof Error ? error.message : String(error),
|
|
1232
|
-
stack: error instanceof Error ? error.stack : void 0
|
|
1763
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1764
|
+
root: server.config.root
|
|
1233
1765
|
});
|
|
1234
1766
|
html = await server.transformIndexHtml(url, html);
|
|
1235
1767
|
res.statusCode = 500;
|
|
@@ -1239,6 +1771,44 @@ async function handleDevError(server, req, res, next, url, error) {
|
|
|
1239
1771
|
next(error);
|
|
1240
1772
|
}
|
|
1241
1773
|
}
|
|
1774
|
+
/**
|
|
1775
|
+
* True when a GET/HEAD document request matches no page route and no API
|
|
1776
|
+
* route — the dev middleware then serves the rich dev-only 404 page instead
|
|
1777
|
+
* of falling through to Vite. Route-state (JSON) requests and non-document
|
|
1778
|
+
* fetches keep their existing 404 behavior.
|
|
1779
|
+
*
|
|
1780
|
+
* Apps that declare a `notFound` page own their 404s: dev renders that page
|
|
1781
|
+
* (exactly as production does) rather than the framework's route table.
|
|
1782
|
+
*/
|
|
1783
|
+
function isDevNotFoundRequest(requestUrl, req, options = {}) {
|
|
1784
|
+
const url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
|
|
1785
|
+
if (options.app?.notFound) return false;
|
|
1786
|
+
if (isRouteStateRequest(url, req)) return false;
|
|
1787
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
1788
|
+
if (method !== "GET" && method !== "HEAD") return false;
|
|
1789
|
+
const accept = readRequestHeader(req.headers.accept).toLowerCase();
|
|
1790
|
+
if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) return false;
|
|
1791
|
+
return !matchesResolvedRoute(url.pathname, options);
|
|
1792
|
+
}
|
|
1793
|
+
async function serveDevNotFound(server, res, next, url, pathname, options) {
|
|
1794
|
+
try {
|
|
1795
|
+
const { buildDevNotFoundHtml } = await server.ssrLoadModule("@pracht/core/dev-404");
|
|
1796
|
+
let html = buildDevNotFoundHtml({
|
|
1797
|
+
apiRoutes: options.apiRoutes.map((route) => ({ path: route.path })),
|
|
1798
|
+
requestedPath: pathname,
|
|
1799
|
+
routes: options.app.routes.map((route) => ({
|
|
1800
|
+
path: route.path,
|
|
1801
|
+
render: route.render ?? null
|
|
1802
|
+
}))
|
|
1803
|
+
});
|
|
1804
|
+
html = await server.transformIndexHtml(url, html);
|
|
1805
|
+
res.statusCode = 404;
|
|
1806
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
1807
|
+
res.end(html);
|
|
1808
|
+
} catch {
|
|
1809
|
+
next();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1242
1812
|
function shouldBypassDevSSR(requestUrl, req, options = {}) {
|
|
1243
1813
|
const url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
|
|
1244
1814
|
const pathname = url.pathname;
|
|
@@ -1274,7 +1844,7 @@ function hasKnownAssetExtension(pathname) {
|
|
|
1274
1844
|
return DEV_ASSET_EXTENSIONS.has(extension);
|
|
1275
1845
|
}
|
|
1276
1846
|
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_");
|
|
1847
|
+
return pathname === "/@pracht/client.js" || pathname === "/@pracht/islands.js" || pathname === "/@vite/client" || pathname === "/@react-refresh" || pathname.startsWith("/@vite/") || pathname.startsWith("/@id/") || pathname.startsWith("/@fs/") || pathname.startsWith("/__vite_");
|
|
1278
1848
|
}
|
|
1279
1849
|
const NON_DOCUMENT_FETCH_DESTINATIONS = new Set([
|
|
1280
1850
|
"audio",
|
|
@@ -1361,12 +1931,24 @@ function pracht(options = {}) {
|
|
|
1361
1931
|
config(_config, env) {
|
|
1362
1932
|
const isEdge = resolved.adapter.edge === true;
|
|
1363
1933
|
const isSSRBuild = env.isSsrBuild;
|
|
1934
|
+
const configRoot = _config.root ?? process.cwd();
|
|
1935
|
+
const wantsIslandsEntry = env.command === "build" && !isSSRBuild && existsSync(resolveConfigPath(configRoot, resolved.islandsDir));
|
|
1364
1936
|
return {
|
|
1365
1937
|
appType: "custom",
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1938
|
+
envPrefix: ["VITE_", PUBLIC_ENV_PREFIX],
|
|
1939
|
+
...isSSRBuild ? {} : { build: { rollupOptions: {
|
|
1940
|
+
...wantsIslandsEntry ? { input: [PRACHT_ISLANDS_CLIENT_MODULE_ID] } : {},
|
|
1941
|
+
output: { manualChunks(id) {
|
|
1942
|
+
if (id.includes("node_modules/preact") || id.includes("node_modules/preact-suspense")) return "vendor";
|
|
1943
|
+
} }
|
|
1944
|
+
} } },
|
|
1945
|
+
...isEdge && isSSRBuild ? {
|
|
1946
|
+
ssr: {
|
|
1947
|
+
noExternal: true,
|
|
1948
|
+
target: "webworker"
|
|
1949
|
+
},
|
|
1950
|
+
build: { rollupOptions: { external: [/^cloudflare:/] } }
|
|
1951
|
+
} : {}
|
|
1370
1952
|
};
|
|
1371
1953
|
},
|
|
1372
1954
|
configResolved(config) {
|
|
@@ -1374,12 +1956,15 @@ function pracht(options = {}) {
|
|
|
1374
1956
|
isBuild = config.command === "build";
|
|
1375
1957
|
routeFileDirs = computeRouteFileDirs(root, resolved);
|
|
1376
1958
|
},
|
|
1377
|
-
resolveId(id) {
|
|
1959
|
+
resolveId(id, importer, resolveIdOptions) {
|
|
1960
|
+
if (isIslandsClientModule(id)) return PRACHT_ISLANDS_CLIENT_MODULE_ID;
|
|
1378
1961
|
if (isClientModule(id)) return PRACHT_CLIENT_MODULE_ID;
|
|
1379
1962
|
if (isServerModule(id)) return PRACHT_SERVER_MODULE_ID;
|
|
1963
|
+
if (id === "@pracht/core/env/server" && !resolveIdOptions?.ssr && !resolveIdOptions?.scan) throw new Error(`[pracht] ${JSON.stringify(SERVER_ENV_MODULE_ID)} was imported by ${JSON.stringify(importer ?? "unknown module")} in client code. serverEnv is server-only — read it inside loaders, middleware, or API routes, or use publicEnv (PRACHT_PUBLIC_-prefixed variables) from "@pracht/core" instead.`);
|
|
1380
1964
|
return null;
|
|
1381
1965
|
},
|
|
1382
1966
|
load(id) {
|
|
1967
|
+
if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved);
|
|
1383
1968
|
if (isClientModule(id)) return createPrachtClientModuleSource(resolved, { root });
|
|
1384
1969
|
if (isServerModule(id)) return createPrachtServerModuleSource(resolved, {
|
|
1385
1970
|
root,
|
|
@@ -1422,7 +2007,8 @@ function pracht(options = {}) {
|
|
|
1422
2007
|
resolved.shellsDir,
|
|
1423
2008
|
resolved.middlewareDir,
|
|
1424
2009
|
resolved.apiDir,
|
|
1425
|
-
resolved.serverDir
|
|
2010
|
+
resolved.serverDir,
|
|
2011
|
+
resolved.islandsDir
|
|
1426
2012
|
].some((dir) => relative.startsWith(dir))) {
|
|
1427
2013
|
const serverMod = server.moduleGraph.getModuleById(PRACHT_SERVER_MODULE_ID);
|
|
1428
2014
|
if (serverMod) server.moduleGraph.invalidateModule(serverMod);
|
|
@@ -1430,6 +2016,10 @@ function pracht(options = {}) {
|
|
|
1430
2016
|
const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
|
|
1431
2017
|
if (clientMod) server.moduleGraph.invalidateModule(clientMod);
|
|
1432
2018
|
}
|
|
2019
|
+
if (relative.startsWith(resolved.islandsDir)) {
|
|
2020
|
+
const islandsMod = server.moduleGraph.getModuleById(PRACHT_ISLANDS_CLIENT_MODULE_ID);
|
|
2021
|
+
if (islandsMod) server.moduleGraph.invalidateModule(islandsMod);
|
|
2022
|
+
}
|
|
1433
2023
|
}
|
|
1434
2024
|
}
|
|
1435
2025
|
};
|
|
@@ -1450,7 +2040,7 @@ function pracht(options = {}) {
|
|
|
1450
2040
|
name: "pracht:optimize-deps-entries",
|
|
1451
2041
|
enforce: "post",
|
|
1452
2042
|
config(config) {
|
|
1453
|
-
return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved));
|
|
2043
|
+
return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved), createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
|
|
1454
2044
|
}
|
|
1455
2045
|
};
|
|
1456
2046
|
const precompilePlugin = resolved.precompileSsrJsx ? preactSsrPrecompile({
|
|
@@ -1461,7 +2051,8 @@ function pracht(options = {}) {
|
|
|
1461
2051
|
...precompilePlugin ? [precompilePlugin] : [],
|
|
1462
2052
|
...preact(),
|
|
1463
2053
|
prachtPlugin,
|
|
1464
|
-
clientModuleTransformPlugin
|
|
2054
|
+
clientModuleTransformPlugin,
|
|
2055
|
+
createEnvSafetyPlugin(resolved.envSafety)
|
|
1465
2056
|
];
|
|
1466
2057
|
const adapterPlugins = resolved.adapter.vitePlugins?.();
|
|
1467
2058
|
if (adapterPlugins?.length) plugins.push(...adapterPlugins);
|
|
@@ -1481,10 +2072,27 @@ function rewriteManifestCoreImports(code) {
|
|
|
1481
2072
|
return `import ${typeKeyword ?? ""}{${specifiers}} from ${quote}@pracht/core/manifest${quote}`;
|
|
1482
2073
|
});
|
|
1483
2074
|
}
|
|
1484
|
-
|
|
2075
|
+
const PRACHT_OPTIMIZE_DEPS_INCLUDE = [
|
|
2076
|
+
"@pracht/core",
|
|
2077
|
+
"@pracht/core/client",
|
|
2078
|
+
"@pracht/core/islands-client",
|
|
2079
|
+
"@pracht/core/manifest"
|
|
2080
|
+
];
|
|
2081
|
+
function createPrachtOptimizeDepsInclude(root) {
|
|
2082
|
+
try {
|
|
2083
|
+
if (!toPosixPath(createRequire(join(root, "package.json")).resolve("@pracht/core/package.json")).includes("/node_modules/")) return [];
|
|
2084
|
+
return PRACHT_OPTIMIZE_DEPS_INCLUDE;
|
|
2085
|
+
} catch {
|
|
2086
|
+
return [];
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function withPrachtOptimizeDepsEntries(config, prachtEntries, prachtInclude) {
|
|
1485
2090
|
const environments = Object.fromEntries(Object.entries(config.environments ?? {}).map(([name, environment]) => [name, { optimizeDeps: { entries: mergeOptimizeDepsEntries(environment.optimizeDeps?.entries, prachtEntries) } }]));
|
|
1486
2091
|
return {
|
|
1487
|
-
optimizeDeps: {
|
|
2092
|
+
optimizeDeps: {
|
|
2093
|
+
entries: mergeOptimizeDepsEntries(config.optimizeDeps?.entries, prachtEntries),
|
|
2094
|
+
...prachtInclude.length > 0 ? { include: mergeOptimizeDepsEntries(config.optimizeDeps?.include, prachtInclude) } : {}
|
|
2095
|
+
},
|
|
1488
2096
|
...Object.keys(environments).length > 0 ? { environments } : {}
|
|
1489
2097
|
};
|
|
1490
2098
|
}
|
|
@@ -1495,14 +2103,16 @@ function createPrachtOptimizeDepsEntries(resolved) {
|
|
|
1495
2103
|
`${toOptimizeDepsEntry(resolved.pagesDir)}/**/*.${routeExtensions}`,
|
|
1496
2104
|
`${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
|
|
1497
2105
|
`${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
1498
|
-
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}
|
|
2106
|
+
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
2107
|
+
`${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}`
|
|
1499
2108
|
] : [
|
|
1500
2109
|
toOptimizeDepsEntry(resolved.appFile),
|
|
1501
2110
|
`${toOptimizeDepsEntry(resolved.routesDir)}/**/*.${routeExtensions}`,
|
|
1502
2111
|
`${toOptimizeDepsEntry(resolved.shellsDir)}/**/*.${routeExtensions}`,
|
|
1503
2112
|
`${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
|
|
1504
2113
|
`${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
1505
|
-
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}
|
|
2114
|
+
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
2115
|
+
`${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}`
|
|
1506
2116
|
];
|
|
1507
2117
|
return [...new Set(entries.filter(Boolean))];
|
|
1508
2118
|
}
|
|
@@ -1570,4 +2180,4 @@ function withTrailingSep(p) {
|
|
|
1570
2180
|
return p.endsWith("/") ? p : `${p}/`;
|
|
1571
2181
|
}
|
|
1572
2182
|
//#endregion
|
|
1573
|
-
export { PRACHT_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, createPrachtClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, pracht };
|
|
2183
|
+
export { PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PUBLIC_ENV_PREFIX, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
|
|
@@ -107,6 +107,7 @@ function scan(dir, root, pages) {
|
|
|
107
107
|
const routePath = filePathToRoutePath(rel);
|
|
108
108
|
const source = readFileSync(abs, "utf-8");
|
|
109
109
|
const renderMode = extractRenderMode(source);
|
|
110
|
+
const hydrationMode = extractHydrationMode(source);
|
|
110
111
|
const hasLoader = detectLoaderExport(source);
|
|
111
112
|
pages.push({
|
|
112
113
|
absolutePath: abs,
|
|
@@ -116,6 +117,7 @@ function scan(dir, root, pages) {
|
|
|
116
117
|
isCatchAll: routePath.split("/").includes("*"),
|
|
117
118
|
isDynamic: routePath.split("/").some((segment) => segment.startsWith(":")),
|
|
118
119
|
renderMode,
|
|
120
|
+
hydrationMode,
|
|
119
121
|
hasLoader
|
|
120
122
|
});
|
|
121
123
|
}
|
|
@@ -162,6 +164,11 @@ function extractRenderMode(source) {
|
|
|
162
164
|
const match = RENDER_MODE_RE.exec(source);
|
|
163
165
|
return match ? match[1] : void 0;
|
|
164
166
|
}
|
|
167
|
+
const HYDRATION_RE = /export\s+const\s+HYDRATION\s*=\s*["'](\w+)["']/;
|
|
168
|
+
function extractHydrationMode(source) {
|
|
169
|
+
const match = HYDRATION_RE.exec(source);
|
|
170
|
+
return match ? match[1] : void 0;
|
|
171
|
+
}
|
|
165
172
|
function generatePagesManifestSource(pages, options) {
|
|
166
173
|
const pagesDir = options.pagesDir;
|
|
167
174
|
const defaultRender = options.pagesDefaultRender ?? "ssr";
|
|
@@ -170,13 +177,21 @@ function generatePagesManifestSource(pages, options) {
|
|
|
170
177
|
const appFile = scanAllFiles(pagesDir).find((f) => basename(f, extname(f)) === "_app" && SHELL_EXTENSIONS.has(extname(f)));
|
|
171
178
|
const lines = ["import { defineApp, group, route } from \"@pracht/core/manifest\";", ""];
|
|
172
179
|
const routeEntries = [];
|
|
180
|
+
const notFoundPage = pages.find((page) => page.routePath === "/404");
|
|
173
181
|
for (const page of pages) {
|
|
182
|
+
if (page === notFoundPage) continue;
|
|
174
183
|
const render = page.renderMode ?? defaultRender;
|
|
175
184
|
const filePath = prefix ? `${prefix}/${page.relativePath.replace(/\\/g, "/")}` : `./${page.relativePath.replace(/\\/g, "/")}`;
|
|
176
185
|
const fileRef = useImport ? `() => import(${JSON.stringify(filePath)})` : JSON.stringify(filePath);
|
|
177
186
|
const metaParts = [`render: ${JSON.stringify(render)}`, `hasLoader: ${page.hasLoader ? "true" : "false"}`];
|
|
187
|
+
if (page.hydrationMode) metaParts.push(`hydration: ${JSON.stringify(page.hydrationMode)}`);
|
|
178
188
|
routeEntries.push(` route(${JSON.stringify(page.routePath)}, ${fileRef}, { ${metaParts.join(", ")} })`);
|
|
179
189
|
}
|
|
190
|
+
const notFoundEntry = notFoundPage ? buildNotFoundEntry(notFoundPage, {
|
|
191
|
+
prefix,
|
|
192
|
+
useImport,
|
|
193
|
+
withShell: !!appFile
|
|
194
|
+
}) : null;
|
|
180
195
|
if (appFile) {
|
|
181
196
|
const appPath = prefix ? `${prefix}/_app.${extname(appFile).slice(1)}` : `./${relative(join(pagesDir, ".."), appFile).replace(/\\/g, "/")}`;
|
|
182
197
|
const shellRef = useImport ? `() => import(${JSON.stringify(appPath)})` : JSON.stringify(appPath);
|
|
@@ -189,17 +204,26 @@ function generatePagesManifestSource(pages, options) {
|
|
|
189
204
|
lines.push(routeEntries.join(",\n"));
|
|
190
205
|
lines.push(" ]),");
|
|
191
206
|
lines.push(" ],");
|
|
207
|
+
if (notFoundEntry) lines.push(notFoundEntry);
|
|
192
208
|
lines.push("});");
|
|
193
209
|
} else {
|
|
194
210
|
lines.push("const app = defineApp({");
|
|
195
211
|
lines.push(" routes: [");
|
|
196
212
|
lines.push(routeEntries.join(",\n"));
|
|
197
213
|
lines.push(" ],");
|
|
214
|
+
if (notFoundEntry) lines.push(notFoundEntry);
|
|
198
215
|
lines.push("});");
|
|
199
216
|
}
|
|
200
217
|
lines.push("");
|
|
201
218
|
return lines.join("\n");
|
|
202
219
|
}
|
|
220
|
+
function buildNotFoundEntry(page, options) {
|
|
221
|
+
const filePath = options.prefix ? `${options.prefix}/${page.relativePath.replace(/\\/g, "/")}` : `./${page.relativePath.replace(/\\/g, "/")}`;
|
|
222
|
+
const configParts = [`component: ${options.useImport ? `() => import(${JSON.stringify(filePath)})` : JSON.stringify(filePath)}`];
|
|
223
|
+
if (options.withShell) configParts.push("shell: \"pages\"");
|
|
224
|
+
if (page.hydrationMode) configParts.push(`hydration: ${JSON.stringify(page.hydrationMode)}`);
|
|
225
|
+
return ` notFound: { ${configParts.join(", ")} },`;
|
|
226
|
+
}
|
|
203
227
|
function scanAllFiles(dir) {
|
|
204
228
|
const results = [];
|
|
205
229
|
let entries;
|
package/dist/pages-router.d.mts
CHANGED
package/dist/pages-router.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-
|
|
1
|
+
import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-BsVlzz-e.mjs";
|
|
2
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.
|
|
3
|
+
"version": "0.6.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.
|
|
48
|
-
"@pracht/
|
|
49
|
-
"@pracht/
|
|
47
|
+
"@pracht/adapter-node": "0.3.0",
|
|
48
|
+
"@pracht/preact-ssr-precompile": "0.1.2",
|
|
49
|
+
"@pracht/core": "0.10.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"vite": "^8.0.0"
|