From e8b15b2f1a7e983eb1b130f177ef849424aea441 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Mon, 26 Oct 2026 21:25:43 +0100 Subject: [PATCH] drop web-worker package --- package-lock.json | 9 +- packages/shell-bson-parser/README.md | 2 +- packages/shell-bson-parser/package.json | 3 +- .../shell-bson-parser/src/browser-worker.ts | 35 ++++ packages/shell-bson-parser/src/index.spec.ts | 190 ++++++++++++++---- packages/shell-bson-parser/src/node-worker.ts | 41 ++++ .../shell-bson-parser/src/worker-client.ts | 152 +++++++------- .../shell-bson-parser/src/worker-source.ts | 4 + .../shell-bson-parser/src/worker-types.ts | 10 + packages/shell-bson-parser/src/worker.ts | 13 +- .../{slow-worker.mjs => slow-worker.js} | 9 +- .../shell-bson-parser/webpack.cjs.config.cjs | 29 +-- .../webpack.worker.config.cjs | 23 ++- 13 files changed, 353 insertions(+), 167 deletions(-) create mode 100644 packages/shell-bson-parser/src/browser-worker.ts create mode 100644 packages/shell-bson-parser/src/node-worker.ts create mode 100644 packages/shell-bson-parser/src/worker-source.ts rename packages/shell-bson-parser/test/fixtures/{slow-worker.mjs => slow-worker.js} (76%) diff --git a/package-lock.json b/package-lock.json index 920852731..c36db6d7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27706,12 +27706,6 @@ "node": ">= 8" } }, - "node_modules/web-worker": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", - "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", - "license": "Apache-2.0" - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -30219,8 +30213,7 @@ "license": "Apache-2.0", "dependencies": { "acorn": "^8.18.0", - "javascript-stringify": "^2.1.0", - "web-worker": "^1.5.0" + "javascript-stringify": "^2.1.0" }, "devDependencies": { "@mongodb-js/eslint-config-devtools": "^0.11.14", diff --git a/packages/shell-bson-parser/README.md b/packages/shell-bson-parser/README.md index 14e25f70d..6992b9823 100644 --- a/packages/shell-bson-parser/README.md +++ b/packages/shell-bson-parser/README.md @@ -87,7 +87,7 @@ This options object will disallow method calls, but will allow comments ## Worker -Every call to `parse` is sent to a shared worker (using [`web-worker`](https://github.com/developit/web-worker), so it works in both Node.js and browsers). The worker is created lazily on the first call and reused for later calls. BSON values are serialized across the worker boundary and rebuilt as `bson` instances on the calling side. +Every call to `parse` is sent to a shared worker: a Web Worker in browsers, or a `worker_threads` worker in Node.js. The worker code ships inside the package and is loaded lazily on the first `parse` call, so it works the same when bundled and adds nothing for consumers that only use `toJSString`. The worker is created lazily on the first call and reused for later calls. BSON values are serialized across the worker boundary and rebuilt as `bson` instances on the calling side. If parsing throws inside the worker, the returned promise rejects with the error message. diff --git a/packages/shell-bson-parser/package.json b/packages/shell-bson-parser/package.json index 265e1b1fa..848d20fa9 100644 --- a/packages/shell-bson-parser/package.json +++ b/packages/shell-bson-parser/package.json @@ -53,8 +53,7 @@ }, "dependencies": { "acorn": "^8.18.0", - "javascript-stringify": "^2.1.0", - "web-worker": "^1.5.0" + "javascript-stringify": "^2.1.0" }, "peerDependencies": { "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.3.2" diff --git a/packages/shell-bson-parser/src/browser-worker.ts b/packages/shell-bson-parser/src/browser-worker.ts new file mode 100644 index 000000000..c24c3e80f --- /dev/null +++ b/packages/shell-bson-parser/src/browser-worker.ts @@ -0,0 +1,35 @@ +import type { WorkerHandle, WorkerCallbacks } from './worker-types.js'; + +export function createBrowserWorker( + source: string, + { onMessage, onError }: WorkerCallbacks, +): WorkerHandle { + // Compass Web injects whatwg-url for bare URL references; it lacks blob APIs. + // Use the native constructor for blob creation and revocation. + const blobUrl = globalThis.URL.createObjectURL( + new Blob([source], { type: 'text/javascript' }), + ); + try { + const worker = new globalThis.Worker(blobUrl); + worker.addEventListener('message', (event: MessageEvent) => + onMessage(event.data), + ); + worker.addEventListener('error', (event: ErrorEvent) => + onError(new Error(event.message || 'Worker error')), + ); + worker.addEventListener('messageerror', () => + onError(new Error('Worker message could not be deserialized')), + ); + + return { + postMessage: (message) => worker.postMessage(message), + terminate: () => { + worker.terminate(); + globalThis.URL.revokeObjectURL(blobUrl); + }, + }; + } catch (err) { + globalThis.URL.revokeObjectURL(blobUrl); + throw err; + } +} diff --git a/packages/shell-bson-parser/src/index.spec.ts b/packages/shell-bson-parser/src/index.spec.ts index ce11ccf27..34a3d6507 100644 --- a/packages/shell-bson-parser/src/index.spec.ts +++ b/packages/shell-bson-parser/src/index.spec.ts @@ -3,15 +3,15 @@ import sinon from 'sinon'; import vm from 'vm'; import fs from 'fs/promises'; import path from 'path'; -import { fileURLToPath } from 'url'; -import * as WebWorkerModule from 'web-worker'; +import { fileURLToPath, pathToFileURL } from 'url'; +import { Worker as NodeWorker } from 'worker_threads'; import * as api from './index.js'; import { terminateWorker, callWorker, getExecutionTimeoutMs, - getWorkerScriptUrl, + setWorkerSource, } from './worker-client.js'; import { restrictGlobalScope, @@ -21,39 +21,42 @@ import { } from './worker.js'; import { PARSE_TEST_CASES } from './../test/parse-test-cases.js'; -const WebWorker = (WebWorkerModule as unknown as { default: typeof Worker }) - .default; +const specDir = path.dirname(fileURLToPath(import.meta.url)); describe('shell-bson-parser with webworker processing', function () { - const initialWorkerScriptUrl = process.env.TEST_WORKER_SCRIPT_URL; + let workerBundle: string; - before(function () { - process.env.TEST_WORKER_SCRIPT_URL = '../dist/worker.js'; + before(async function () { + // Specs run from src, where worker-source.ts is an empty placeholder. + workerBundle = await fs.readFile( + path.join(specDir, '..', 'dist', 'worker.js'), + 'utf8', + ); + setWorkerSource(workerBundle); }); after(function () { - if (initialWorkerScriptUrl) { - process.env.TEST_WORKER_SCRIPT_URL = initialWorkerScriptUrl; - } else { - delete process.env.TEST_WORKER_SCRIPT_URL; - } + setWorkerSource(null); terminateWorker(); }); - it('returns the file URL for the worker script', async function () { - const initialWorkerScriptUrl = process.env.TEST_WORKER_SCRIPT_URL; - delete process.env.TEST_WORKER_SCRIPT_URL; + it('inlines the bundled worker into dist/worker-source.js', async function () { + const { default: inlined } = await import( + pathToFileURL(path.join(specDir, '..', 'dist', 'worker-source.js')).href + ); + expect(inlined).to.equal(workerBundle); + }); + it('rejects when the worker source is missing', async function () { + terminateWorker(); + setWorkerSource(''); try { - expect(await getWorkerScriptUrl()).to.equal( - new URL('./worker.js', import.meta.url).toString(), - ); + await api.parse('{ a: 1 }'); + expect.fail('Expected parse to throw'); + } catch (err) { + expect((err as Error).message).to.match(/worker source is missing/); } finally { - if (initialWorkerScriptUrl) { - process.env.TEST_WORKER_SCRIPT_URL = initialWorkerScriptUrl; - } else { - delete process.env.TEST_WORKER_SCRIPT_URL; - } + setWorkerSource(workerBundle); } }); @@ -176,7 +179,7 @@ describe('shell-bson-parser with webworker processing', function () { let terminateSpy: sinon.SinonSpy; beforeEach(function () { - terminateSpy = sinon.spy(WebWorker.prototype, 'terminate'); + terminateSpy = sinon.spy(NodeWorker.prototype, 'terminate'); }); afterEach(function () { @@ -199,14 +202,118 @@ describe('shell-bson-parser with webworker processing', function () { }); }); - it('strips dangerous globals and locks down Object.prototype when the real worker starts', async function () { - const workerBundlePath = path.join( - path.dirname(fileURLToPath(import.meta.url)), - '..', - 'dist', - 'worker.js', - ); - const code = await fs.readFile(workerBundlePath, 'utf8'); + describe('worker lifecycle', function () { + afterEach(function () { + terminateWorker(); + setWorkerSource(workerBundle); + }); + + it('rejects pending requests right away when the worker thread exits', async function () { + terminateWorker(); + setWorkerSource('process.exit(3)'); + try { + await callWorker([1]); + expect.fail('Expected callWorker to throw'); + } catch (err) { + expect((err as Error).message).to.equal('Worker exited with code 3'); + } + }); + + it('does not let the exit of a terminated worker kill its replacement', async function () { + await api.parse('{ a: 1 }'); + terminateWorker(); + // The old thread's 'exit' event lands while the new request is pending + const res = await api.parse('{ b: 2 }'); + expect(res).to.deep.equal({ b: 2 }); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(await api.parse('{ c: 3 }')).to.deep.equal({ c: 3 }); + }); + }); + + describe('browser worker', function () { + type Listener = (event: unknown) => void; + + class FakeWorker { + static instances: FakeWorker[] = []; + listeners = new Map(); + terminated = false; + constructor(public url: string) { + FakeWorker.instances.push(this); + } + addEventListener(type: string, fn: Listener) { + this.listeners.set(type, fn); + } + postMessage(message: { id: number }) { + const respond = this.listeners.get('message'); + setTimeout(() => + respond?.({ + data: { + id: message.id, + ok: true, + result: { data: 'from-browser', bsonTypes: new Map() }, + }, + }), + ); + } + terminate() { + this.terminated = true; + } + } + + let createObjectURL: sinon.SinonStub; + let revokeObjectURL: sinon.SinonStub; + + beforeEach(function () { + terminateWorker(); + FakeWorker.instances = []; + (globalThis as { Worker?: unknown }).Worker = FakeWorker; + createObjectURL = sinon + .stub(globalThis.URL, 'createObjectURL') + .returns('blob:fake'); + revokeObjectURL = sinon.stub(globalThis.URL, 'revokeObjectURL'); + }); + + afterEach(function () { + terminateWorker(); + delete (globalThis as { Worker?: unknown }).Worker; + createObjectURL.restore(); + revokeObjectURL.restore(); + }); + + it('starts the worker from a blob URL and revokes it on terminate', async function () { + expect(await callWorker(['x'])).to.equal('from-browser'); + expect(FakeWorker.instances).to.have.length(1); + expect(FakeWorker.instances[0].url).to.equal('blob:fake'); + const blob = createObjectURL.firstCall.args[0] as Blob; + expect(await blob.text()).to.equal(workerBundle); + expect(revokeObjectURL.called).to.equal(false); + + terminateWorker(); + expect(FakeWorker.instances[0].terminated).to.equal(true); + expect(revokeObjectURL.calledOnceWith('blob:fake')).to.equal(true); + }); + + it('rejects pending requests on a worker error event', async function () { + sinon.stub(FakeWorker.prototype, 'postMessage').callsFake(function ( + this: FakeWorker, + ) { + // Never responds; fails with an error event instead + setTimeout(() => this.listeners.get('error')?.({ message: 'boom' })); + }); + try { + await callWorker(['x']); + expect.fail('Expected callWorker to throw'); + } catch (err) { + expect((err as Error).message).to.equal('boom'); + } finally { + (FakeWorker.prototype.postMessage as sinon.SinonStub).restore(); + } + expect(FakeWorker.instances[0].terminated).to.equal(true); + }); + }); + + it('strips dangerous globals and locks down Object.prototype when the real worker starts', function () { + const code = workerBundle; const sandbox: Record = Object.create(null); let messageHandler: ((event: MessageEvent) => void) | undefined; @@ -265,20 +372,19 @@ describe('shell-bson-parser with webworker processing', function () { }); describe('execution timeout', function () { - const initialWorkerScriptUrl = process.env.TEST_WORKER_SCRIPT_URL; - - beforeEach(function () { + beforeEach(async function () { terminateWorker(); - process.env.TEST_WORKER_SCRIPT_URL = '../test/fixtures/slow-worker.mjs'; + setWorkerSource( + await fs.readFile( + path.join(specDir, '..', 'test', 'fixtures', 'slow-worker.js'), + 'utf8', + ), + ); }); afterEach(function () { terminateWorker(); - if (initialWorkerScriptUrl) { - process.env.TEST_WORKER_SCRIPT_URL = initialWorkerScriptUrl; - } else { - delete process.env.TEST_WORKER_SCRIPT_URL; - } + setWorkerSource(workerBundle); }); it('rejects a request whose worker thread is wedged past the timeout', async function () { diff --git a/packages/shell-bson-parser/src/node-worker.ts b/packages/shell-bson-parser/src/node-worker.ts new file mode 100644 index 000000000..c8f9ff34e --- /dev/null +++ b/packages/shell-bson-parser/src/node-worker.ts @@ -0,0 +1,41 @@ +import type { Worker as NodeWorker } from 'worker_threads'; +import type { WorkerHandle, WorkerCallbacks } from './worker-types.js'; + +export function createNodeWorker( + source: string, + { onMessage, onError }: WorkerCallbacks, +): WorkerHandle { + // getBuiltinModule keeps bundlers from rewriting or polyfilling the import. + const threads = + typeof process !== 'undefined' && process.getBuiltinModule + ? process.getBuiltinModule('worker_threads') + : undefined; + if (!threads) { + throw new Error( + '@mongodb-js/shell-bson-parser needs a global Worker or process.getBuiltinModule', + ); + } + + // The worker bundle talks to worker_threads' parentPort itself. + const worker: NodeWorker = new threads.Worker(source, { eval: true }); + // Fails pending requests right away if the thread dies, instead of waiting + // for their timeout. Also fires after terminate(), ignored by the client. + worker.on('exit', (code: number) => + onError(new Error(`Worker exited with code ${code}`)), + ); + worker.on('message', onMessage); + worker.on('error', (err: Error) => + onError(new Error(err?.message || 'Worker error')), + ); + worker.on('messageerror', () => + onError(new Error('Worker message could not be deserialized')), + ); + // An idle worker should not keep the host process alive. Must come after + // the listeners, since adding a 'message' listener re-refs the port. + worker.unref(); + + return { + postMessage: (message) => worker.postMessage(message), + terminate: () => void worker.terminate(), + }; +} diff --git a/packages/shell-bson-parser/src/worker-client.ts b/packages/shell-bson-parser/src/worker-client.ts index 006bb83d6..02618821c 100644 --- a/packages/shell-bson-parser/src/worker-client.ts +++ b/packages/shell-bson-parser/src/worker-client.ts @@ -1,8 +1,11 @@ -import * as WebWorkerModule from 'web-worker'; -const WebWorker = (WebWorkerModule as unknown as { default: typeof Worker }) - .default; +import { createBrowserWorker } from './browser-worker.js'; +import { createNodeWorker } from './node-worker.js'; import { trackBSON, untrackBSON } from './structured-clone-bson.js'; -import type { WorkerResponse } from './worker-types.js'; +import type { + WorkerHandle, + WorkerCallbacks, + WorkerResponse, +} from './worker-types.js'; /** Default execution timeout for worker requests */ const DEFAULT_EXECUTION_TIMEOUT_MS = 5_000; @@ -20,9 +23,9 @@ export function getExecutionTimeoutMs(executionTimeoutMs?: number): number { return executionTimeoutMs; } -let worker: Worker | null = null; -let workerPromise: Promise | null = null; -let blobUrl: string | null = null; +let worker: WorkerHandle | null = null; +let workerSourceOverride: string | null = null; +let workerPromise: Promise | null = null; let nextId = 0; const pending = new Map< number, @@ -34,93 +37,78 @@ const pending = new Map< >(); /** Exported for tests */ -export async function getWorkerScriptUrl(): Promise { - const override = - typeof process !== 'undefined' - ? process.env.TEST_WORKER_SCRIPT_URL - : undefined; - if (override) { - return new URL(override, import.meta.url).toString(); - } +export function setWorkerSource(source: string | null): void { + workerSourceOverride = source; +} - const scriptUrl = new URL('./worker.js', import.meta.url); - if (scriptUrl.protocol === 'file:') { - return scriptUrl.toString(); - } - const response = await fetch(scriptUrl); - if (!response.ok) { - throw new Error( - `Failed to fetch shell-bson-parser worker script: ${response.status} ${response.statusText}`, - ); +async function getWorkerSource(): Promise { + if (workerSourceOverride !== null) { + return workerSourceOverride; } - const code = await response.text(); - // Compass Web injects whatwg-url for bare URL references; it lacks blob APIs. - // Use the native constructor for blob creation and revocation. - blobUrl = globalThis.URL.createObjectURL( - new Blob([code], { type: 'text/javascript' }), - ); - return blobUrl; + const source = await import('./worker-source.js'); + return source.default; } -function createWorker(): Promise { +function createWorker(): Promise { if (worker) { return Promise.resolve(worker); } if (workerPromise) { return workerPromise; } - - workerPromise = (async () => { - const scriptUrl = await getWorkerScriptUrl(); - const newWorker = new WebWorker( - scriptUrl, - typeof globalThis.Worker === 'function' ? undefined : { type: 'module' }, - ); - const onMessageHandler = (event: MessageEvent) => { - const response = event.data; - const entry = pending.get(response.id); - if (!entry) { - return; - } - clearTimeout(entry.executionTimer); - pending.delete(response.id); - if (!response.ok) { - const { error, errorProps } = response; - entry.reject( - typeof error === 'object' && error !== null - ? Object.assign(error, errorProps) - : error, - ); - return; - } - try { - entry.resolve(untrackBSON(response.result)); - } catch (err) { - entry.reject(err as Error); - } - }; - - const onErrorHandler = (event: ErrorEvent) => { - terminateWorker(new Error(event.message || 'Worker error')); - }; - - const onMessageErrorHandler = () => { - terminateWorker(new Error('Worker message could not be deserialized')); + const onMessage = (response: WorkerResponse) => { + const entry = pending.get(response.id); + if (!entry) { + return; + } + clearTimeout(entry.executionTimer); + pending.delete(response.id); + if (!response.ok) { + const { error, errorProps } = response; + entry.reject( + typeof error === 'object' && error !== null + ? Object.assign(error, errorProps) + : error, + ); + return; + } + try { + entry.resolve(untrackBSON(response.result)); + } catch (err) { + entry.reject(err as Error); + } + }; + + const startup = (async () => { + const source = await getWorkerSource(); + if (!source) { + throw new Error( + '@mongodb-js/shell-bson-parser worker source is missing from the build', + ); + } + // Late errors/exits from an already terminated worker must not kill its + // replacement, so only react when this handle is still the active one. + let newWorker: WorkerHandle | null = null; + const handlers: WorkerCallbacks = { + onMessage, + onError: (err) => { + if (newWorker && newWorker === worker) terminateWorker(err); + }, }; - - newWorker.addEventListener('message', onMessageHandler); - newWorker.addEventListener('error', onErrorHandler); - newWorker.addEventListener('messageerror', onMessageErrorHandler); + newWorker = + typeof globalThis.Worker === 'function' + ? createBrowserWorker(source, handlers) + : createNodeWorker(source, handlers); worker = newWorker; return newWorker; })(); - - workerPromise.catch(() => { - workerPromise = null; + startup.catch(() => { + if (workerPromise === startup) workerPromise = null; }); + workerPromise = startup; - return workerPromise; + return startup; } export async function callWorker( @@ -149,9 +137,11 @@ export async function callWorker( }); } catch (err) { const entry = pending.get(id); - if (entry) clearTimeout(entry.executionTimer); - pending.get(id)?.reject(err as Error); - pending.delete(id); + if (entry) { + clearTimeout(entry.executionTimer); + pending.delete(id); + entry.reject(err as Error); + } } return promise; } @@ -160,11 +150,9 @@ export function terminateWorker( reason: Error = new Error('Worker terminated'), ): void { if (worker) worker.terminate(); - if (blobUrl) globalThis.URL.revokeObjectURL(blobUrl); worker = null; workerPromise = null; - blobUrl = null; for (const [id, entry] of pending) { clearTimeout(entry.executionTimer); diff --git a/packages/shell-bson-parser/src/worker-source.ts b/packages/shell-bson-parser/src/worker-source.ts new file mode 100644 index 000000000..043c06348 --- /dev/null +++ b/packages/shell-bson-parser/src/worker-source.ts @@ -0,0 +1,4 @@ +// Placeholder, overwritten in dist with the bundled worker code by +// webpack.worker.config.cjs so the worker never has to be resolved by path. +const workerSource = ''; +export default workerSource; diff --git a/packages/shell-bson-parser/src/worker-types.ts b/packages/shell-bson-parser/src/worker-types.ts index 41ff331cc..7bda84170 100644 --- a/packages/shell-bson-parser/src/worker-types.ts +++ b/packages/shell-bson-parser/src/worker-types.ts @@ -9,3 +9,13 @@ export type WorkerResponse = { id: number } & ( | { ok: true; result: TrackedPayload } | { ok: false; error: Error; errorProps: Record } ); + +export type WorkerCallbacks = { + onMessage: (response: WorkerResponse) => void; + onError: (err: Error) => void; +}; + +export type WorkerHandle = { + postMessage: (message: unknown) => void; + terminate: () => void; +}; diff --git a/packages/shell-bson-parser/src/worker.ts b/packages/shell-bson-parser/src/worker.ts index 095cbb1a3..137ac5976 100644 --- a/packages/shell-bson-parser/src/worker.ts +++ b/packages/shell-bson-parser/src/worker.ts @@ -4,6 +4,10 @@ import { trackBSON, untrackBSON } from './structured-clone-bson.js'; import type { WorkerRequest, WorkerResponse } from './worker-types.js'; const { self, postMessage } = globalThis; +const parentPort = + typeof process !== 'undefined' && process.getBuiltinModule + ? process.getBuiltinModule('worker_threads')?.parentPort + : null; // Exported for tests export const ALLOWED_GLOBALS = new Set([ @@ -82,7 +86,14 @@ export function handleRequest(request: WorkerRequest): WorkerResponse { } } -if (typeof self !== 'undefined') { +if (parentPort) { + const postMessage = parentPort.postMessage.bind(parentPort); + parentPort.on('message', (request: WorkerRequest) => { + postMessage(handleRequest(request)); + }); + restrictObjectPrototype(); + restrictGlobalScope(globalThis); +} else if (typeof self !== 'undefined') { const sendResponse = postMessage.bind(self); self.addEventListener('message', (event: MessageEvent) => { sendResponse(handleRequest(event.data)); diff --git a/packages/shell-bson-parser/test/fixtures/slow-worker.mjs b/packages/shell-bson-parser/test/fixtures/slow-worker.js similarity index 76% rename from packages/shell-bson-parser/test/fixtures/slow-worker.mjs rename to packages/shell-bson-parser/test/fixtures/slow-worker.js index c1c47b89a..a6ac6a934 100644 --- a/packages/shell-bson-parser/test/fixtures/slow-worker.mjs +++ b/packages/shell-bson-parser/test/fixtures/slow-worker.js @@ -1,21 +1,22 @@ // Avoiding the import of these two functions, // adding them here to keep this file self-contained +const { parentPort } = require('worker_threads'); + function untrackBSON(value) { return value.data; } function trackBSON(value) { return { data: value, bsonTypes: new Map() }; } -self.onmessage = (event) => { - const { id, args } = event.data; +parentPort.on('message', ({ id, args }) => { const [delayMs] = untrackBSON(args); const start = Date.now(); while (Date.now() - start < delayMs) { // noop } - self.postMessage({ + parentPort.postMessage({ id, ok: true, result: trackBSON('done'), }); -}; +}); diff --git a/packages/shell-bson-parser/webpack.cjs.config.cjs b/packages/shell-bson-parser/webpack.cjs.config.cjs index 821719c2b..e1de78288 100644 --- a/packages/shell-bson-parser/webpack.cjs.config.cjs +++ b/packages/shell-bson-parser/webpack.cjs.config.cjs @@ -1,6 +1,5 @@ 'use strict'; const path = require('path'); -const webpack = require('webpack'); const distDir = path.resolve(__dirname, 'dist'); const cjsDir = path.join(distDir, 'cjs'); @@ -10,12 +9,7 @@ module.exports = { target: 'node', entry: { index: path.join(distDir, 'index.js'), - 'web-worker': { - import: 'web-worker', - library: { type: 'commonjs2', export: 'default' }, - }, }, - resolve: { conditionNames: ['import', 'node', 'default'] }, output: { path: cjsDir, clean: true, @@ -27,31 +21,16 @@ module.exports = { /^acorn/, /^javascript-stringify/, /^bson/, - ({ request, contextInfo }, callback) => { - if (request === 'web-worker' && contextInfo.issuer) { - return callback(null, 'commonjs ./web-worker.js'); - } - callback(); - }, + ({ request }, callback) => + request === './worker-source.js' + ? callback(null, 'import ../worker-source.js') + : callback(), ], module: { parser: { javascript: { - url: false, - import: false, importExportsPresence: 'warn', }, }, }, - plugins: [ - new webpack.DefinePlugin({ - 'import.meta.url': webpack.DefinePlugin.runtimeValue( - ({ module }) => - module.resource === path.join(distDir, 'worker-client.js') - ? 'require("node:url").pathToFileURL(require("node:path").join(__dirname, "..", "worker-client.js")).href' - : 'require("node:url").pathToFileURL(__filename).href', - true, - ), - }), - ], }; diff --git a/packages/shell-bson-parser/webpack.worker.config.cjs b/packages/shell-bson-parser/webpack.worker.config.cjs index 0f736a65e..66ba8d378 100644 --- a/packages/shell-bson-parser/webpack.worker.config.cjs +++ b/packages/shell-bson-parser/webpack.worker.config.cjs @@ -1,12 +1,30 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); +const distDir = path.resolve(__dirname, 'dist'); + +// Inlines the bundled worker as a string module, so consumers never have to +// resolve worker.js by path (which breaks once they bundle this package). +class InlineWorkerSourcePlugin { + apply(compiler) { + compiler.hooks.afterEmit.tap('InlineWorkerSourcePlugin', () => { + const code = fs.readFileSync(path.join(distDir, 'worker.js'), 'utf8'); + fs.writeFileSync( + path.join(distDir, 'worker-source.js'), + `const workerSource = ${JSON.stringify(code)};\nexport default workerSource;\n`, + ); + fs.rmSync(path.join(distDir, 'worker-source.js.map'), { force: true }); + }); + } +} + module.exports = { mode: 'production', target: 'webworker', - entry: path.resolve(__dirname, 'dist', 'worker.js'), + entry: path.join(distDir, 'worker.js'), output: { - path: path.resolve(__dirname, 'dist'), + path: distDir, filename: 'worker.js', }, module: { @@ -18,4 +36,5 @@ module.exports = { }, }, }, + plugins: [new InlineWorkerSourcePlugin()], };