Bundler plugins that preserve the semantics of new URL('./file.js', import.meta.url) after bundling.
This library simplifies the distribution of libraries that use web workers, such as the Monaco Editor. Note that the scope of this project is intentionally narrow — feature requests are unlikely to be addressed.
In native ESM, this pattern works perfectly:
const workerUrl = new URL('./worker.js', import.meta.url);
// Use it anywhere:
new Worker(workerUrl);
fetch(workerUrl);
passToLibrary(workerUrl);The URL resolves correctly at runtime because import.meta.url points to the current module.
But bundlers break this. After bundling:
import.meta.urlnow points to the bundled file's location- The relative path
./worker.jsno longer resolves to the right file - The worker file wasn't included in the bundle output at all
Bundlers have added special detection for specific patterns:
// Webpack 5 detects this:
new Worker(new URL('./worker.js', import.meta.url));But this only works when new Worker() directly wraps the URL. The moment you separate them, it breaks:
// ❌ Bundler doesn't recognize this as a worker entry:
const workerUrl = new URL('./worker.js', import.meta.url);
new Worker(workerUrl);Your code runs correctly from source, but breaks after bundling. That's a correctness problem.
Mark URLs that should be bundled as separate entry points with ?esm:
const workerUrl = new URL('./worker.js?esm', import.meta.url);The plugin ensures this code behaves identically before and after bundling:
- The referenced file is bundled as a separate entry with all its dependencies
- The URL is rewritten to point to the bundled output
import.meta.urlresolution continues to work correctly
new URL('<path>?esm', import.meta.url)Where:
<path>is a relative path to the file to bundle?esmsignals that this file should be bundled as a separate entry pointimport.meta.urlprovides the base URL for resolution
The plugin guarantees that bundled code behaves the same as source code:
Paths resolve relative to the importing file, just like in native ESM:
// In /src/features/chat/index.js:
new URL('./workers/processor.js?esm', import.meta.url)
// Resolves to: /src/features/chat/workers/processor.js
new URL('../shared/worker.js?esm', import.meta.url)
// Resolves to: /src/features/shared/worker.jsThe resolved file becomes a separate entry point:
- Gets its own bundle with all its dependencies
- Emitted alongside the main bundle
- Output filename may differ to avoid collisions
The path is updated to point to the bundled output:
// Source:
new URL('./worker.js?esm', import.meta.url)
// After bundling:
new URL('./worker.js', import.meta.url)| Package | Bundler | Status |
|---|---|---|
@vscode/esm-url-webpack-plugin |
Webpack 5 | ✅ Available |
@vscode/rollup-plugin-esm-url |
Rollup | ✅ Available |
@vscode/rollup-plugin-esm-url |
Vite | ✅ Available (uses Rollup plugin) |
@vscode/esbuild-plugin-esm-url |
esbuild | ✅ Available |
| — | Parcel | ✅ Works natively (no plugin needed) |
Parcel natively handles new URL('./file.js', import.meta.url) patterns without requiring a plugin. The ?esm query parameter is ignored, but the file will be bundled as a separate entry point. We test Parcel compatibility as part of our test suite.
# For Webpack
npm install @vscode/esm-url-webpack-plugin --save-dev
# For Rollup
npm install @vscode/rollup-plugin-esm-url --save-dev
# For Vite (uses the Rollup plugin)
npm install @vscode/rollup-plugin-esm-url --save-dev
# For esbuild
npm install @vscode/esbuild-plugin-esm-url --save-dev// webpack.config.js
const { EsmUrlPlugin } = require('@vscode/esm-url-webpack-plugin');
module.exports = {
plugins: [new EsmUrlPlugin()],
};// rollup.config.js
import { esmUrlPlugin } from '@vscode/rollup-plugin-esm-url';
export default {
plugins: [esmUrlPlugin()],
};// vite.config.js
import { defineConfig } from 'vite';
import { esmUrlPlugin } from '@vscode/rollup-plugin-esm-url';
export default defineConfig({
plugins: [esmUrlPlugin()],
});// build.mjs
import * as esbuild from 'esbuild';
import { esmUrlPlugin } from '@vscode/esbuild-plugin-esm-url';
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
format: 'esm',
outdir: 'dist',
plugins: [esmUrlPlugin()],
});MIT