Skip to content

Commit d7aee6f

Browse files
authored
Merge pull request #224 from JoviDeCroock/JoviDeCroock/build-time-module-keys
Resolve client module keys exactly via the build-time app dir
2 parents f370c3a + 10bbd46 commit d7aee6f

2 files changed

Lines changed: 49 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pracht/vite-plugin": patch
3+
---
4+
5+
Resolve client module keys exactly against the app manifest directory instead of runtime suffix matching. The virtual client entry previously built a suffix index over every glob key at startup and matched manifest refs by path suffix — ambiguous refs (e.g. two routes both named `index.tsx`) silently resolved to whichever key iterated first. Refs now canonicalize against the manifest file's directory (known at build time) for an exact lookup. In dev, refs that only resolve by suffix still work but log a console error explaining how to fix them; production builds resolve strictly and drop the fallback entirely.

packages/vite-plugin/src/plugin-codegen.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ export function createPrachtClientModuleSource(
160160
? `${resolved.pagesDir}/**/_app.tsrx`
161161
: `${resolved.shellsDir}/**/*.tsrx`;
162162

163+
// Base directory for relative manifest refs: the app manifest file's
164+
// directory (refs like "./routes/home.tsx" are written relative to it).
165+
const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
166+
const appFileAbs = appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`;
167+
const appDir = appFileAbs.replace(/\/[^/]*$/, "") || "/";
168+
163169
return [
164170
'import { resolveApp, initClientRouter, readHydrationState } from "@pracht/core/client";',
165171
appImport,
@@ -178,31 +184,57 @@ export function createPrachtClientModuleSource(
178184
"applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
179185
"",
180186
...createApplyRouteLoaderHintsSource(),
181-
"function normalizeModuleKey(key) {",
182-
' return key.split("?")[0].replace(/^\\.?\\//, "");',
187+
`const APP_DIR = ${JSON.stringify(appDir)};`,
188+
"",
189+
"// Manifest refs are written relative to the app manifest file",
190+
'// ("./routes/home.tsx") while import.meta.glob keys are root-absolute',
191+
'// ("/src/routes/home.tsx"). Both sides canonicalize against APP_DIR —',
192+
"// known at build time — replacing the previous runtime suffix index.",
193+
"function canonicalModuleKey(path) {",
194+
' const raw = path.split("?")[0];',
195+
' const joined = raw.startsWith("/") ? raw : APP_DIR + "/" + raw;',
196+
" const parts = [];",
197+
' for (const segment of joined.split("/")) {',
198+
' if (!segment || segment === ".") continue;',
199+
' if (segment === "..") parts.pop();',
200+
" else parts.push(segment);",
201+
" }",
202+
' return "/" + parts.join("/");',
183203
"}",
184204
"",
185205
"const moduleKeyIndexes = new WeakMap();",
186206
"function getModuleKeyIndex(modules) {",
187207
" let index = moduleKeyIndexes.get(modules);",
188208
" if (index) return index;",
189209
" index = new Map();",
190-
" for (const key of Object.keys(modules)) {",
191-
" const normalized = normalizeModuleKey(key);",
192-
" if (!normalized) continue;",
193-
" if (!index.has(normalized)) index.set(normalized, key);",
194-
' for (let i = normalized.indexOf("/"); i !== -1; i = normalized.indexOf("/", i + 1)) {',
195-
" const suffix = normalized.slice(i + 1);",
196-
" if (suffix && !index.has(suffix)) index.set(suffix, key);",
197-
" }",
198-
" }",
210+
" for (const key of Object.keys(modules)) index.set(canonicalModuleKey(key), key);",
199211
" moduleKeyIndexes.set(modules, index);",
200212
" return index;",
201213
"}",
202214
"",
203215
"function findModuleKey(modules, file) {",
204216
" if (file in modules) return file;",
205-
" return getModuleKeyIndex(modules).get(normalizeModuleKey(file)) ?? null;",
217+
" const key = getModuleKeyIndex(modules).get(canonicalModuleKey(file));",
218+
" if (key != null) return key;",
219+
" if (import.meta.env?.DEV) {",
220+
" // Dev-only lenient fallback so refs that never canonicalize (written",
221+
" // relative to a file other than the app manifest) keep working while",
222+
" // the console error tells the author to fix them — production builds",
223+
" // resolve strictly and drop this branch.",
224+
' const suffix = "/" + file.split("?")[0].replace(/^\\.?\\//, "");',
225+
" for (const candidate of Object.keys(modules)) {",
226+
" if (canonicalModuleKey(candidate).endsWith(suffix)) {",
227+
" console.error(",
228+
" `[pracht] Module ref ${JSON.stringify(file)} only resolved by suffix matching ` +",
229+
" `against ${JSON.stringify(candidate)}. Write manifest refs relative to the app ` +",
230+
' `manifest file (e.g. "./routes/home.tsx") — suffix matching is disabled in ` +',
231+
" `production builds.`,",
232+
" );",
233+
" return candidate;",
234+
" }",
235+
" }",
236+
" }",
237+
" return null;",
206238
"}",
207239
"",
208240
"const state = readHydrationState();",

0 commit comments

Comments
 (0)