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));