Skip to content

feat(bundle): support browser field map in package.json#34407

Merged
nathanwhit merged 2 commits into
denoland:mainfrom
nathanwhit:browser-field-map
May 27, 2026
Merged

feat(bundle): support browser field map in package.json#34407
nathanwhit merged 2 commits into
denoland:mainfrom
nathanwhit:browser-field-map

Conversation

@nathanwhit

Copy link
Copy Markdown
Member

Closes #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.

"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

  • 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
  • All 49 existing bundle::* spec tests still pass
  • node_resolver unit tests still pass
  • --platform deno bundles are unchanged (gate is prefer_browser_field)

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
nathanwhit force-pushed the browser-field-map branch from 8673f96 to a5a411e Compare May 27, 2026 01:01
@nathanwhit
nathanwhit enabled auto-merge (squash) May 27, 2026 01:01

@fibibot fibibot 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.

  1. key_path == resolved_clean only matches relative browser-map keys after raw path normalization. Packages commonly publish extensionless keys, for example the existing postcss fixture has "./lib/terminal-highlight": false; an import that resolves to ./lib/terminal-highlight.js will 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
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 fibibot 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.

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
nathanwhit enabled auto-merge (squash) May 27, 2026 01:19

@fibibot fibibot 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.

CI is green now, promoting prior review to APPROVE.

@nathanwhit
nathanwhit merged commit 57853cb into denoland:main May 27, 2026
136 checks passed
@fibibot

fibibot commented May 27, 2026

Copy link
Copy Markdown
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`)
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.

deno bundle doesn't resolve using browser map in package.json

2 participants