Any tools for developing modular serviceworker in typescript?

819 Views Asked by At

I would like to share source modules (classes) between the web app and the service worker but cannot find a way to do that using typescript alone. When using amd, as soon as I import a module the "self" is no longer on the initial thread. All that module source needs to be loaded synchronously, unlike my web app, where I can require() it in async. I tried using es2015 module instead of amd but ran into "Cannot use import statement outside a module". Even if it succeeded I would have the same issue I am having with amd. There seems to be no getting around the need to use importScripts() from a service worker.

If I could instruct typescript that I am targeting a worker from a tsconfig.json file it could, in theory, generate serviceworker-style imports but I see no such support. So to my question:

Is there a way to build a modular service worker solution from typescript?

Fail:

import { Module1 } from "./mods/module1.js";

self.addEventListener("install", (event: any) => {
  new Module1("from index");
});

I suspect there is no good answer since importScripts does not return anything. Those script need to register with a repo. If almond.js had a synchronous mode I think that would solve my problem, since Typescript generates this:

define(["require", "exports", "./mods/module1.js"], function (require, exports, module1_js_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    self.addEventListener("install", function (event) {
        new module1_js_1.Module1("from index");
    });
});

If require() were not async I could get access to "self" on the same thread and things should go smoothly. Removing setTimeout() from almond.js breaks the loader.

UPDATE: there is a pattern that works using amd and almond loader:

self.importScripts("../node_modules/almond/almond.js");
self.addEventListener("install", event => {
  requirejs(["worker/index"], (worker: { run: () => void }) => worker.run());
});

I am just now developing it so may run into limitations. worker/index.ts looks like this:

import {Module1} from "../mods/module1";
export function run() {
    new Module1("running from worker");
}

And therefore returns the developer back to standard typescript modules.

UPDATE:

The following tsconfig.json worked great:

{
  "compilerOptions": {
      "outFile": "./app/service_worker.js",
    "target": "ES2015",
    "module": "amd",
    "strict": true,
    "inlineSourceMap": true
  },
  "files": ["./app/service_worker.ts","./app/serviceworker/index.ts"]
}

Using this package.json:

{
  "name": "pwa-poc",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "server": "cd server && tsc && node server.js",
    "worker": "tsc -p tsconfig-worker.json",
    "test": "cd tests && tsc -p tsconfig.json",
    "app": "tsc -p tsconfig.json",
    "watch:worker": "npm run worker -- -w",
    "watch:test": "npm run test -- -w",
    "watch:app": "npm run app -- -w",
    "all": "npm-run-all -p server watch:worker watch:app watch:test"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "almond": "^0.3.3",
    "typescript": "^3.8.2",
    "xml2js": "^0.4.23"
  },
  "devDependencies": {
    "@types/chai": "^4.2.10",
    "@types/mocha": "^7.0.2",
    "chai": "^4.2.0",
    "mocha": "^7.1.0",
    "npm-run-all": "^4.1.5"
  }
}

The important dependency is almond. The resulting index.js contains all the amd define instances and almond implements the define method.

The index.html file loads the packages:

<body class="theme">
    <script src="../static/almond.js"></script>
    <script src="index.js"></script>
    <script>
        window.addEventListener('load', () => {
            require(["index"], App => {
                App.run();
            });
        });
    </script>
</body>

The run() is available because index.ts exports it:

import { TableExplorer } from "./ux/TableExplorer";
import { MetaForm } from "./ux/MetaForm";
import { DataGrid } from "./ux/DataGrid";
import { saveFormData } from "./ux/saveFormData";
import { getServices } from "./fun/getServices";

export async function run() {
  navigator.serviceWorker.register("./service_worker.js");
  const params = new URLSearchParams(window.location.search);
  const table = params.get("table");
  const family = params.get("family") || "";
  const offline = !!params.get("offline");

  // ask service work to install all metadata
  const registration = await navigator.serviceWorker.ready;
  offline && registration.sync.register("offline");

  const services = getServices();

  if (!table) {
    const explorer = new TableExplorer({ services, family, useLinks: true });
    document.body.appendChild(await explorer.render());
    return;
  }

  document.title = table;
  const form = new MetaForm({ family, table, services });
  await form.render();
  document.body.appendChild(form.form);
  form.form.onsubmit = event => {
    saveFormData({ family, table, services, form });
    event.preventDefault();
    grid.refresh();
  };

  const grid = new DataGrid({ family, table, services });
  await grid.render();
  document.body.appendChild(grid.grid);
}

tsconfig-worker.json:

{
  "compilerOptions": {
      "outFile": "./app/service_worker.js",
    "target": "ES2015",
    "module": "amd",
    "strict": true,
    "inlineSourceMap": true
  },
  "files": ["./app/service_worker.ts","./app/serviceworker/index.ts"]
}

And finally, service_worker.ts:

declare var requirejs: Function;

self.importScripts("../static/almond.js");
requirejs(["serviceworker/index"], (worker: { run: (worker:any) => void }) => worker.run(self));
1

There are 1 best solutions below

1
frank-dspeed On

The only solution is to use rolluo with plugin typescript and then transpil to systemjs it has a ServiceWorker Compatible loader that allows ESM style import inside workers other module systems may also work eg: AMD UMD but this all defends the rolling Update concept of a serviceworker.

Importent general info about ESM Support in Browser Workers

ESM even when supported in the browser it self is till today most time not supported in workers and service workers but systemjs is a shim that works.

Importent general Info why there is no Module Support

As Service Workers are designed to be deployed via Rolling Updates a module system would often break the deployment

so do not use modules always deploy a SingleFileServiceWorker.js

you can register diffrent serviceWorker.js files per Path there can be only one per Path.

async import and download from modules inside the service worker is highly recommended to be avoided at any cost.