Skip to content

fix(unplugin): preserve Bun loader composition and build cache boundaries #976

Description

@samchon

Problem

The Bun adapter claims every TypeScript-shaped onLoad path and returns original source for paths the shared transform excludes. That prevents a later Bun plugin from handling declarations, node_modules sources, or another excluded match.

The adapter also has no build-boundary reset when it is used with Bun.build, even though that plugin builder exposes onStart. Bun runtime plugin builders in exact 1.3.14 do not expose the hook, so the lifecycle must remain feature-detected.

Evidence

packages/unplugin/src/bun.ts registers sourceFilePattern directly. Its loader then:

  1. reads the matched path;
  2. calls the raw Unplugin transform;
  3. returns the original contents when no transform result exists.

The raw plugin's transformInclude is the owner that excludes declarations and node_modules, but the Bun adapter does not ask it before reading or returning.

Exact Bun 1.3.14 build controls used a later loader that replaces a dependency with export default 42:

Plugin order Result
Replacement plugin only 42
Earlier broad loader returns undefined, then replacement 42
ttsc first with transforms disabled, then replacement 1

The third result proves that ttsc completed the load with original contents and shadowed the later plugin.

The exact Bun build plugin builder exposed onStart; the exact runtime builder supplied to Bun.plugin did not. Calling onStart unconditionally in runtime setup throws.

A custom-namespace Bun build control remained composable with ttsc registered first. Namespace capture was investigated and rejected.

Consequence surface

  • A downstream loader cannot transform an excluded dependency that happens to match the TypeScript extension filter.
  • ttsc performs avoidable filesystem reads for files it will not transform.
  • Bun.build watch or repeated build sessions can retain a project cache across build boundaries, unlike the standard Unplugin adapters.
  • Runtime registration must continue to work on Bun 1.3.14 without onStart.
  • Lazy option resolution in bun-register must remain last-write-wins before the first load.
  • Plugin-reported dependencies must remain harmless even though Bun runtime supplies no watch-registration channel.

Approach

Build the raw plugin lazily through one accessor. Before reading a path, call its transformInclude; return undefined when the file is excluded so Bun can continue to later loaders.

Feature-detect build.onStart. Where present, forward the build boundary to the raw plugin's buildStart hook so it clears the project cache. Where absent, register only onLoad.

Acceptance and verification

  • An excluded node_modules TypeScript path returns undefined without reading the file.
  • A later Bun loader handles that path.
  • An included source still receives the ttsc transform and the correct ts or tsx loader.
  • Bun runtime setup without onStart remains valid.
  • Bun build setup with onStart clears a previously created raw cache at the next build boundary.
  • Lazy explicit options in bun-register retain current ordering and idempotence.
  • Exact Bun 1.3.14 composition is rechecked in addition to stub tests.
  • Run:
pnpm --filter @ttsc/unplugin build
pnpm --filter @ttsc/test-unplugin start -- --include=bun
pnpm --filter @ttsc/test-unplugin start

Coordination

Issue #969 reports Bun runtime startup, while #252 owns the shared validation amplification. This issue corrects Bun-specific composition and lifecycle behavior without changing the checker-backed project transform contract.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions