Skip to content

bundler: implement dataurl and base64 loaders - #36327

Closed
robobun wants to merge 9 commits into
mainfrom
farm/e711a5a5/bundler-dataurl-base64-loaders
Closed

robobun wants to merge 9 commits into
mainfrom
farm/e711a5a5/bundler-dataurl-base64-loaders

Conversation

@robobun

@robobun robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #20917

Problem

Bun.build / bun build accept the dataurl and base64 loader values (via loader: {".png": "dataurl"}, import x from "./f" with {type: "base64"}, or --loader .png:dataurl) but silently emit an empty-string module with success: true and no warning:

await Bun.build({
  entrypoints: ["e.mjs"],
  loader: { ".dat": "dataurl" },
});
// bundled output: var x_default = "";

This is the exact loader example shown in docs/bundler/index.mdx, so users following the docs get silent data loss.

For reference, esbuild with the same input:

--loader:.dat=dataurl  ->  "data:text/plain;charset=utf-8,ABC"
--loader:.dat=base64   ->  "QUJD"

Cause

The get_ast loader switch in src/bundler/ParseTask.rs had a stub arm:

// TODO:
Loader::Dataurl | Loader::Base64 | Loader::Bunsh => {
    return get_empty_ast::<E::String>(log, transpiler, opts, bump, source);
}

so both loaders returned a lazy-export AST with an empty E::String instead of encoding the source.

Fix

Implement both loaders, matching esbuild semantics:

  • base64: standard base64 of the raw file bytes as a string, via bun_base64::encode.
  • dataurl: data: URL with MIME type derived from the file extension (via MimeType::by_extension_no_default, with a whatwg binary-byte content sniff for unknown extensions), encoded as percent-escaped or base64 via the existing DataURL::encode_string_as_shortest_data_url. The resulting URL is also set as url_for_css so CSS url(...) references resolve inline.

Loader::handles_empty_file gains Base64 | Dataurl so an empty input yields "" / "data:<mime>," rather than {}.

Loader::Bunsh is left unchanged here; the shell-script bundling path is a separate feature.

The esbuild comparison table in docs/bundler/esbuild.mdx is updated (dataurl and base64 are no longer listed as unimplemented), and docs/bundler/loaders.mdx gains entries for both loaders.

Verification

New tests in test/bundler/bundler_loader.test.ts cover both loader config and with {type: ...} import attributes, text and binary inputs, known and unknown extensions, and empty files.

This also enables five previously-skipped tests ported from esbuild's loader suite by adding base64 and dataurl to expectBundled's supported-loader list:

  • loader/RequireCustomExtensionBase64
  • loader/RequireCustomExtensionDataURL
  • loader/AutoDetectMimeTypeFromExtension
  • loader/Base64CommonJSAndES6
  • loader/DataURLCommonJSAndES6

loader/RequireCustomExtensionPreferLongest is marked todo (it tests multi-dot extension matching in the loader map, which is a separate gap).


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/esbuild/loader.test.ts

The dataurl and base64 loaders were accepted by the option validator and
documented in the bundler docs, but emitted an empty-string module with
success: true and no warning. A loader config like {".png": "dataurl"}
(the example shown in docs/bundler/index.mdx) produced var x_default = ""
for any input.

Implement both loaders in the ParseTask switch, matching esbuild:

  base64   standard base64 encoding of the file bytes, as a string
  dataurl  a data: URL with MIME type from the file extension (falling
           back to a binary-byte content sniff for unknown extensions),
           encoded as percent-escaped or base64, whichever is shorter

Both reuse existing helpers: bun_base64::encode for the base64 loader
and DataURL::encode_string_as_shortest_data_url for the dataurl loader.

Also mark both loaders as handles_empty_file so an empty input yields
an empty string rather than {} / undefined.

Enables five previously-skipped tests ported from esbuild's loader suite
and updates the esbuild comparison docs.
@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is green, CI red is unrelated flake; needs a maintainer to merge

Reproduced with:

USE_SYSTEM_BUN=1 bun test test/bundler/bundler_loader.test.ts -t "loader-base64|loader-dataurl"
# 10 fail (emit "", CSS url() emits resolved path)
bun bd test test/bundler/bundler_loader.test.ts -t "loader-base64|loader-dataurl"
# 10 pass

The bundler loader tests (bundler_loader.test.ts, esbuild/loader.test.ts) pass on every lane in both CI runs (builds 84960 and 85072). Remaining CI red is unrelated to this diff: test/js/bun/webview/webview-chrome.test.ts segfault on debian x64-asan (webview CDP click/animation, no overlap with bundler code; reported for main triage) plus known flakes in spawn/napi/http/install that pass on retry.

Also addressed from review:

  • Loader::side_effects() marks Base64/Dataurl as NoSideEffectsPureData so unused imports tree-shake (dabcc3b)
  • Loader::Base64 sets url_for_css so CSS url() references inline as data URLs (045d1e4)
  • type Loader union in bun.d.ts and docs/bundler/index.mdx include both values (ad2d22b, d122334)
  • Extension lookup lowercases before by_extension_no_default so LOGO.PNG resolves to image/png (2a5be9b)

Deferred to #36334 (pre-existing, different dispatches, not the bundler get_ast path this PR covers):

  • src/bundler/transpiler.rs:2944 (--no-bundle path) still panics on these loaders
  • src/runtime/jsc_hooks.rs:2115 (runtime bun script.ts path) falls through to the file loader for with {type: "base64"}
  • BOM-stripping in resolver/fs.rs::finish_arena_contents runs on binary-loader reads

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b08954b5-00e1-4b35-98ac-d376f703e9aa

📥 Commits

Reviewing files that changed from the base of the PR and between 1a1f433 and 2a5be9b.

📒 Files selected for processing (2)
  • src/bundler/ParseTask.rs
  • test/bundler/bundler_loader.test.ts

Walkthrough

Changes

Base64 and dataurl loaders now classify assets as pure data, infer MIME types, encode JavaScript and CSS imports, and handle empty files. Bundler tests cover imports, CSS URLs, DCE, binary content, and unknown extensions. Documentation and loader validation lists were updated.

Base64 and dataurl loader support

