parcel: Parcel breaks `import.meta.url` in worker entry files

🐛 bug report

Parcel breaks trampoline code for worker instantiation:

// index.js
const workerURL = (await import("./worker.js")).WORKER_ENTRY_FILE_URL;
const blob = new Blob([`import "${workerURL}";`], { type: "text/javascript" });
new Worker(URL.createObjectURL(blob), { type: "module" });

// worker.js
export const WORKER_ENTRY_FILE_URL = import.meta.url;

🤔 Expected Behavior

The worker is instantiated properly, as it does when the source code is served either directly or with other tools:

Command URL
npx serve (or any static file server) http://localhost:3000/src/
npx vite http://localhost:3000/src/
npx wmr http://localhost:8080/src/
npx esbuild src/index.js --format=esm --bundle --splitting --servedir=src http://localhost:8000/src/

😯 Current Behavior

Parcel errors with:

Not allowed to load local resource: file:///src/worker.js

💁 Possible Solution

There should be a way to preserve the semantics of import.meta.url.

At the very least, it would be sufficient in our case if import.meta.url is left untransformed when it is assigned to an export.

🔦 Context

We maintain a library that:

  1. is used in node and on the web (in particular, is available from a CDN), and
  2. must use web workers for acceptable end-user experience, and
  3. is published and consumed by bundlers through npm.

Unfortunately, instantiating workers in a portable way is challenging. The trampoline approach is the closest to a universal approach.

💻 Code Sample

See https://github.com/lgarron/parcel-import-meta-url-bug for a runnable repro:

git clone https://github.com/lgarron/parcel-import-meta-url-bug && cd parcel-import-meta-url-bug
npx parcel src/index.html

🌍 Your Environment

Software Version(s)
Parcel 2.2.1
Node v17.4.0
npm/Yarn npm 8.3.1
Operating System macOS 12.2

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 1
  • Comments: 24 (7 by maintainers)

Most upvoted comments

As far as I know, this is still relevant.

though perhaps old, this is still relevant

Yeah, it’s been over a year and the choice continues to be:

With both approaches it’s “obvious” how to maintain correctness with standard code splitting, so it’s a matter of the ecosystem settling on a cross-compatible solution.

For now, we’ve given up on Parcel compatibility, given that this issue is stalled.

I have read throughout this conversation and I do understand the maintainers and the users point of view somehow.

I just want to share some insights from using webpack for a similar issue.

we wanted to develop the worker script in typescript rather than in javascript. so webpack/tsc needs to transpile the typescript into javascript first and than provide a link to that javascript file, that works within a deployed environment - so no file:/// links. also we out-sourced the shared worker into it’s own library, so multiple applications can use it, instead of having the same shared worker file duplicated.

instance SharedWorker:

return new SharedWorker(
  new URL(
    '@my/api/shared-worker',
    import.meta.url
  ) /* webpackChunkName: 'shared-worker' */,
  {
    type: 'module',
    name: 'My Shared Worker',
  }
);

the interessting part here is, that the combination new SharedWorker(new URL('xxx', import.meta.url)) must be written like this. it is not possible put any of those terms into a variable. otherwise webpack will not produce a working worker, or will even prevent it from compiling…

it seems webpack handles to the combination of (Shared)Worker and URL specially to just fullfill the usecase.

https://webpack.js.org/guides/web-workers/

more from the example - in case someone stumbles across this.

tsconfig.ts

...
  "@my/api/shared-worker": [
    "libs/api/worker/shared-worker.ts"
  ],
...

api module`s index.ts

....
// May not be exported because otherwise the worker will not be executed in the web worker context.
// But must be imported so that the file is compiled.
import * as SharedWorker from '@my/api/shared-worker';

shared-worker.ts

imports ...

export default self.onconnect = (event: MessageEvent<any>) => {
  const [port] = event.ports;
  start(port);
};

if ('DedicatedWorkerGlobalScope' in self) start(self as unknown as MessagePort);

function start(port: MessagePort) {
  // do worker stuff
  port.postMessage({ data: 'data' })
  port.onmessage = (e: MessageEvent) => {
      messageHandler(e, port, subscriptions, dpConnectSubs);
  };
}