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

  1. 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.
  2. 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.
  3. Run `bun build <entrypoints...> --outdir site` manually and inspect the tree before the move loop runs.
  4. If Bun's output layout changed, add the actual emitted path as a third candidate in resolveBuiltHtmlPath rather than special-casing callers.
  5. 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

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


AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12). Data as JSON: /api/errors/1905b612899681b3. Report an issue: GitHub.