Layer / File(s) Summary
Loader classification and empty-file handling
src/ast/loader.rs
Base64 and dataurl loaders are recognized as pure data and valid empty-file loaders.
MIME detection and asset encoding
src/bundler/ParseTask.rs
MIME types are inferred and assets are emitted as base64 or shortest-form data URLs for JavaScript and CSS paths.
Loader validation and public types
test/bundler/expectBundled.ts, docs/bundler/index.mdx, packages/bun-types/bun.d.ts
Validation lists and Loader unions accept base64 and dataurl.
Bundler coverage and documentation
test/bundler/bundler_loader.test.ts, test/bundler/esbuild/loader.test.ts, docs/bundler/*
Tests cover imports, CSS, DCE, binary data, empty files, and unknown extensions; documentation describes the loaders and updated esbuild comparisons.

Possibly related PRs

  • oven-sh/bun#36334: Implements related base64/dataurl loader wiring and side-effect classification.

Suggested reviewers: alii, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding bundler support for dataurl and base64 loaders.
Description check ✅ Passed The description covers the problem, cause, fix, and verification, though it does not use the template's exact headings.
Linked Issues check ✅ Passed The changes implement dataurl handling for loader inputs, including MIME-aware encoding and empty-file behavior, matching #20917's expected fix.
Out of Scope Changes check ✅ Passed The docs, tests, and type updates all support the loader implementation and do not appear unrelated.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:13 AM PT - Jul 29th, 2026

❌ @robobun, your commit 2a5be9b has 1 failures in Build #85072 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36327

That installs a local version of the PR into your bun-36327 executable, so you can run:

bun-36327 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. plugin onLoad with dataurl loader bundles empty content "" broken and incompatible with esbuild #20917 - Reports that using loader: "dataurl" in a bundler plugin's onLoad produces an empty string "" instead of the actual data URL; this PR implements the missing Loader::Dataurl and Loader::Base64 handling in ParseTask.rs

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #20917

🤖 Generated with Claude Code

Comment thread src/ast/loader.rs
Both emit a pure E::String lazy export exactly like Loader::Text does,
so they belong in the NoSideEffectsPureData arm of Loader::side_effects.
Without this an unused base64/dataurl import is kept in the bundle.
Comment thread src/bundler/ParseTask.rs
Build the data URL for CSS url() inlining in the same bump allocation as
the base64 payload, reusing the encoded bytes (the JS module exports the
base64 tail, CSS gets the full data: URL). Matches esbuild LoaderBase64
and the sibling dataurl/text/md arms.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bundler/ParseTask.rs`:
- Line 1340: Remove the non-informative `// TODO:` comment in the affected
branch of `ParseTask`; do not add a replacement unless there is a specific
compatibility invariant that must be documented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 72a4566a-45bc-4be2-bf80-5e99f0125d29

📥 Commits

Reviewing files that changed from the base of the PR and between 59242d6 and 045d1e4.

📒 Files selected for processing (7)
  • docs/bundler/esbuild.mdx
  • docs/bundler/loaders.mdx
  • src/ast/loader.rs
  • src/bundler/ParseTask.rs
  • test/bundler/bundler_loader.test.ts
  • test/bundler/esbuild/loader.test.ts
  • test/bundler/expectBundled.ts

Comment thread src/bundler/ParseTask.rs Outdated
Comment thread src/bundler/ParseTask.rs
Comment thread docs/bundler/loaders.mdx
@robobun
robobun requested a review from alii as a code owner July 29, 2026 06:30
Comment thread packages/bun-types/bun.d.ts
Comment thread docs/bundler/loaders.mdx
Comment thread src/bundler/ParseTask.rs
Comment thread src/bundler/ParseTask.rs
Comment thread src/bundler/ParseTask.rs
by_extension_no_default is case-sensitive with all-lowercase keys, so
LOGO.PNG missed the table and fell through to the content sniff.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found in this pass — the uppercase-extension fix in 2a5be9b and the earlier type/docs updates address everything raised. That said, this is a new user-facing bundler feature (~90 lines of native code in get_ast, new MIME-sniff helper, url_for_css wiring) with a few design calls a maintainer should sign off on: the intentional full-file-UTF-8 vs 512-byte-sniff asymmetry, and the three sibling dispatches (transpiler.rs, jsc_hooks.rs, BOM-stripping in resolver/fs.rs) deferred to #36334 rather than fixed here.

What was reviewed:

  • Both new Loader::Base64/Loader::Dataurl arms in ParseTask.rs — arena allocation, url_for_css, lazy-export AST shape mirror the existing Text arm.
  • guess_mime_type_for_data_url: extension lowercasing, ;charset=utf-8 append only for text/* without an existing charset param, WHATWG binary-byte sniff fallback.
  • handles_empty_file / side_effects additions and the DCE test that exercises them.
  • Test coverage: loader map + import attribute, text/binary, known/unknown ext, empty file, CSS url(), uppercase ext, plus the five un-skipped esbuild-ported tests.
Extended reasoning...

Overview

Implements the base64 and dataurl bundler loaders that were previously accepted but stubbed to an empty string. Touches src/bundler/ParseTask.rs (new guess_mime_type_for_data_url helper + two new get_ast match arms, ~90 lines), src/ast/loader.rs (adds both variants to handles_empty_file and side_effects), packages/bun-types/bun.d.ts, three docs pages, and the bundler test suite (new tests in bundler_loader.test.ts, one todo marker in esbuild/loader.test.ts, and adds both loader names to expectBundled.ts's supported list).

Security risks

None identified. The new code encodes already-read file bytes into a string literal in the output AST; input is bundler source files, not untrusted network data. No auth, crypto, path-traversal, or resource-limit surface. Buffer sizing for the base64 arm uses bun_base64::encode_len and slices into a bump-allocated buffer of exactly prefix_len + encode_len.

Level of scrutiny

Medium-high. This is production bundler code on the get_ast hot path and a user-facing feature (fixes #20917, matches esbuild semantics). It went through five rounds of inline review here — every point was either applied (Loader type union in .d.ts and index.mdx, extension lowercasing + test) or explicitly deferred with justification (full-file UTF-8 check kept intentionally; transpiler.rs/jsc_hooks.rs/BOM-stripping tracked in #36334 as pre-existing). The implementation itself is straightforward and mirrors the sibling Loader::Text arm, but the deferred-sibling scope decision and the sniff-semantics divergence from esbuild are the kind of calls REVIEW.md flags for maintainer confirmation ("fix the whole class in the same PR" vs. scope-tight follow-up).

Other factors

Test coverage is thorough: import-attribute and loader-map entry points, text and binary payloads, known/unknown/uppercase extensions, empty files, CSS url() inlining for both loaders, and DCE of unused imports (verifying the side_effects addition). Five previously-skipped esbuild-ported tests are enabled by adding the loader names to expectBundled's allowlist. The one newly-todo'd test (RequireCustomExtensionPreferLongest) is for a separate multi-dot-extension gap and is commented as such. Given the feature scope and the deferred follow-ups, I'm deferring rather than approving so a maintainer can confirm the #36334 split is acceptable.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-29, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. The linked issue (#20917) stays open. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin onLoad with dataurl loader bundles empty content "" broken and incompatible with esbuild

2 participants