@pracht/vite-plugin 0.5.0 → 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;
|
|
@@ -78,12 +118,26 @@ interface PrachtPluginOptions {
|
|
|
78
118
|
* Client bundles keep the normal Preact JSX transform for hydration.
|
|
79
119
|
*/
|
|
80
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;
|
|
81
128
|
}
|
|
82
129
|
//#endregion
|
|
83
130
|
//#region src/plugin-codegen.d.ts
|
|
84
131
|
declare function createPrachtClientModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
85
132
|
root?: string;
|
|
86
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;
|
|
87
141
|
declare function createPrachtServerModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
88
142
|
root?: string;
|
|
89
143
|
isBuild?: boolean;
|
|
@@ -93,4 +147,4 @@ declare function createPrachtRegistryModuleSource(options?: PrachtPluginOptions)
|
|
|
93
147
|
//#region src/index.d.ts
|
|
94
148
|
declare function pracht(options?: PrachtPluginOptions): Plugin[];
|
|
95
149
|
//#endregion
|
|
96
|
-
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,10 +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
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
|
|
4
4
|
import preact from "@preact/preset-vite";
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
6
6
|
import { parseAst } from "vite";
|
|
7
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
8
|
import { createNodeServerEntryModule } from "@pracht/adapter-node";
|
|
9
9
|
//#region src/client-module-query.ts
|
|
10
10
|
const CLIENT_MODULE_QUERY = "pracht-client";
|
|
@@ -888,20 +888,323 @@ function enqueueDependencies(target, dependencies) {
|
|
|
888
888
|
for (const name of dependencies) target.add(name);
|
|
889
889
|
}
|
|
890
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
|
|
891
1190
|
//#region src/plugin-assets.ts
|
|
892
1191
|
const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
|
|
893
1192
|
const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
|
|
1193
|
+
const PRACHT_ISLANDS_CLIENT_MODULE_ID = "virtual:pracht/islands-client";
|
|
894
1194
|
const CLIENT_BROWSER_PATH = "/@pracht/client.js";
|
|
1195
|
+
const ISLANDS_CLIENT_BROWSER_PATH = "/@pracht/islands.js";
|
|
895
1196
|
function readClientBuildAssets(root = process.cwd()) {
|
|
896
1197
|
const manifestPath = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"].map((candidate) => resolve(root, candidate)).find((candidate) => existsSync(candidate));
|
|
897
1198
|
if (!manifestPath) return {
|
|
898
1199
|
clientEntryUrl: null,
|
|
1200
|
+
islandsEntryUrl: null,
|
|
899
1201
|
cssManifest: {},
|
|
900
1202
|
jsManifest: {}
|
|
901
1203
|
};
|
|
902
1204
|
const rawManifest = readFileSync(manifestPath, "utf-8");
|
|
903
1205
|
const manifest = JSON.parse(rawManifest);
|
|
904
1206
|
const clientEntry = manifest[PRACHT_CLIENT_MODULE_ID];
|
|
1207
|
+
const islandsEntry = manifest[PRACHT_ISLANDS_CLIENT_MODULE_ID];
|
|
905
1208
|
const cssManifest = {};
|
|
906
1209
|
const jsManifest = {};
|
|
907
1210
|
for (const [key, entry] of Object.entries(manifest)) {
|
|
@@ -911,12 +1214,20 @@ function readClientBuildAssets(root = process.cwd()) {
|
|
|
911
1214
|
if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((f) => `/${f}`);
|
|
912
1215
|
if (deps.js.length > 0) jsManifest[manifestKey] = deps.js.map((f) => `/${f}`);
|
|
913
1216
|
}
|
|
1217
|
+
addEntryDeps(manifest, jsManifest, PRACHT_CLIENT_MODULE_ID, clientEntry);
|
|
1218
|
+
addEntryDeps(manifest, jsManifest, PRACHT_ISLANDS_CLIENT_MODULE_ID, islandsEntry);
|
|
914
1219
|
return {
|
|
915
1220
|
clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
|
|
1221
|
+
islandsEntryUrl: islandsEntry ? `/${islandsEntry.file}` : null,
|
|
916
1222
|
cssManifest,
|
|
917
1223
|
jsManifest
|
|
918
1224
|
};
|
|
919
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
|
+
}
|
|
920
1231
|
function collectTransitiveDeps(manifest, key) {
|
|
921
1232
|
const css = /* @__PURE__ */ new Set();
|
|
922
1233
|
const js = /* @__PURE__ */ new Set();
|
|
@@ -942,6 +1253,9 @@ function isClientModule(id) {
|
|
|
942
1253
|
function isServerModule(id) {
|
|
943
1254
|
return id === "virtual:pracht/server" || id.endsWith("virtual:pracht/server");
|
|
944
1255
|
}
|
|
1256
|
+
function isIslandsClientModule(id) {
|
|
1257
|
+
return id === "virtual:pracht/islands-client" || id === "/@pracht/islands.js" || id.endsWith("virtual:pracht/islands-client");
|
|
1258
|
+
}
|
|
945
1259
|
//#endregion
|
|
946
1260
|
//#region src/plugin-adapter.ts
|
|
947
1261
|
function createDefaultNodeAdapter() {
|
|
@@ -962,13 +1276,15 @@ const DEFAULTS = {
|
|
|
962
1276
|
shellsDir: "/src/shells",
|
|
963
1277
|
apiDir: "/src/api",
|
|
964
1278
|
serverDir: "/src/server",
|
|
1279
|
+
islandsDir: "/src/islands",
|
|
965
1280
|
adapter: createDefaultNodeAdapter(),
|
|
966
1281
|
pagesDir: "",
|
|
967
1282
|
pagesDefaultRender: "ssr",
|
|
968
1283
|
prerenderConcurrency: 10,
|
|
969
1284
|
maxBodySize: 1024 * 1024,
|
|
970
1285
|
budgets: {},
|
|
971
|
-
precompileSsrJsx: false
|
|
1286
|
+
precompileSsrJsx: false,
|
|
1287
|
+
envSafety: {}
|
|
972
1288
|
};
|
|
973
1289
|
function resolveOptions(options) {
|
|
974
1290
|
const resolved = {
|
|
@@ -990,6 +1306,102 @@ function validateBudgets(budgets) {
|
|
|
990
1306
|
}
|
|
991
1307
|
//#endregion
|
|
992
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
|
+
}
|
|
993
1405
|
function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
994
1406
|
const resolved = resolveOptions(options);
|
|
995
1407
|
const isPagesMode = !!resolved.pagesDir;
|
|
@@ -998,16 +1410,21 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
998
1410
|
const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
|
|
999
1411
|
const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
|
|
1000
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;
|
|
1001
1416
|
const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
|
|
1002
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(/\/[^/]*$/, "") || "/";
|
|
1003
1420
|
return [
|
|
1004
1421
|
"import { resolveApp, initClientRouter, readHydrationState } from \"@pracht/core/client\";",
|
|
1005
1422
|
appImport,
|
|
1006
1423
|
"",
|
|
1007
1424
|
`const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
|
|
1008
1425
|
`const routeModules = {`,
|
|
1009
|
-
` ...import.meta.glob(${JSON.stringify(
|
|
1010
|
-
` ...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)}),`,
|
|
1011
1428
|
`};`,
|
|
1012
1429
|
`const shellModules = {`,
|
|
1013
1430
|
` ...import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
|
|
@@ -1018,8 +1435,22 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1018
1435
|
"applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
|
|
1019
1436
|
"",
|
|
1020
1437
|
...createApplyRouteLoaderHintsSource(),
|
|
1021
|
-
|
|
1022
|
-
"
|
|
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(\"/\");",
|
|
1023
1454
|
"}",
|
|
1024
1455
|
"",
|
|
1025
1456
|
"const moduleKeyIndexes = new WeakMap();",
|
|
@@ -1027,22 +1458,34 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1027
1458
|
" let index = moduleKeyIndexes.get(modules);",
|
|
1028
1459
|
" if (index) return index;",
|
|
1029
1460
|
" index = new Map();",
|
|
1030
|
-
" for (const key of Object.keys(modules))
|
|
1031
|
-
" const normalized = normalizeModuleKey(key);",
|
|
1032
|
-
" if (!normalized) continue;",
|
|
1033
|
-
" if (!index.has(normalized)) index.set(normalized, key);",
|
|
1034
|
-
" for (let i = normalized.indexOf(\"/\"); i !== -1; i = normalized.indexOf(\"/\", i + 1)) {",
|
|
1035
|
-
" const suffix = normalized.slice(i + 1);",
|
|
1036
|
-
" if (suffix && !index.has(suffix)) index.set(suffix, key);",
|
|
1037
|
-
" }",
|
|
1038
|
-
" }",
|
|
1461
|
+
" for (const key of Object.keys(modules)) index.set(canonicalModuleKey(key), key);",
|
|
1039
1462
|
" moduleKeyIndexes.set(modules, index);",
|
|
1040
1463
|
" return index;",
|
|
1041
1464
|
"}",
|
|
1042
1465
|
"",
|
|
1043
1466
|
"function findModuleKey(modules, file) {",
|
|
1044
1467
|
" if (file in modules) return file;",
|
|
1045
|
-
"
|
|
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;",
|
|
1046
1489
|
"}",
|
|
1047
1490
|
"",
|
|
1048
1491
|
"const state = readHydrationState();",
|
|
@@ -1060,6 +1503,23 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1060
1503
|
""
|
|
1061
1504
|
].join("\n");
|
|
1062
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
|
+
}
|
|
1063
1523
|
function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
1064
1524
|
const resolved = resolveOptions(options);
|
|
1065
1525
|
const isPagesMode = !!resolved.pagesDir;
|
|
@@ -1067,23 +1527,37 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
|
1067
1527
|
const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
|
|
1068
1528
|
const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root) : {
|
|
1069
1529
|
clientEntryUrl: null,
|
|
1530
|
+
islandsEntryUrl: null,
|
|
1070
1531
|
cssManifest: {},
|
|
1071
1532
|
jsManifest: {}
|
|
1072
1533
|
};
|
|
1073
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}`;
|
|
1074
1539
|
const source = [
|
|
1075
|
-
|
|
1076
|
-
|
|
1540
|
+
prachtImports,
|
|
1541
|
+
"import { registerServerIslands, setIslandsClientEntryUrl } from \"@pracht/core/server\";",
|
|
1542
|
+
appImport,
|
|
1077
1543
|
"",
|
|
1078
1544
|
`const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
|
|
1079
1545
|
...createApplyRouteLoaderHintsSource(),
|
|
1080
1546
|
registrySource,
|
|
1081
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
|
+
"",
|
|
1082
1555
|
"export const resolvedApp = resolveApp(app);",
|
|
1083
1556
|
"applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
|
|
1084
1557
|
`export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
|
|
1085
1558
|
`export const buildTarget = ${JSON.stringify(adapter?.id ?? "node")};`,
|
|
1086
1559
|
`export const clientEntryUrl = ${JSON.stringify(clientBuild.clientEntryUrl ?? "/@pracht/client.js")};`,
|
|
1560
|
+
`export const islandsEntryUrl = ${JSON.stringify(islandsEntryUrl ?? null)};`,
|
|
1087
1561
|
`export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
|
|
1088
1562
|
`export const jsManifest = ${JSON.stringify(clientBuild.jsManifest)};`,
|
|
1089
1563
|
`export const prerenderConcurrency = ${JSON.stringify(resolved.prerenderConcurrency)};`,
|
|
@@ -1230,7 +1704,7 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
1230
1704
|
apiRoutes: serverMod.apiRoutes,
|
|
1231
1705
|
timings
|
|
1232
1706
|
});
|
|
1233
|
-
if (response.status === 404) return next();
|
|
1707
|
+
if (response.status === 404 && !routeMatchers.app?.notFound) return next();
|
|
1234
1708
|
const contentType = response.headers.get("content-type") ?? "text/html";
|
|
1235
1709
|
let body = await response.text();
|
|
1236
1710
|
if (contentType.includes("text/html")) body = await server.transformIndexHtml(url, body);
|
|
@@ -1302,9 +1776,13 @@ async function handleDevError(server, req, res, next, url, error) {
|
|
|
1302
1776
|
* route — the dev middleware then serves the rich dev-only 404 page instead
|
|
1303
1777
|
* of falling through to Vite. Route-state (JSON) requests and non-document
|
|
1304
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.
|
|
1305
1782
|
*/
|
|
1306
1783
|
function isDevNotFoundRequest(requestUrl, req, options = {}) {
|
|
1307
1784
|
const url = typeof requestUrl === "string" ? new URL(requestUrl, "http://localhost") : requestUrl;
|
|
1785
|
+
if (options.app?.notFound) return false;
|
|
1308
1786
|
if (isRouteStateRequest(url, req)) return false;
|
|
1309
1787
|
const method = (req.method ?? "GET").toUpperCase();
|
|
1310
1788
|
if (method !== "GET" && method !== "HEAD") return false;
|
|
@@ -1366,7 +1844,7 @@ function hasKnownAssetExtension(pathname) {
|
|
|
1366
1844
|
return DEV_ASSET_EXTENSIONS.has(extension);
|
|
1367
1845
|
}
|
|
1368
1846
|
function isReservedDevPath(pathname) {
|
|
1369
|
-
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_");
|
|
1370
1848
|
}
|
|
1371
1849
|
const NON_DOCUMENT_FETCH_DESTINATIONS = new Set([
|
|
1372
1850
|
"audio",
|
|
@@ -1453,11 +1931,17 @@ function pracht(options = {}) {
|
|
|
1453
1931
|
config(_config, env) {
|
|
1454
1932
|
const isEdge = resolved.adapter.edge === true;
|
|
1455
1933
|
const isSSRBuild = env.isSsrBuild;
|
|
1934
|
+
const configRoot = _config.root ?? process.cwd();
|
|
1935
|
+
const wantsIslandsEntry = env.command === "build" && !isSSRBuild && existsSync(resolveConfigPath(configRoot, resolved.islandsDir));
|
|
1456
1936
|
return {
|
|
1457
1937
|
appType: "custom",
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
+
} } },
|
|
1461
1945
|
...isEdge && isSSRBuild ? {
|
|
1462
1946
|
ssr: {
|
|
1463
1947
|
noExternal: true,
|
|
@@ -1472,12 +1956,15 @@ function pracht(options = {}) {
|
|
|
1472
1956
|
isBuild = config.command === "build";
|
|
1473
1957
|
routeFileDirs = computeRouteFileDirs(root, resolved);
|
|
1474
1958
|
},
|
|
1475
|
-
resolveId(id) {
|
|
1959
|
+
resolveId(id, importer, resolveIdOptions) {
|
|
1960
|
+
if (isIslandsClientModule(id)) return PRACHT_ISLANDS_CLIENT_MODULE_ID;
|
|
1476
1961
|
if (isClientModule(id)) return PRACHT_CLIENT_MODULE_ID;
|
|
1477
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.`);
|
|
1478
1964
|
return null;
|
|
1479
1965
|
},
|
|
1480
1966
|
load(id) {
|
|
1967
|
+
if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved);
|
|
1481
1968
|
if (isClientModule(id)) return createPrachtClientModuleSource(resolved, { root });
|
|
1482
1969
|
if (isServerModule(id)) return createPrachtServerModuleSource(resolved, {
|
|
1483
1970
|
root,
|
|
@@ -1520,7 +2007,8 @@ function pracht(options = {}) {
|
|
|
1520
2007
|
resolved.shellsDir,
|
|
1521
2008
|
resolved.middlewareDir,
|
|
1522
2009
|
resolved.apiDir,
|
|
1523
|
-
resolved.serverDir
|
|
2010
|
+
resolved.serverDir,
|
|
2011
|
+
resolved.islandsDir
|
|
1524
2012
|
].some((dir) => relative.startsWith(dir))) {
|
|
1525
2013
|
const serverMod = server.moduleGraph.getModuleById(PRACHT_SERVER_MODULE_ID);
|
|
1526
2014
|
if (serverMod) server.moduleGraph.invalidateModule(serverMod);
|
|
@@ -1528,6 +2016,10 @@ function pracht(options = {}) {
|
|
|
1528
2016
|
const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
|
|
1529
2017
|
if (clientMod) server.moduleGraph.invalidateModule(clientMod);
|
|
1530
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
|
+
}
|
|
1531
2023
|
}
|
|
1532
2024
|
}
|
|
1533
2025
|
};
|
|
@@ -1559,7 +2051,8 @@ function pracht(options = {}) {
|
|
|
1559
2051
|
...precompilePlugin ? [precompilePlugin] : [],
|
|
1560
2052
|
...preact(),
|
|
1561
2053
|
prachtPlugin,
|
|
1562
|
-
clientModuleTransformPlugin
|
|
2054
|
+
clientModuleTransformPlugin,
|
|
2055
|
+
createEnvSafetyPlugin(resolved.envSafety)
|
|
1563
2056
|
];
|
|
1564
2057
|
const adapterPlugins = resolved.adapter.vitePlugins?.();
|
|
1565
2058
|
if (adapterPlugins?.length) plugins.push(...adapterPlugins);
|
|
@@ -1582,6 +2075,7 @@ function rewriteManifestCoreImports(code) {
|
|
|
1582
2075
|
const PRACHT_OPTIMIZE_DEPS_INCLUDE = [
|
|
1583
2076
|
"@pracht/core",
|
|
1584
2077
|
"@pracht/core/client",
|
|
2078
|
+
"@pracht/core/islands-client",
|
|
1585
2079
|
"@pracht/core/manifest"
|
|
1586
2080
|
];
|
|
1587
2081
|
function createPrachtOptimizeDepsInclude(root) {
|
|
@@ -1609,14 +2103,16 @@ function createPrachtOptimizeDepsEntries(resolved) {
|
|
|
1609
2103
|
`${toOptimizeDepsEntry(resolved.pagesDir)}/**/*.${routeExtensions}`,
|
|
1610
2104
|
`${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
|
|
1611
2105
|
`${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
1612
|
-
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}
|
|
2106
|
+
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
2107
|
+
`${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}`
|
|
1613
2108
|
] : [
|
|
1614
2109
|
toOptimizeDepsEntry(resolved.appFile),
|
|
1615
2110
|
`${toOptimizeDepsEntry(resolved.routesDir)}/**/*.${routeExtensions}`,
|
|
1616
2111
|
`${toOptimizeDepsEntry(resolved.shellsDir)}/**/*.${routeExtensions}`,
|
|
1617
2112
|
`${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
|
|
1618
2113
|
`${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
1619
|
-
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}
|
|
2114
|
+
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
2115
|
+
`${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}`
|
|
1620
2116
|
];
|
|
1621
2117
|
return [...new Set(entries.filter(Boolean))];
|
|
1622
2118
|
}
|
|
@@ -1684,4 +2180,4 @@ function withTrailingSep(p) {
|
|
|
1684
2180
|
return p.endsWith("/") ? p : `${p}/`;
|
|
1685
2181
|
}
|
|
1686
2182
|
//#endregion
|
|
1687
|
-
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"
|