Parcel: 馃檵 Service worker support

Created on 18 Dec 2017  路  20Comments  路  Source: parcel-bundler/parcel

Since service workers are a big part of the "spa web" and especially PWA:s I think that parcel should support it out of the box. As mentioned in https://github.com/parcel-bundler/parcel/issues/301 and according to the MDN docs:

The register() method of the ServiceWorkerContainer interface creates or updates a ServiceWorkerRegistration for the given scriptURL.

The key point to take away being that a service worker has to be provided to the browser via a URL.
And as it's shown in this SO answer, it can't be provided as a data URI. Witch only leaves one option, to compile and bundle a second bundle with the service worker as its entry point.

_Example of registration a service worker:_

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./service-worker.js')
        .then(function(registration) {
            console.log('Registration successful, scope is:', registration.scope);
        })
        .catch(function(error) {
            console.log('Service worker registration failed, error:', error);
        });
}

馃 Expected Behavior

The service worker should be built and bundled on its own, and its name should remain the same as it was prior to the bundling.

馃槸 Current Behavior

Currently the service worker is excluded from the build process, unless explicitly imported, in which case the service worker ends up as part of the main bundle, which is not what we want.

馃拋 Possible Solution

IMO this could be implemented in one of three ways:

  1. Supplying parcel with the service worker entry point through a command line argument.
    This would make it declarative and easily distinguishable from the rest of the code base. It would also allow parcel to supply a "default" service worker if none is provided. _(as suggested by kennetpostigo)_
  2. Following the implementation of https://github.com/parcel-bundler/parcel/issues/189 the service worker entry point could simply be supplied along side the apps entry point.
    This would reduce the amount of work needed in order to implement this issue, as well as limiting parcels dependency on the service worker spec. However this would remove the option of supplying a default service worker.
  3. Searching through all source files for a reference to the service worker registration API, and then dynamically including the service worker as its own entry point.
    This would remove any integration friction in setting up a service worker with parcel. However this would require allot of work and could in many cases end up being more of a problem than an asset. This would also tightly couple parcel to the service worker registration API.

馃敠 Context

I'm am writing a template for typescript and react based web applications in which I use parcel for building and bundling. And I'm now extending the template to include the fundamentals of a PWA, including a service worker.

Feature

Most helpful comment

I also took an approach for this.
My attempt was to use an offline.html in which I linked my entry files (index.js, etc) and also included blank hyperlinks pointing to my service-worker.js which then gets put into dist.
The filenames stay the same, except if parcel renames them based on hashing, etc

basically saying if you want your service-worker bundled but not in your main module,
use a html entry file using

<a href="./relativeFile"></a>
<!--optionally style it invisible (they dont get displayed anyway) -->
<a href="./relativeFile" style="display:none"></a>

My approach can be found here: https://github.com/peanutbother/parcel-serviceworker-example
Example on heroku: https://parcel-serviceworker-example.herokuapp.com/

All 20 comments

Can this be accomplished via code-splitting?

@davidnagli As it says in the parcel docs:

Code splitting is controlled by use of the dynamic import() function syntax proposal, which works like the normal import statement or require function, but returns a Promise.

The dynamic import function functions just the same as an import statement in this respect. Aka, the service worker would end up being part of the main bundle. Or am I misunderstanding the docs?
Also the service worker has to be provided to the browser via a __URL__ and not as an imported module.

