Making More NPM Packages Work with JsDelivr ESM Mode

jimaek1 pts0 comments

Making More npm Packages Work with jsDelivr ESM mode

Skip to content

jsdelivr.com

Over the past few months, we upgraded the /+esm toolchain, worked through the open compatibility reports, and then used production APM data to find the failures that had not been reported yet. This post covers what we found and the changes now running in production.<br>When we introduced jsDelivr’s ESM bundling service, the idea was simple: take a package published to npm and return a browser-ready ES module.<br>A /+esm request does considerably more than change the module syntax. jsDelivr resolves package exports and browser entry points, converts CommonJS where necessary, provides browser-compatible implementations of supported Node.js APIs, bundles dependencies, removes unused code, minifies the result, and generates a source map. We described many of those capabilities when we announced the service in 2023.<br>That implementation already worked for most of the npm ecosystem. The packages that remained were not one clear category. They combined generated CommonJS helpers, newer JavaScript syntax, browser aliases, source maps, WebAssembly files, Node.js APIs, and output produced by several generations of build tools.<br>Before making another round of changes to the bundling pipeline, we wanted to update the backend and its dependencies. Several packages were multiple major versions behind, and some current releases had moved to ESM-only distribution. We converted the backend, its scripts, configuration, tests, and supporting tools to native ESM, then upgraded the dependency stack together.<br>This included moving from Rollup 2 to Rollup 4 and updating its CommonJS, JSON, and replacement plugins. Rollup 4 itself did not require the backend to be ESM, but the wider dependency upgrade was much easier once the application used the same module system as the growing number of ESM-only packages around it. We also wanted that work finished before adding more substantial fixes and refactorings to versions we already planned to replace.<br>With the current toolchain in place, we went through the outstanding compatibility reports one by one. After resolving most of the known cases, we used production APM data to inspect the /+esm requests that were still failing, grouped them by cause, and fixed the recurring problems that could be handled safely on our side.<br>What the dependency upgrade fixed by itself<br>Some reports were resolved directly by the newer Rollup stack.<br>JSON import attributes are one example. Packages increasingly use the standardized syntax:<br>import metadata from './package.json' with { type: 'json' };<br>A reported failure involving @uppy/core came from this syntax. The previous Rollup 2-based pipeline could not process the published package, while the current Rollup and JSON plugin handle it correctly. We added the syntax to our regression fixtures so that future toolchain changes continue to cover it.<br>Most of the remaining failures were not solved by upgrading Rollup alone. They came from jsDelivr’s own resolver, CommonJS handling, package transforms, or assumptions that had worked for older package output but no longer covered what was being published.<br>Modern syntax still depends on package metadata<br>Top-level await was one such case.<br>Rollup already supports top-level await in ESM. The failure occurred because some .js package entry points were being passed through CommonJS conversion even when their package declared:<br>"type": "module"<br>For a package marked as ESM, that .js file should be parsed as an ES module from the beginning. Running it through the CommonJS plugin could reject the top-level await before Rollup processed the module normally.<br>For packages with "type": "module", CommonJS conversion is now limited to .cjs files and .js files inside nested dependencies. The package’s own .js entry points stay on the native ESM path.<br>Replacing NODE_ENV in more forms<br>The /+esm pipeline has long replaced Node-style environment checks with a production value. Many packages contain code such as:<br>if (process.env.NODE_ENV !== 'production') {<br>enableDevelopmentWarnings();<br>Replacing process.env.NODE_ENV with "production" lets Rollup remove development-only branches and avoids requiring a complete process implementation just for an environment check.<br>Published packages do not always use that exact expression, however. The GraphQL failure used guarded access through globalThis.process, while other packages used global.process or checked whether process existed before reading from it:<br>typeof process !== 'undefined' && process.env.NODE_ENV;

global.process && global.process.env.NODE_ENV;

globalThis.process && globalThis.process.env.NODE_ENV;<br>Those cases are now included in the replacement pass.<br>A later Vite failure exposed another variation:<br>process.env['NODE_ENV'];<br>process.env["NODE_ENV"];<br>We added the bracket-notation forms as well. At the same time, the matching was narrowed so that it does not replace quoted object keys or unrelated member chains...

process package packages rollup node_env module

Related Articles