feat(bundle): support browser field map in package.json#34407
Merged
Conversation
Implements the object form of the npm `browser` field for `deno bundle
--platform browser`. The simple string form was already supported.
```json
"browser": {
"./bar.js": "./bar.browser.js",
"crypto": false,
"foo": "./shims/foo.js"
}
```
Esbuild's built-in browser-map handling is bypassed because Deno's
`on_resolve` plugin intercepts every resolve with filter `.*`, so we
have to apply the mapping ourselves.
The substitution lives in `node_resolver` gated on `prefer_browser_field`
(only set when bundling with `--platform browser`). A new `PathDisabled`-
style error variant (`BrowserMapDisabledError`) propagates the `false`
case up to the bundle, where `on_load` emits an empty-module stub.
Closes denoland#30024
nathanwhit
force-pushed
the
browser-field-map
branch
from
May 27, 2026 01:01
8673f96 to
a5a411e
Compare
nathanwhit
enabled auto-merge (squash)
May 27, 2026 01:01
fibibot
requested changes
May 27, 2026
fibibot
left a comment
Contributor
There was a problem hiding this comment.
key_path == resolved_cleanonly matches relative browser-map keys after raw path normalization. Packages commonly publish extensionless keys, for example the existingpostcssfixture has"./lib/terminal-highlight": false; an import that resolves to./lib/terminal-highlight.jswill miss this map entry and still bundle the Node-only module. Resolve/finalize the map key the same way the import was resolved, or add extension probing here, and add a spec with an extensionless relative key.
nathanwhit
disabled auto-merge
May 27, 2026 01:09
A key like `"./foo": false` should disable `./foo.js`, `./foo.cjs`, `./foo/index.js`, etc. — not just an exact-string match. Apply the same node-style probe extensions when comparing browser-map keys to the resolved file path.
fibibot
reviewed
May 27, 2026
fibibot
left a comment
Contributor
There was a problem hiding this comment.
The updated head fixes my prior blocker: browser_map_key_matches() now checks the resolved path against the raw key plus Node-style extension and index.* probes, and the fixture covers both "./extensionless": false and "./dir-extensionless": false. I do not see another resolver issue in the changed browser-map paths. Holding approval until CI is green.
nathanwhit
enabled auto-merge (squash)
May 27, 2026 01:19
fibibot
approved these changes
May 27, 2026
fibibot
left a comment
Contributor
There was a problem hiding this comment.
CI is green now, promoting prior review to APPROVE.
Contributor
|
@bartlomieju this is ready to merge |
littledivy
pushed a commit
to crowlKats/deno
that referenced
this pull request
Jun 10, 2026
…4407) Closes denoland#30024 ## Summary Implements the object form of the npm `browser` field for `deno bundle --platform browser`. The simple string form was already supported; the mapped form was silently dropped. ```json "browser": { "./bar.js": "./bar.browser.js", "crypto": false, "foo": "./shims/foo.js" } ``` - relative-path keys remap the resolved file (`./bar.js` → `./bar.browser.js`) - bare-name keys remap an import specifier (`foo` → `./shims/foo.js`), with `node:` prefix stripped before lookup so `import "node:crypto"` matches a `"crypto"` key - a value of `false` disables the module — the bundler substitutes an empty stub Esbuild's built-in browser-map handling is bypassed because Deno's `on_resolve` plugin claims every resolve with filter `.*`, so we have to apply the mapping ourselves. ## Where the logic lives - **`libs/package_json`** — parses the object form into `browser_map: Option<IndexMap<String, BrowserMapEntry>>` (`Replace(String) | Disabled`). The string form keeps populating the existing `browser` field. - **`libs/node_resolver`** — substitution happens here, gated on the existing `prefer_browser_field` flag (only set when bundling with `--platform browser`). Hooks in `resolve`, `resolve_package`, and `resolve_package_subpath_from_deno_module`. A new `BrowserMapDisabledError` propagates `false` entries up to the caller. - **`cli/tools/bundle/mod.rs`** — only catches `BrowserMapDisabledError` from the resolver, returns a `\0deno-browser-disabled:<orig>` sentinel path, and the `on_load` hook turns it into `module.exports = {}`. ## Test plan - [x] New spec `tests/specs/bundle/browser_platform_map` covers all four cases (relative remap, bare remap, bare disabled, transitive within-package import) using new npm fixture `@denotest/browser-field-map` - [x] All 49 existing `bundle::*` spec tests still pass - [x] `node_resolver` unit tests still pass - [x] `--platform deno` bundles are unchanged (gate is `prefer_browser_field`)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #30024
Summary
Implements the object form of the npm
browserfield fordeno bundle --platform browser. The simple string form was already supported; the mapped form was silently dropped../bar.js→./bar.browser.js)foo→./shims/foo.js), withnode:prefix stripped before lookup soimport "node:crypto"matches a"crypto"keyfalsedisables the module — the bundler substitutes an empty stubEsbuild's built-in browser-map handling is bypassed because Deno's
on_resolveplugin claims every resolve with filter.*, so we have to apply the mapping ourselves.Where the logic lives
libs/package_json— parses the object form intobrowser_map: Option<IndexMap<String, BrowserMapEntry>>(Replace(String) | Disabled). The string form keeps populating the existingbrowserfield.libs/node_resolver— substitution happens here, gated on the existingprefer_browser_fieldflag (only set when bundling with--platform browser). Hooks inresolve,resolve_package, andresolve_package_subpath_from_deno_module. A newBrowserMapDisabledErrorpropagatesfalseentries up to the caller.cli/tools/bundle/mod.rs— only catchesBrowserMapDisabledErrorfrom the resolver, returns a\0deno-browser-disabled:<orig>sentinel path, and theon_loadhook turns it intomodule.exports = {}.Test plan
tests/specs/bundle/browser_platform_mapcovers all four cases (relative remap, bare remap, bare disabled, transitive within-package import) using new npm fixture@denotest/browser-field-mapbundle::*spec tests still passnode_resolverunit tests still pass--platform denobundles are unchanged (gate isprefer_browser_field)