As I see it, workers (both service and web) should just be babelized and moved to the dist folder.

  • Service Worker. register ('sw.js')
  • new Worker ('someWorker. j's')
  • ImportScripts ('someJSFile. j's')
    Handling these three things natively is something missing in most module-bundler. While using webpack I'm doing it with a small hack based on file-loader. But it's really specific. I would like something which is not ...

398 adds support for navigator.serviceWorker.register('./worker.js'). It does not preserve the name of the worker script. The register call will be re-written with the name of the worker bundle ([...hash...].js)

@jdanyow will this work for both JS and TS?
Aka, will it detect the registration in both JS and TS. And will it transpile both JS and TS workers?

@Olian04 yes 馃憤

Merged #398 which adds support for detecting calls to navigator.serviceWorker.register. Closing this issue.

Would someone like to do a PR to detect new Worker('somescript.js') calls as well? importScripts doesn't seem terribly useful since you could just use require/import in parcel, but willing to be proven wrong there. Thoughts?

Agreed- importScripts wouldn't be useful.

Here's the PR for new Worker(...): #441

Yeah indeed, importScripts isn't very useful, but it's standard ...

And there is also SharedWorker to support : https://developer.mozilla.org/fr/docs/Web/API/SharedWorker

@BrieucP I intentionally left SharedWorker out the PR because it's not well supported. Should be pretty easy to add if necessary.

Regarding importScripts(...), I'm guessing some folks would want it to work like System.import(...) (separate bundle) while others would want it to work like import ... from ... (same bundle) and still others might want parcel to ignore those expressions entirely. Maybe a separate RFC should be opened for this so that feedback can be gathered.

I also took an approach for this.
My attempt was to use an offline.html in which I linked my entry files (index.js, etc) and also included blank hyperlinks pointing to my service-worker.js which then gets put into dist.
The filenames stay the same, except if parcel renames them based on hashing, etc

basically saying if you want your service-worker bundled but not in your main module,
use a html entry file using

<a href="./relativeFile"></a>
<!--optionally style it invisible (they dont get displayed anyway) -->
<a href="./relativeFile" style="display:none"></a>

My approach can be found here: https://github.com/peanutbother/parcel-serviceworker-example
Example on heroku: https://parcel-serviceworker-example.herokuapp.com/

@jdanyow Hi jeremy, Is there any plan to add SharedWorker support to parcel?

Why are there downvotes for @peanutbother's example?

I guess my approach was not fitting for the purpose of those people. Reactions are useful in either way, but a little explanation might be more helpful.

It might be nicer to use some sort of link tag with a custom parcel-specific rel in the head section, rather than an a tag
Unless there's a link[rel] that is supposed to be used for ServiceWorkers...

the traditional usage would go as follows: https://github.com/parcel-bundler/parcel/issues/1957

I'm actually exporting a script that takes a config object and runs the navigator.... Is there a way to "manually" trigger waking up Parcel to the service worker instead of relying on code exposed to the scope of an import?

Here's a snippet of my code:

export const registerBackup = (config = defaultConfig) =>
  navigator.serviceWorker.register(config.SW_file).then(async worker => {
...

I'm not sure if it's the best way to hack this, but if I add a redundant inclusion of the SW file above the function that actually uses the worker, parcel works 馃

+ navigator.serviceWorker.register('sw.js')

export const registerBackup = (config = defaultConfig) =>   
navigator.serviceWorker.register(config.SW_file).then(async worker => {
...

Parcel [1.12.4] is bundling my application to a nested folder inside the dist one, something like this: /dist/__/client/container/App/serviceWorker.js. I did this to solve my problem:

navigator.serviceWorker.register('./serviceWorker.js').then(() => {
  console.log('Parcel will not work here, service worker will be called by http!')
}).catch(() => {
  navigator.serviceWorker.register('http://localhost:3000/__/src/client/containers/App/serviceWorker.js')
    .then(() => {
      console.log('Service worker registered by http!')
    }).catch(() => {
      console.error('Failed to register service worker!')
    })
})

Better then putting the sw.js inside the dist folder. Hope it helps!

I was trying to register a service worker as officially recommended above (https://github.com/parcel-bundler/parcel/issues/331#issuecomment-354314502) but my problem was that Parcel (or Babel behind it) adds a bunch of _invalid_ JS-code into my serviceWorker.js e.g. window, Websockets (for HMR), etc. This causes errors as window and such do not exist within the workers' scope.

Here is how I solved this problem:
1) Install concurrently and copy-and-watch:

npm i concurrently --save-dev
npm i copy-and-watch --save-dev

2) In package.json add:

"scripts": {
  "dev": "concurrently \"parcel public/index.html --port 5000\" \"copy-and-watch --watch src/workers/serviceWorker.js dist\"",
  "build": "parcel build public/index.html && copy-and-watch src/workers/serviceWorker.js dist"
}

How it works:
copy-and-watch simply copies your serviceWorker.js into dist w/o any changes. And concurrently allows running copy-and-watch and parcel simultaneously on npm run dev

Was this page helpful?
0 / 5 - 0 ratings

Related issues

davidnagli picture davidnagli  路  3Comments

will-stone picture will-stone  路  3Comments

Niggler picture Niggler  路  3Comments

medhatdawoud picture medhatdawoud  路  3Comments

jzimmek picture jzimmek  路  3Comments