Skip to content

fix(build): observe failOnWarn warnings independently of logLevel#960

Draft
spokodev wants to merge 1 commit into
rolldown:mainfrom
spokodev:fix/fail-on-warn-decoupled-from-log-level
Draft

fix(build): observe failOnWarn warnings independently of logLevel#960
spokodev wants to merge 1 commit into
rolldown:mainfrom
spokodev:fix/fail-on-warn-decoupled-from-log-level

Conversation

@spokodev
Copy link
Copy Markdown

Problem

failOnWarn: true is documented as a way to fail a build whenever a warning is emitted. In practice it relied on rolldown's defaultHandler to escalate 'warn' into a build error, but when the caller passes logLevel: 'silent' rolldown's defaultHandler becomes a no-op. So warnings such as unresolved imports get silently externalised and the build succeeds when the user explicitly opted into failing.

A common shape where this bites is CI scripts that pass logLevel: 'silent' to keep their own logs clean and failOnWarn: true to fail builds with unresolved imports. The current behaviour means those builds silently pass.

Repro

import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { build } from 'tsdown'

const dir = mkdtempSync(join(tmpdir(), 'tsdown-failonwarn-'))
writeFileSync(
  join(dir, 'package.json'),
  JSON.stringify({ type: 'module', private: true }),
)
writeFileSync(
  join(dir, 'index.ts'),
  `import express from 'express'\nexport const handler = () => express()\n`,
)

await build({
  entry: { index: join(dir, 'index.ts') },
  outDir: join(dir, 'dist'),
  format: 'esm',
  platform: 'node',
  dts: false,
  config: false,
  logLevel: 'silent',
  failOnWarn: true,
  deps: { alwaysBundle: () => true, onlyBundle: false },
})
// before: resolves, unresolved import was silently externalised
// after:  throws "Build failed with 1 error" as expected

The same call with logLevel: 'warn' already throws, so the bug only surfaces under silent.

Fix

Decouple rolldown's logLevel from tsdown's logger.level whenever failOnWarn is enabled and the user-facing log level would otherwise suppress warnings:

logLevel: logger.options?.failOnWarn
  ? logger.level === 'silent' || logger.level === 'error'
    ? 'warn'
    : logger.level
  : logger.level === 'error'
    ? 'silent'
    : logger.level,

This keeps existing behaviour for callers who do not set failOnWarn and for callers whose logLevel already allows warnings. The escalation path inside the onLog handler is unchanged.

Tests

Four direct-build tests added to tests/e2e.test.ts that bypass the testBuild helper, which is necessary because testBuild's inputOptions override forces rolldown's logLevel to 'info' and therefore could never reproduce the silent-mode path. The scenarios cover all four axes:

  • Positive: silent + failOnWarn + warning => build() throws.
  • Negative: silent + failOnWarn + clean build (no warnings) => build() succeeds.
  • Negative: silent + failOnWarn: false + warning => build() succeeds (opt-out respected).
  • Regression preservation: warn + failOnWarn + warning => build() still throws.

Without the fix the positive scenario fails on main (the build resolves silently); the other three pass on baseline already and continue to pass with the patch, confirming the fix narrowly targets the broken path.

pnpm lint clean, pnpm typecheck clean, full e2e suite green for all 5 failOnWarn cases (1 pre-existing + 4 new). The 3 pre-existing exe.test.ts failures on main are unrelated to this change.

Closes #952

Note on disclosure

Bug surfacing and the repro are from @tbeseda in the issue body. I traced the coupling in src/features/rolldown.ts, reproduced it via the script above, applied the minimal log-level decoupling, and added the tests. AI assistance was used while drafting parts of the test boilerplate and this PR body; the fix and tests reflect my own engineering decisions.

`failOnWarn: true` is documented as a way to fail a build whenever a warning
is emitted. In practice it relied on rolldown's defaultHandler to escalate
'warn' into a build error, but when the caller passes `logLevel: 'silent'`
rolldown's defaultHandler becomes a no-op. The result is that warnings such
as unresolved imports get silently externalised and the build succeeds when
the user explicitly opted into failing.

Decouple the two by giving rolldown a 'warn' log level whenever failOnWarn
is enabled and the user-facing log level would otherwise suppress warnings.
This keeps the existing behaviour for callers who do not set failOnWarn and
for callers whose logLevel already allows warnings. The escalation path in
the onLog handler is unchanged.

Add direct-build tests in tests/e2e.test.ts that bypass the testBuild helper
(which overrides rolldown's logLevel) so the silent-mode path is actually
exercised. The four scenarios cover: silent + failOnWarn + warning (must
throw), silent + failOnWarn + clean build (must succeed), silent +
failOnWarn=false + warning (must succeed), and warn + failOnWarn + warning
(must still throw, regression preservation).

Closes rolldown#952
@netlify
Copy link
Copy Markdown

netlify Bot commented May 25, 2026

Deploy Preview for tsdown-main ready!

Name Link
🔨 Latest commit b13025d
🔍 Latest deploy log https://app.netlify.com/projects/tsdown-main/deploys/6a146ffb6b16fa00087905e7
😎 Deploy Preview https://deploy-preview-960--tsdown-main.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 25, 2026

Open in StackBlitz

tsdown

pnpm add https://pkg.pr.new/tsdown@960 -D
npm i https://pkg.pr.new/tsdown@960 -D
yarn add https://pkg.pr.new/tsdown@960.tgz -D

create-tsdown

pnpm add https://pkg.pr.new/create-tsdown@960 -D
npm i https://pkg.pr.new/create-tsdown@960 -D
yarn add https://pkg.pr.new/create-tsdown@960.tgz -D

@tsdown/css

pnpm add https://pkg.pr.new/@tsdown/css@960 -D
npm i https://pkg.pr.new/@tsdown/css@960 -D
yarn add https://pkg.pr.new/@tsdown/css@960.tgz -D

@tsdown/exe

pnpm add https://pkg.pr.new/@tsdown/exe@960 -D
npm i https://pkg.pr.new/@tsdown/exe@960 -D
yarn add https://pkg.pr.new/@tsdown/exe@960.tgz -D

tsdown-migrate

pnpm add https://pkg.pr.new/tsdown-migrate@960 -D
npm i https://pkg.pr.new/tsdown-migrate@960 -D
yarn add https://pkg.pr.new/tsdown-migrate@960.tgz -D

commit: b13025d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

failOnWarn is coupled to logging output; silent logging will succeed

1 participant