Summary
7.11.0 added @resvg/resvg-js as the Node PNG backend, but did not add a matching
"@resvg/resvg-js": false entry to the browser map in package.json. It is now the only
runtime dependency with a Node-only import site and no browser mapping, so browser bundlers
resolve it for real and fail on the prebuilt native addon.
Reproduction
mkdir jsroot-vite-repro && cd jsroot-vite-repro
npm init -y && npm pkg set type=module
npm i jsroot@7.11.1 && npm i -D vite@^6
mkdir src && echo "import { openFile, draw } from 'jsroot'; export { openFile, draw };" > src/main.js
cat > vite.config.js <<'CFG'
import { defineConfig } from 'vite';
export default defineConfig({ build: { lib: { entry: 'src/main.js', formats: ['es'], fileName: 'main' } } });
CFG
npx vite build
Result (vite 6.4.3, rollup, node 20+, linux x64)
[plugin vite:resolve] Module "fs" has been externalized for browser compatibility,
imported by ".../node_modules/@resvg/resvg-js/js-binding.js"
(same for "path" and "child_process")
✓ 377 modules transformed.
✗ Build failed
[commonjs--resolver] node_modules/@resvg/resvg-js-linux-x64-gnu/resvgjs.linux-x64-gnu.node (1:0):
Unexpected character '\u{7f}' (Note that you need plugins to import files that are not JavaScript)
7.10.0 and earlier build cleanly. Bisects to the dependency added in 7.11.0.
Cause
modules/base/BasePainter.mjs (~L893) does import('@resvg/resvg-js') inside if (isNodeJs()).
The runtime guard is invisible to the bundler — the specifier is a static string literal, so
Rollup/Vite resolves and bundles it regardless of reachability. Every other Node-only import
(canvas, jsdom, tmp, xhr2, mathjax, fs) is stubbed via the browser map and
therefore resolves to an empty module; @resvg/resvg-js is not in that map.
Suggested fix
"browser": {
"canvas": false,
+ "@resvg/resvg-js": false,
"jsdom": false,
Verified locally: patching node_modules/jsroot/package.json this way makes the build above
succeed, and the import site resolves to __vite-browser-external, matching how canvas and
jsdom are already handled. Node behaviour is unaffected — the browser field is only read by
browser bundlers.
master (7.11.99) still lacks the entry. A CI check diffing dependencies against browser
keys would prevent the next recurrence:
import json
p = json.load(open('package.json'))
missing = [k for k in p['dependencies'] if k not in p['browser'] and k != 'three']
assert not missing, missing
Summary
7.11.0 added
@resvg/resvg-jsas the Node PNG backend, but did not add a matching"@resvg/resvg-js": falseentry to thebrowsermap inpackage.json. It is now the onlyruntime dependency with a Node-only import site and no browser mapping, so browser bundlers
resolve it for real and fail on the prebuilt native addon.
Reproduction
Result (vite 6.4.3, rollup, node 20+, linux x64)
7.10.0 and earlier build cleanly. Bisects to the dependency added in 7.11.0.
Cause
modules/base/BasePainter.mjs(~L893) doesimport('@resvg/resvg-js')insideif (isNodeJs()).The runtime guard is invisible to the bundler — the specifier is a static string literal, so
Rollup/Vite resolves and bundles it regardless of reachability. Every other Node-only import
(
canvas,jsdom,tmp,xhr2,mathjax,fs) is stubbed via thebrowsermap andtherefore resolves to an empty module;
@resvg/resvg-jsis not in that map.Suggested fix
"browser": { "canvas": false, + "@resvg/resvg-js": false, "jsdom": false,Verified locally: patching
node_modules/jsroot/package.jsonthis way makes the build abovesucceed, and the import site resolves to
__vite-browser-external, matching howcanvasandjsdomare already handled. Node behaviour is unaffected — thebrowserfield is only read bybrowser bundlers.
master(7.11.99) still lacks the entry. A CI check diffingdependenciesagainstbrowserkeys would prevent the next recurrence: