GoogleChromeLabs / worker-plugin

👩‍🏭 Adds native Web Worker bundling support to Webpack.
https://npm.im/worker-plugin
Apache License 2.0
1.92k stars 79 forks source link

[feature request] Cross domain blob building fallback #36

Open mizchi opened 5 years ago

mizchi commented 5 years ago

I tried to publish 3rd party script with webpack and worker-plugin.

https://cdn.example.com/main.js <- entry
https://cdn.example.com/sub.js <- chunk
https://cdn.example.com/0.worker.js <- worker

I set output.publicPath to "https://cdn.example.com/" in this case;

But I can not exec worker because of cross domain restriction.

> new Worker("http://localhost:8080/0.worker.js")
VM84:1 Uncaught DOMException: Failed to construct 'Worker': Script at 'http://localhost:8080/0.worker.js' cannot be accessed from origin 'https://www.google.com'.

I know this fallback works to avoid it.

// ASSET_HOST="https://cdn.example.com/" webpack --mode production
if (process.env.ASSET_HOST === location.protocol + "//" + location.host) {
    return new Worker(process.env.ASSET_HOST + "0.worker.js")
} else {
    const code = await fetch(process.env.ASSET_HOST + "0.worker.js").then(res =>
      res.text()
    );
    // console.log(t);
    const blob = new Blob([code], { type: "text/javascript" });
    const url = URL.createObjectURL(blob);
    return worker = new Worker(url);
}

but publisher need to add CORS header to fetch. (Most CDN have CORS header)

I will fork and try it at first in my hand.

developit commented 5 years ago

Note for folks finding this: this is a proposal, inline doesn't yet exist

This isn't because of CORS, but rather because sites like google.com tend to disallow all subresources using something like a Content Security Policy. The issue with CSP is that most configurations you find in the wild also block Blob and Data URL sources, so none of these solutions would work.

CSP issues aside, I wonder if your use-case an inline Blob would work? It would be easy to add an option for this in worker-plugin. It would look something like this:

input: (your source code)

const w = new Worker('./my-worker.js', {
  type: 'module',
  inline: true  // <-- special property observed by the plugin
})

compiled output:


var w = new Worker(URL.createObjectURL(
    new Blob(["onmessage=e=>postMessage('pong')"])
));
            // ^ bundled worker code compiled into main JS as a string
TotallWAR commented 5 years ago

@developit I tried setup you suggested but it doesn't work.

const worker = new Worker('./worker.js', {
      inline: true,
      type: 'module'
});

ia have next error: Uncaught DOMException: Failed to construct 'Worker': ..... cannot be accessed from origin......

jackyef commented 4 years ago

@developit Adding an inline option would be great! I came from using workerize-loader, then comlink-loader, and now trying to move to worker-plugin + comlink because the formers are no longer actively maintained. This is the only thing keeping me from moving.

piotrblasiak commented 4 years ago

Same here - inline mode is the one thing stopping me from moving from the seemingly unmaintained worker-loader.

alangdm commented 4 years ago

Does anyone have a solution for this? The inline flag doesn't seem to do anything, compiled code just ends up like this:

var s=new Worker(e,{inline:!0})
developit commented 4 years ago

For the folks commenting about inline not working - I was proposing that feature, it is not something WorkerPlugin currently implements.

Building transparent Blob/inline support is likely possible, but in the meantime I would suggest using something like this to patch Worker for your use-case:

function Worker(url, opts) {
  return self.Worker('data:,importScripts('+JSON.stringify(url)+')', opts);
}

new Worker("./my-worker.js", { type: "module" });

The result will be a Worker instantiated via a data URL (which will mean it has an opaque origin), where the bundled worker script is fetched via importScripts. This may help with CSP, since the request will be validated as "script-src", not "worker-src".

Honestly though, most websites that ship restrictive CSP's also disable Blob, Data URL and eval() scripts. I don't think any solution that tries to "work around" this is going to help much.

alangdm commented 4 years ago

@developit Thanks for your answer, I think the inline feature you propose would be a great enhancement for my use case at least

I tried using the workaround you mentioned but that still couldn't bypass the error

Blobs apparently bypass it successfully though, I've been using worker-loader's blobs so far and they work just fine

