one
Version:
One is a new React Framework that makes Vite serve both native and web.
109 lines (99 loc) • 4.48 kB
text/typescript
// a browser content blocker (safari's built-in blockers, ublock, adguard,
// brave shields) matches filter rules against the request url. in dev the
// vite module url mirrors the source path, so an ordinary app file can match
// a tracking rule purely by where it lives — easylist, easyprivacy and
// adguard all ship the plain substring rule `/analytics/pageview`, which is
// enough to refuse `src/features/site/analytics/PageviewTracker.tsx`.
//
// one blocked module fails the whole dynamic import, and the browser reports
// only `TypeError: Importing a module script failed` pointing at one's own
// route loader. nothing in that names the file, the app, or the blocker, so
// it reads as a framework or vite bug and sends people restarting dev servers
// and clearing caches.
//
// a blocked request also fails `fetch()`, so walking the route's module graph
// from the browser finds the exact module the blocker refused. dev only, and
// only after a route has already failed to load.
const MAX_MODULES = 500
const TIMEOUT_MS = 5000
// vite rewrites bare specifiers to absolute paths, so most of these are
// already root-relative; the rest resolve against the importing module.
function collectImports(source: string, base: string): string[] {
const out: string[] = []
const add = (spec: string) => {
if (!/^[./]|^https?:/.test(spec)) return
try {
out.push(new URL(spec, base).href)
} catch {}
}
for (const m of source.matchAll(/(?:^|[\s;}])(?:import|export)[^'"]{0,300}?from\s*["']([^"']+)["']/g))
add(m[1])
for (const m of source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g)) add(m[1])
for (const m of source.matchAll(/(?:^|[\s;}])import\s*["']([^"']+)["']/g)) add(m[1])
return out
}
/**
* Walks the module graph under `entryUrl` looking for a module the browser
* refuses to fetch. Returns the blocked module's url, or null when every
* module in the graph is reachable (which means the import failed for some
* other reason, such as a syntax error inside one of them).
*/
export async function findBlockedModule(entryUrl: string): Promise<string | null> {
const deadline = Date.now() + TIMEOUT_MS
const seen = new Set<string>()
let frontier = [new URL(entryUrl, window.location.href).href]
while (frontier.length && seen.size < MAX_MODULES && Date.now() < deadline) {
const batch = frontier.filter((url) => !seen.has(url)).slice(0, 32)
if (!batch.length) break
frontier = frontier.slice(batch.length)
const results = await Promise.all(
batch.map(async (url) => {
seen.add(url)
let res: Response
try {
res = await fetch(url)
} catch {
// the request never reached the server. in dev the server is
// demonstrably up (the page and one's own runtime loaded from it),
// so this is the browser refusing to make the request.
return { url, blocked: true, imports: [] as string[] }
}
if (!res.ok) return { url, blocked: false, imports: [] }
const type = res.headers.get('content-type') ?? ''
if (!type.includes('javascript')) return { url, blocked: false, imports: [] }
return { url, blocked: false, imports: collectImports(await res.text(), url) }
})
)
for (const result of results) {
if (result.blocked) return result.url
for (const next of result.imports) {
if (!seen.has(next)) frontier.push(next)
}
}
}
return null
}
/**
* Turns a failed route import into a message that names the responsible file.
* Returns null when the graph is fully reachable, leaving the original error
* as the only report.
*/
export async function diagnoseRouteLoadFailure(
routeId: string,
routeUrl: string
): Promise<string | null> {
const blocked = await findBlockedModule(routeUrl)
if (!blocked) return null
const path = blocked.replace(window.location.origin, '')
return [
`Route "${routeId}" failed to load because this browser refused to fetch ${path}`,
``,
`That request never reached the dev server, so a content blocker or privacy`,
`extension is blocking it. In dev the module URL is the source path, and a`,
`path containing a word like "analytics", "pageview" or "track" matches the`,
`tracking rules those blockers ship.`,
``,
`Rename the file or the directory so its path no longer matches, or allow`,
`${window.location.origin} in the blocker.`,
].join('\n')
}