chenglou/pretext · error · Error
Built HTML not found for ${relativePath}
Error message
Built HTML not found for ${relativePath} What it means
Thrown by resolveBuiltHtmlPath in the demo-site build script. After `bun build` emits HTML for the listed entrypoints into `site/`, the script relocates each output to a pretty URL path. resolveBuiltHtmlPath checks two candidate locations — `site/<relativePath>` and `site/pages/demos/<relativePath>` — and throws if neither exists. This means `bun build` finished (exit code 0 was checked earlier) but did not emit the file the targets table expects.
Source
Thrown at scripts/build-demo-site.ts:61
]
for (let index = 0; index < targets.length; index++) {
const entry = targets[index]!
await moveBuiltHtml(entry.source, entry.target)
}
await rm(path.join(outdir, 'pages'), { recursive: true, force: true })
async function resolveBuiltHtmlPath(relativePath: string): Promise<string> {
const candidates = [
path.join(outdir, relativePath),
path.join(outdir, 'pages', 'demos', relativePath),
]
for (let index = 0; index < candidates.length; index++) {
const candidate = candidates[index]!
if (await Bun.file(candidate).exists()) return candidate
}
throw new Error(`Built HTML not found for ${relativePath}`)
}
async function moveBuiltHtml(sourceRelativePath: string, targetRelativePath: string): Promise<void> {
const sourcePath = await resolveBuiltHtmlPath(sourceRelativePath)
const targetPath = path.join(outdir, targetRelativePath)
let html = await readFile(sourcePath, 'utf8')
html = rebaseRelativeAssetUrls(html, sourcePath, targetPath)
html = rewriteDemoLinksForStaticRoot(html, targetRelativePath)
await mkdir(path.dirname(targetPath), { recursive: true })
await writeFile(targetPath, html)
if (sourcePath !== targetPath) await rm(sourcePath)
}
function rebaseRelativeAssetUrls(html: string, sourcePath: string, targetPath: string): string {
return html.replace(/\b(src|href)="([^"]+)"/g, (_match, attr: string, value: string) => {
if (!value.startsWith('.')) return `${attr}="${value}"`
View on GitHub (pinned to ac49b09b7d)
Solutions
- List `site/` and `site/pages/demos/` after the build step to see what Bun actually emitted — the discrepancy between actual and expected filenames is the bug.
- Reconcile the `entrypoints` array (lines 6-17) with the `targets` array (lines 32-43): every `source` in targets must correspond to a file bun build produces from an entrypoint.
- Run `bun build <entrypoints...> --outdir site` manually and inspect the tree before the move loop runs.
- If Bun's output layout changed, add the actual emitted path as a third candidate in resolveBuiltHtmlPath rather than special-casing callers.
- Make the error message list the candidates it tried (currently it only prints relativePath) so the mismatch is visible without a debugger.
Example fix
// before
async function resolveBuiltHtmlPath(relativePath: string): Promise<string> {
const candidates = [
path.join(outdir, relativePath),
path.join(outdir, 'pages', 'demos', relativePath),
]
for (let index = 0; index < candidates.length; index++) {
const candidate = candidates[index]!
if (await Bun.file(candidate).exists()) return candidate
}
throw new Error(`Built HTML not found for ${relativePath}`)
}
// after — list the candidates tried so the missing path is obvious
async function resolveBuiltHtmlPath(relativePath: string): Promise<string> {
const candidates = [
path.join(outdir, relativePath),
path.join(outdir, 'pages', 'demos', relativePath),
]
for (const candidate of candidates) {
if (await Bun.file(candidate).exists()) return candidate
}
throw new Error(`Built HTML not found for ${relativePath}. Looked at: ${candidates.join(', ')}`)
} Defensive patterns
Strategy: validation
Validate before calling
// Reconcile entrypoints <-> targets before the move loop runs
function reconcileBuildTargets(entrypoints: string[], targets: { source: string }[]): void {
for (const target of targets) {
const matchingEntrypoint = entrypoints.find(e => e.endsWith(target.source))
if (matchingEntrypoint === undefined) {
throw new Error(`targets references ${target.source} but no entrypoint ends with it; build will not emit it.`)
}
}
} Prevention
- Keep the entrypoints and targets arrays in lockstep: every targets[].source must correspond to an entry in entrypoints.
- After `bun build`, list the outdir tree before invoking moveBuiltHtml so a missing emit is visible.
- If Bun's output layout changes across versions, add the new actual path as another candidate in resolveBuiltHtmlPath.
- Make resolveBuiltHtmlPath's error list the candidates it tried — the current message hides where it looked.
When it happens
Trigger: The targets array (source/target pairs) drives moveBuiltHtml for each entry. Throw when both Bun.file(...).exists() checks return false. Concrete causes: an entry in `entrypoints` was renamed or deleted so `bun build` legitimately produced no output for it but the targets table still references the old name; `bun build` emits nested-path outputs (e.g. masonry/index.html) at a path that matches neither candidate; a Bun version change altered the output layout (e.g. preserving more of the input path under outdir); the file was emitted but with a different extension or case.
Common situations: A demo page was added to entrypoints but its target/source names in the targets table don't match the actual emitted filename; a directory-style entrypoint (masonry/index.html) emits to site/masonry/index.html (candidate 1) which works, but a typo in targets sends the script looking for a non-existent candidate; partial build where Bun skipped a failed entrypoint but still exited 0; cross-platform path-separator issues when constructing candidates with path.join.
Related errors
- Could not determine Unicode version from DerivedBidiClass he
- Generated bidi data is stale: ${outputPath}
AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12).
Data as JSON: /api/errors/1905b612899681b3.
Report an issue: GitHub.