But as some of the other comments here mention, if possible, I would rather drop the unmaintained worker-loader for this plugin so it would be amazing if the inline feature became a reality

developit commented 4 years ago

@alangdm try this version:

function Worker(url, opts) {
  var blob = new Blob(['importScripts('+JSON.stringify(url)+')'], { type: 'text/javascript' });
  return self.Worker(URL.createObjectURL(blob), opts);
}

new Worker("./my-worker.js", { type: "module" });

The issue I have with the inline option I proposed above is that "inline" isn't spec'd anywhere and only works if bundled. It breaks the premise of this plugin, which is that it transparently bundles Module Workers.

alangdm commented 4 years ago

@developit I tried doing it as you said but got an error regarding the usage of new, I changed it slightly and the closest I got was by doing this:

export function Worker(url, opts) {
  var blob = new Blob(["importScripts(" + JSON.stringify(url) + ")"], {
    type: "text/javascript"
  });
  return new self.Worker(URL.createObjectURL(blob), opts);
}

new Worker("../workers/my.worker.js", {
  type: "module"
});

Which threw this error in Chrome:

Uncaught TypeError: Failed to execute 'importScripts' on 'WorkerGlobalScope': 
Module scripts don't support importScripts().

Doing it like this also didn't seem to be bundling the worker at all though 😢

The issue I have with the inline option I proposed above is that "inline" isn't spec'd anywhere and only works if bundled. It breaks the premise of this plugin, which is that it transparently bundles Module Workers.

To be honest I agree with you on this, I don't really like that kind of non-standard syntax, but Blobs seem to be the only option for use cases like mine and some of the other people who commented before me

alangdm commented 4 years ago

@developit You can pretty much ignore my last comment, I managed to get this working, thanks a lot!! 💯

The important steps are: Add the following to a script that's directly on the html:

(function() {
  var _Worker = window.Worker;
  window.Worker = function (url, opts) {
    var blob = new Blob(["importScripts(" + JSON.stringify(url) + ")"], {
      type: "text/javascript"
    });
    return new _Worker(URL.createObjectURL(blob), opts);
  }
})();

And on the code actually getting bundled just use it as normally recommended:

new Worker("./my-worker.js", {
  type: "module"
});

(My problem on the last comment was that I was adding the fix as part of the bundled code, that didn't go well)

maksnester commented 4 years ago

I didn't make it work from the first time, so just wanted to clarify that it's not necessary to add that script directly to HTML. It's a hack where you just replace native Worker with your own implementation (with importScript that won't suffer from CORS). So it's ok to just have that in your code:

const _Worker = window.Worker;
window.Worker = function (url, opts) {
  const blob = new Blob(["importScripts(" + JSON.stringify(url) + ")"], {
    type: "text/javascript"
  });
  return new _Worker(URL.createObjectURL(blob), opts);
}
// worker-plugin magic still works, 
// but now you can use CORS, you can specify webpack's publicPath pointing to CDN
new Worker("./my-worker.js", {
  type: "module"
});
window.Worker = _Worker // put it back to not break any other worker usage
developit commented 4 years ago

FWIW I'd be open to adding an option to worker-plugin that outputs the shimmed code.

Better yet, an option to specify "here's how to get the Worker constructor", like:

// to use Node worker_threads:
new WorkerPlugin({
    workerConstructor: `require('web-worker')`
})

// to use the importScripts workaround for CORS/preload:
new WorkerPlugin({
    workerConstructor: `(function(u,o){return new Worker(URL.createObjectURL(new Blob(['importScripts('+JSON.stringify(u)+')'])),o)})`
})
finalljx commented 2 years ago

@developit You can pretty much ignore my last comment, I managed to get this working, thanks a lot!! 💯

The important steps are: Add the following to a script that's directly on the html:

(function() {
  var _Worker = window.Worker;
  window.Worker = function (url, opts) {
    var blob = new Blob(["importScripts(" + JSON.stringify(url) + ")"], {
      type: "text/javascript"
    });
    return new _Worker(URL.createObjectURL(blob), opts);
  }
})();

And on the code actually getting bundled just use it as normally recommended:

new Worker("./my-worker.js", {
  type: "module"
});

(My problem on the last comment was that I was adding the fix as part of the bundled code, that didn't go well)

this works for me, with protocal. url =location.protocol + url;