README.md

Service worker #

A module service worker that strips .ts/.mts module requests on the way to the page, so the page imports TypeScript with no build step.

pnpm build   # if build/*.wasm is missing
node examples/service-worker/serve.mjs   # then open http://localhost:8000/examples/service-worker/

The page shows a greeting and a table with the cache status of each module.

How it works #

  • Matching. The worker handles same-origin GET requests whose path ends in .ts or .mts. It matches on the extension, not the response's Content-Type, because servers label TypeScript inconsistently (video/mp2t is common). It always responds with text/javascript, because module scripts need a JavaScript MIME type.
  • Mode. Every module goes through downlevel mode, so enums, namespaces, parameter properties, import = and export = all convert without a scan or a retry. Lines match the source everywhere, and columns match outside converted constructs, so DevTools positions under the .ts URL need no source map. The cost is that the worker loads the larger engine: 17,844 B gzip, compared with 12,610 B for erase mode.
  • Streaming. Each upstream body streams through transform, and the page receives pieces as they're produced.
  • Errors. Downlevel mode leaves every construct it reports in place: a <T> cast before an ASI-sensitive return/throw/yield, or import x =/export = in a file with ESM syntax. None of them is valid JavaScript, so the browser throws its own SyntaxError at that line, for example Unexpected token '<'. The worker passes onError so that the generator doesn't throw mid-stream, which would make the import fail as a network error. It also logs the offset to the worker console. A result with errors is not cached.
  • Concurrency. The browser requests a module's imports all at once, and each miss streams on its own engine instance from typestrip's pool. The worker drains each upstream body as fast as it arrives, not as fast as the page reads it, so the instance goes back to the pool and the cache write happens even if the page stops reading.
  • Caching. Stripped output goes into Cache Storage together with the upstream ETag. On the next request, the worker sends If-None-Match with cache: "no-store". A 304 serves the cached copy without stripping again. If the network fails, the worker falls back to the cached copy. Responses without an ETag are stripped every time. To clear the cache after rebuilding the engine, bump CACHE in sw.js.

Limits #

  • Import specifiers need the extension: import "./greet.ts" works, but extensionless ./greet does not resolve. Bare specifiers need an import map.
  • The worker controls the page only after it activates. index.html waits for controllerchange before it imports app/main.ts.
  • This example was tested in Chromium. Firefox and Safari support for module service workers (register(url, { type: "module" })) is untested here. For a host without module service workers, bundle sw.js into a classic script.