@@ -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