@inkandswitch/patchwork-bootloader 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * these dependencies will be built into the outdir, and injected into the importmap
3
+ */
4
+ declare const externals: string[];
5
+ export default externals;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * these dependencies will be built into the outdir, and injected into the importmap
3
+ */
4
+ const externals = [
5
+ "@automerge/automerge",
6
+ "@automerge/automerge/slim",
7
+ "@automerge/automerge-repo",
8
+ "@automerge/automerge-repo/slim",
9
+ "@automerge/automerge-repo-keyhive",
10
+ "@keyhive/keyhive",
11
+ "@keyhive/keyhive/slim",
12
+ "@inkandswitch/patchwork-bootloader",
13
+ "@inkandswitch/patchwork-elements",
14
+ "@inkandswitch/patchwork-filesystem",
15
+ "@inkandswitch/patchwork-plugins",
16
+ // sad
17
+ "@codemirror/state",
18
+ "@codemirror/view",
19
+ ];
20
+ export default externals;
@@ -0,0 +1,170 @@
1
+ import * as esbuild from "esbuild";
2
+ export type ContextOptions = {
3
+ keyhiveEnabled: boolean;
4
+ syncServerUrl: string;
5
+ syncServerStorageId: string;
6
+ outdir?: string;
7
+ serviceWorkerPath?: string;
8
+ serviceWorkerType?: WorkerType;
9
+ };
10
+ export declare function getBuildOptions({ keyhiveEnabled, syncServerUrl: syncServer, syncServerStorageId: storageId, outdir, serviceWorkerPath, serviceWorkerType, }: ContextOptions): esbuild.BuildOptions;
11
+ export default function createContext(contextOptions: ContextOptions): Promise<{
12
+ sw: esbuild.BuildContext<{
13
+ entryPoints: string[];
14
+ bundle: boolean;
15
+ format: "esm" | "iife";
16
+ sourcemap: false;
17
+ minify: true;
18
+ splitting?: boolean;
19
+ preserveSymlinks?: boolean;
20
+ outfile?: string;
21
+ metafile?: boolean;
22
+ outdir?: string;
23
+ outbase?: string;
24
+ external?: string[];
25
+ packages?: "bundle" | "external";
26
+ alias?: Record<string, string>;
27
+ loader?: {
28
+ [ext: string]: esbuild.Loader;
29
+ };
30
+ resolveExtensions?: string[];
31
+ mainFields?: string[];
32
+ conditions?: string[];
33
+ write?: boolean;
34
+ allowOverwrite?: boolean;
35
+ tsconfig?: string;
36
+ outExtension?: {
37
+ [ext: string]: string;
38
+ };
39
+ publicPath?: string;
40
+ entryNames?: string;
41
+ chunkNames?: string;
42
+ assetNames?: string;
43
+ inject?: string[];
44
+ banner?: {
45
+ [type: string]: string;
46
+ };
47
+ footer?: {
48
+ [type: string]: string;
49
+ };
50
+ stdin?: esbuild.StdinOptions;
51
+ plugins?: esbuild.Plugin[];
52
+ absWorkingDir?: string;
53
+ nodePaths?: string[];
54
+ legalComments?: "none" | "inline" | "eof" | "linked" | "external";
55
+ sourceRoot?: string;
56
+ sourcesContent?: boolean;
57
+ globalName?: string;
58
+ target?: string | string[];
59
+ supported?: Record<string, boolean>;
60
+ platform?: esbuild.Platform;
61
+ mangleProps?: RegExp;
62
+ reserveProps?: RegExp;
63
+ mangleQuoted?: boolean;
64
+ mangleCache?: Record<string, string | false>;
65
+ drop?: esbuild.Drop[];
66
+ dropLabels?: string[];
67
+ minifyWhitespace?: boolean;
68
+ minifyIdentifiers?: boolean;
69
+ minifySyntax?: boolean;
70
+ lineLimit?: number;
71
+ charset?: esbuild.Charset;
72
+ treeShaking?: boolean;
73
+ ignoreAnnotations?: boolean;
74
+ jsx?: "transform" | "preserve" | "automatic";
75
+ jsxFactory?: string;
76
+ jsxFragment?: string;
77
+ jsxImportSource?: string;
78
+ jsxDev?: boolean;
79
+ jsxSideEffects?: boolean;
80
+ define?: {
81
+ [key: string]: string;
82
+ };
83
+ pure?: string[];
84
+ keepNames?: boolean;
85
+ color?: boolean;
86
+ logLevel?: esbuild.LogLevel;
87
+ logLimit?: number;
88
+ logOverride?: Record<string, esbuild.LogLevel>;
89
+ tsconfigRaw?: string | esbuild.TsconfigRaw;
90
+ }>;
91
+ setup: esbuild.BuildContext<{
92
+ entryPoints: string[];
93
+ format: "esm";
94
+ bundle: false;
95
+ splitting?: boolean;
96
+ preserveSymlinks?: boolean;
97
+ outfile?: string;
98
+ metafile?: boolean;
99
+ outdir?: string;
100
+ outbase?: string;
101
+ external?: string[];
102
+ packages?: "bundle" | "external";
103
+ alias?: Record<string, string>;
104
+ loader?: {
105
+ [ext: string]: esbuild.Loader;
106
+ };
107
+ resolveExtensions?: string[];
108
+ mainFields?: string[];
109
+ conditions?: string[];
110
+ write?: boolean;
111
+ allowOverwrite?: boolean;
112
+ tsconfig?: string;
113
+ outExtension?: {
114
+ [ext: string]: string;
115
+ };
116
+ publicPath?: string;
117
+ entryNames?: string;
118
+ chunkNames?: string;
119
+ assetNames?: string;
120
+ inject?: string[];
121
+ banner?: {
122
+ [type: string]: string;
123
+ };
124
+ footer?: {
125
+ [type: string]: string;
126
+ };
127
+ stdin?: esbuild.StdinOptions;
128
+ plugins?: esbuild.Plugin[];
129
+ absWorkingDir?: string;
130
+ nodePaths?: string[];
131
+ sourcemap?: boolean | "linked" | "inline" | "external" | "both";
132
+ legalComments?: "none" | "inline" | "eof" | "linked" | "external";
133
+ sourceRoot?: string;
134
+ sourcesContent?: boolean;
135
+ globalName?: string;
136
+ target?: string | string[];
137
+ supported?: Record<string, boolean>;
138
+ platform?: esbuild.Platform;
139
+ mangleProps?: RegExp;
140
+ reserveProps?: RegExp;
141
+ mangleQuoted?: boolean;
142
+ mangleCache?: Record<string, string | false>;
143
+ drop?: esbuild.Drop[];
144
+ dropLabels?: string[];
145
+ minify?: boolean;
146
+ minifyWhitespace?: boolean;
147
+ minifyIdentifiers?: boolean;
148
+ minifySyntax?: boolean;
149
+ lineLimit?: number;
150
+ charset?: esbuild.Charset;
151
+ treeShaking?: boolean;
152
+ ignoreAnnotations?: boolean;
153
+ jsx?: "transform" | "preserve" | "automatic";
154
+ jsxFactory?: string;
155
+ jsxFragment?: string;
156
+ jsxImportSource?: string;
157
+ jsxDev?: boolean;
158
+ jsxSideEffects?: boolean;
159
+ define?: {
160
+ [key: string]: string;
161
+ };
162
+ pure?: string[];
163
+ keepNames?: boolean;
164
+ color?: boolean;
165
+ logLevel?: esbuild.LogLevel;
166
+ logLimit?: number;
167
+ logOverride?: Record<string, esbuild.LogLevel>;
168
+ tsconfigRaw?: string | esbuild.TsconfigRaw;
169
+ }>;
170
+ }>;
@@ -0,0 +1,83 @@
1
+ import * as esbuild from "esbuild";
2
+ export function getBuildOptions({ keyhiveEnabled, syncServerUrl: syncServer, syncServerStorageId: storageId, outdir, serviceWorkerPath, serviceWorkerType, }) {
3
+ serviceWorkerType ||= "classic";
4
+ return {
5
+ absWorkingDir: import.meta.dirname,
6
+ outdir,
7
+ define: {
8
+ __CACHE_VERSION__: JSON.stringify(`cache-${new Date().toISOString()}`),
9
+ __SYNC_SERVER_URL__: JSON.stringify(syncServer),
10
+ __SYNC_SERVER_STORAGE_ID__: JSON.stringify(storageId),
11
+ __SERVICE_WORKER_PATH__: JSON.stringify(serviceWorkerPath || "/service-worker.js"),
12
+ __SERVICE_WORKER_TYPE__: JSON.stringify(serviceWorkerType),
13
+ __KEYHIVE_ENABLED__: `${keyhiveEnabled}`,
14
+ },
15
+ };
16
+ }
17
+ export default async function createContext(contextOptions) {
18
+ const sharedOptions = getBuildOptions(contextOptions);
19
+ const sw = await esbuild.context({
20
+ ...sharedOptions,
21
+ entryPoints: ["../template/service-worker/service-worker.ts"],
22
+ bundle: contextOptions.serviceWorkerType != "module",
23
+ format: contextOptions.serviceWorkerType == "module" ? "esm" : "iife",
24
+ sourcemap: false,
25
+ minify: true,
26
+ });
27
+ const setup = await esbuild.context({
28
+ ...sharedOptions,
29
+ entryPoints: ["../template/client/setup.ts"],
30
+ format: "esm",
31
+ bundle: false,
32
+ });
33
+ return { sw, setup };
34
+ }
35
+ if (import.meta.main) {
36
+ const { parseArgs } = await import("node:util");
37
+ const { values: { keyhive, "sync-server": syncServer, "storage-id": storageId, outdir, watch, }, } = parseArgs({
38
+ args: process.argv.slice(2),
39
+ options: {
40
+ keyhive: {
41
+ type: "boolean",
42
+ default: false,
43
+ short: "k",
44
+ },
45
+ "sync-server": {
46
+ type: "string",
47
+ },
48
+ "storage-id": {
49
+ type: "string",
50
+ },
51
+ outdir: {
52
+ type: "string",
53
+ default: process.cwd(),
54
+ short: "o",
55
+ },
56
+ watch: {
57
+ type: "boolean",
58
+ default: false,
59
+ short: "w",
60
+ },
61
+ },
62
+ });
63
+ if (!syncServer || !storageId) {
64
+ throw new Error("--sync-server and --storage-id are required args");
65
+ }
66
+ const { sw, setup } = await createContext({
67
+ keyhiveEnabled: keyhive,
68
+ syncServerUrl: syncServer,
69
+ syncServerStorageId: storageId,
70
+ outdir,
71
+ serviceWorkerPath: "service-worker.js",
72
+ });
73
+ if (watch) {
74
+ await sw.watch();
75
+ await setup.watch();
76
+ }
77
+ else {
78
+ await sw.rebuild();
79
+ await setup.rebuild();
80
+ await sw.dispose();
81
+ await setup.dispose();
82
+ }
83
+ }
@@ -0,0 +1,40 @@
1
+ import type { Plugin } from "vite";
2
+ type Imports = {
3
+ [name: string]: string;
4
+ };
5
+ type ImportMap = {
6
+ imports: Imports;
7
+ scopes?: {
8
+ [scope: string]: Imports;
9
+ };
10
+ };
11
+ export interface PatchworkVitePluginOptions {
12
+ /** the wss:// syncServer URL that'll be connected to in the service-worker */
13
+ syncServerUrl: string;
14
+ /** the storage id for the syncServer, subscribed to in the main thread */
15
+ syncServerStorageId: string;
16
+ /** extra importmap to be merged into the index.html */
17
+ importmap?: ImportMap;
18
+ extraBuiltins?: Record<string, string>;
19
+ /** currently unused */
20
+ serviceWorkerType?: "classic" | "module";
21
+ /** currently unused */
22
+ keyhiveEnabled?: boolean;
23
+ }
24
+ export default function patchwork(options: PatchworkVitePluginOptions): Plugin<any>[];
25
+ /**
26
+ * these dependencies will be built into the outdir, and injected into the importmap
27
+ */
28
+ export declare const defaultBuiltins: {
29
+ "@automerge/automerge": string;
30
+ "@automerge/automerge/slim": string;
31
+ "@automerge/automerge-repo": string;
32
+ "@automerge/automerge-repo/slim": string;
33
+ "@automerge/vanillajs": string;
34
+ "@automerge/vanillajs/slim": string;
35
+ "@keyhive/keyhive": string;
36
+ "@keyhive/keyhive/slim": string;
37
+ "@keyhive/keyhive/keyhive_wasm.base64.js": string;
38
+ };
39
+ export declare function plugin(options: PatchworkVitePluginOptions): Plugin;
40
+ export {};
@@ -0,0 +1,192 @@
1
+ import * as esbuild from "esbuild";
2
+ import { getBuildOptions } from "./generate.js";
3
+ import * as path from "node:path";
4
+ import { createReadStream } from "node:fs";
5
+ export default function patchwork(options) {
6
+ return [plugin(options)];
7
+ }
8
+ /**
9
+ * these dependencies will be built into the outdir, and injected into the importmap
10
+ */
11
+ export const defaultBuiltins = {
12
+ "@automerge/automerge": "/packages/@automerge/automerge/index.js",
13
+ "@automerge/automerge/slim": "/packages/@automerge/automerge/slim.js",
14
+ "@automerge/automerge-repo": "/packages/@automerge/automerge-repo/index.js",
15
+ "@automerge/automerge-repo/slim": "/packages/@automerge/automerge-repo/slim.js",
16
+ "@automerge/vanillajs": "/packages/@automerge/vanillajs/index.js",
17
+ "@automerge/vanillajs/slim": "/packages/@automerge/vanillajs/slim.js",
18
+ "@keyhive/keyhive": "/packages/@keyhive/keyhive/index.js",
19
+ "@keyhive/keyhive/slim": "/packages/@keyhive/keyhive/slim.js",
20
+ "@keyhive/keyhive/keyhive_wasm.base64.js": "/packages/@keyhive/keyhive/keyhive_wasm.base64.js",
21
+ };
22
+ async function generateJavaScript(options) {
23
+ const rb = await esbuild.build({
24
+ ...options,
25
+ write: false,
26
+ outdir: ".",
27
+ });
28
+ const code = rb.outputFiles?.find((x) => x.path.endsWith(".js"))?.text;
29
+ if (code)
30
+ return {
31
+ code,
32
+ map: ["external", false, undefined].includes(options.sourcemap)
33
+ ? // force vite not to generate a sourcemap for the service worker to reduce size
34
+ { mappings: "", names: [], sources: [], version: 0 }
35
+ : undefined,
36
+ };
37
+ }
38
+ /**
39
+ * merge the importmap option with our builtins
40
+ */
41
+ function createImportMap(options) {
42
+ const builtins = Object.assign({}, defaultBuiltins, options.extraBuiltins ?? {});
43
+ const importmap = structuredClone(options.importmap ?? { imports: {}, scopes: {} });
44
+ importmap.imports ??= {};
45
+ importmap.scopes ??= {};
46
+ Object.assign(importmap.imports, builtins);
47
+ return { importmap, builtins };
48
+ }
49
+ export function plugin(options) {
50
+ const { importmap, builtins } = createImportMap(options);
51
+ const serviceWorkerModuleId = "service-worker.js";
52
+ const serviceWorkerSource = path.resolve(import.meta.dirname, "../template/service-worker/service-worker.ts");
53
+ const automergeWasmSource = path.resolve(import.meta.dirname, "../node_modules/@automerge/automerge/dist/automerge.wasm");
54
+ // https://vite.dev/guide/api-plugin.html#importing-a-virtual-file
55
+ const patchworkSetupModuleId = "virtual:patchwork/setup";
56
+ const resolvedPatchworkSetupModuleId = "\0" + patchworkSetupModuleId;
57
+ const patchworkSetupSource = path.resolve(import.meta.dirname, "../template/client/setup.ts");
58
+ const sharedOptions = getBuildOptions({
59
+ keyhiveEnabled: Boolean(options.keyhiveEnabled),
60
+ syncServerStorageId: options.syncServerStorageId,
61
+ syncServerUrl: options.syncServerUrl,
62
+ serviceWorkerPath: serviceWorkerModuleId,
63
+ });
64
+ const serviceWorkerBuildOptions = {
65
+ ...sharedOptions,
66
+ entryPoints: [serviceWorkerSource],
67
+ bundle: options.serviceWorkerType != "module",
68
+ format: options.serviceWorkerType == "module" ? "esm" : "iife",
69
+ minify: true,
70
+ sourcemap: "external",
71
+ };
72
+ const patchworkSetupBuildOptions = {
73
+ ...sharedOptions,
74
+ format: "esm",
75
+ entryPoints: [patchworkSetupSource],
76
+ bundle: false,
77
+ sourcemap: true,
78
+ };
79
+ let viteBuildInfo;
80
+ function shouldPlaceholdKeyhive(id) {
81
+ // if keyhive is disabled,
82
+ // but the setup code is still importing keyhive,
83
+ // then we should emit an empty keyhive file so the build works
84
+ return ((!options.keyhiveEnabled && id.startsWith("@keyhive/")) ||
85
+ [
86
+ "@automerge/automerge-keyhive-network-adapter",
87
+ "@automerge/automerge-repo-keyhive",
88
+ ].includes(id));
89
+ }
90
+ return {
91
+ name: "@patchwork/vite",
92
+ async buildStart() {
93
+ if (this.environment.mode == "build") {
94
+ for (const [id, fileName] of Object.entries(builtins)) {
95
+ if (shouldPlaceholdKeyhive(id)) {
96
+ continue;
97
+ }
98
+ this.emitFile({
99
+ type: "chunk",
100
+ fileName: fileName.slice(1),
101
+ id,
102
+ preserveSignature: "strict",
103
+ });
104
+ }
105
+ this.emitFile({
106
+ type: "asset",
107
+ fileName: "automerge.wasm",
108
+ source: await this.fs.readFile(automergeWasmSource),
109
+ });
110
+ }
111
+ },
112
+ resolveId(id) {
113
+ if (id == `/${serviceWorkerModuleId}` || id == serviceWorkerModuleId) {
114
+ return serviceWorkerModuleId;
115
+ }
116
+ else if (id == patchworkSetupModuleId) {
117
+ return resolvedPatchworkSetupModuleId;
118
+ }
119
+ else if (shouldPlaceholdKeyhive(id)) {
120
+ return id;
121
+ }
122
+ else if (id in importmap.imports && !(id in builtins)) {
123
+ return { id: importmap.imports[id], external: true };
124
+ }
125
+ },
126
+ async load(id) {
127
+ if (id == resolvedPatchworkSetupModuleId) {
128
+ return generateJavaScript(patchworkSetupBuildOptions);
129
+ }
130
+ else if (id == serviceWorkerModuleId) {
131
+ return generateJavaScript(serviceWorkerBuildOptions);
132
+ }
133
+ else if (shouldPlaceholdKeyhive(id)) {
134
+ return "export default {}";
135
+ }
136
+ },
137
+ transformIndexHtml: {
138
+ order: "pre",
139
+ handler(html, ctx) {
140
+ const map = structuredClone(importmap);
141
+ if (ctx.server) {
142
+ // serve builtins from dev server in dev
143
+ // mode
144
+ for (const id of Object.keys(builtins)) {
145
+ map.imports[id] = `/@id/${id}`;
146
+ }
147
+ }
148
+ return {
149
+ html,
150
+ tags: [
151
+ {
152
+ tag: "script",
153
+ attrs: { type: "importmap" },
154
+ children: JSON.stringify(map, null, 2),
155
+ },
156
+ ],
157
+ };
158
+ },
159
+ },
160
+ configResolved(config) {
161
+ viteBuildInfo = config.build;
162
+ },
163
+ configureServer: {
164
+ handler(server) {
165
+ server.middlewares.use((request, response, next) => {
166
+ const url = new URL(request.url, "http://example.com");
167
+ if (url.pathname == "/automerge.wasm") {
168
+ response.setHeaders(new Headers({
169
+ "content-type": "application/wasm",
170
+ }));
171
+ createReadStream(automergeWasmSource).pipe(response);
172
+ }
173
+ else {
174
+ next();
175
+ }
176
+ });
177
+ },
178
+ },
179
+ closeBundle: {
180
+ sequential: true,
181
+ async handler(error) {
182
+ if (error) {
183
+ throw error;
184
+ }
185
+ await esbuild.build({
186
+ ...serviceWorkerBuildOptions,
187
+ outfile: path.resolve(viteBuildInfo.outDir, "service-worker.js"),
188
+ });
189
+ },
190
+ },
191
+ };
192
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,164 @@
1
+ /// <reference types="service-worker-types" />
2
+ let cachename = "default";
3
+ let debugging = false;
4
+ function log(...args) {
5
+ if (!debugging)
6
+ return;
7
+ console.log.call(console, `%cpatchwork:serviceworker%c\n`, `color: #00ffcc; font-weight: bold`, "color: inherit", ...args);
8
+ }
9
+ self.addEventListener("install", () => self.skipWaiting());
10
+ async function clearOldCaches() {
11
+ const cacheWhitelist = [cachename];
12
+ const cacheNames = await caches.keys();
13
+ const deletePromises = cacheNames.map((cacheName) => {
14
+ if (!cacheWhitelist.includes(cacheName)) {
15
+ return caches.delete(cacheName);
16
+ }
17
+ });
18
+ await Promise.all(deletePromises);
19
+ }
20
+ self.addEventListener("activate", async () => {
21
+ await clearOldCaches();
22
+ clients.claim();
23
+ });
24
+ // a map of response promises by their id
25
+ const responseResolvers = new Map();
26
+ function accept(message) {
27
+ const responseItem = responseResolvers.get(message.id);
28
+ if (!responseItem) {
29
+ return console.warn(`No read response found for id ${message.id}`);
30
+ }
31
+ return responseItem.resolve(message.response);
32
+ }
33
+ const bc = new BroadcastChannel("@patchwork/handoff");
34
+ bc.addEventListener("message", (event) => {
35
+ if (event.data.type == "response")
36
+ accept(event.data);
37
+ });
38
+ // when we receive a `response` req, we resolve the promise with that id
39
+ self.addEventListener("message", async (event) => {
40
+ if (event.data.type == "response") {
41
+ accept(event.data);
42
+ }
43
+ else if (event.data.type == "port") {
44
+ log("recieved messagechannel");
45
+ const [port] = event.ports;
46
+ port.addEventListener("message", (event) => {
47
+ if (event.data.type == "response") {
48
+ accept(event.data);
49
+ }
50
+ });
51
+ }
52
+ else if (event.data.type == "cachename") {
53
+ const nextCachename = event.data.cachename;
54
+ if (cachename == nextCachename) {
55
+ return;
56
+ }
57
+ console.info(`deleting ${cachename} and setting cache name to ${nextCachename}`);
58
+ caches.delete(cachename);
59
+ cachename = nextCachename;
60
+ }
61
+ else if (event.data.type == "debug") {
62
+ debugging = event.data.debug;
63
+ log("serviceworker debugging enabled");
64
+ }
65
+ });
66
+ // request ids are kept in a counter
67
+ let reqcount = 0;
68
+ self.addEventListener("fetch", async (fetchEvent) => {
69
+ const request = fetchEvent.request;
70
+ if (request.method !== "GET")
71
+ return fetchEvent.respondWith(fetch(request));
72
+ const url = new URL(fetchEvent.request.url);
73
+ let handoffURL;
74
+ if (url.hostname == self.location.hostname &&
75
+ url.port == self.location.port &&
76
+ url.protocol == self.location.protocol) {
77
+ try {
78
+ // trap any request like /url e.g. /automerge%3Awhatever or /http%3A%2F%2Fsomething.com
79
+ // URI encoded so we can include hashes etc
80
+ handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
81
+ log(`received handoff request ${handoffURL}`);
82
+ }
83
+ catch { }
84
+ }
85
+ fetchEvent.respondWith((async () => {
86
+ const cache = await caches.open(cachename);
87
+ const match = await cache.match(request);
88
+ try {
89
+ if (handoffURL) {
90
+ // cache-first strategy for handoff requests
91
+ if (match)
92
+ return match;
93
+ const client = await self.clients.get(fetchEvent.clientId);
94
+ // set up a request id
95
+ const reqid = reqcount++;
96
+ // create a place for the response event handler to put the response
97
+ const resolvers = Promise.withResolvers();
98
+ responseResolvers.set(reqid, resolvers);
99
+ // i don't think this can happen
100
+ if (!client) {
101
+ throw new Error(`the client has gone missing!!! ${fetchEvent.clientId}. i have NO IDEA what to do`);
102
+ }
103
+ const message = {
104
+ id: reqid,
105
+ type: "request",
106
+ cache: cachename,
107
+ request: {
108
+ url: handoffURL.href,
109
+ headers: Object.fromEntries(request.headers.entries()),
110
+ method: request.method,
111
+ destination: request.destination,
112
+ referrer: request.referrer,
113
+ },
114
+ };
115
+ log("sending handoff request", message);
116
+ // send request event to main thread to ask them how to handle it
117
+ client.postMessage(message);
118
+ // this'll finish when the main thread gets back to us
119
+ fetchEvent.waitUntil(resolvers.promise);
120
+ const handoffResponse = await resolvers.promise;
121
+ log("received handoff response", handoffResponse);
122
+ if (handoffResponse) {
123
+ const response = new Response(handoffResponse.body, {
124
+ status: handoffResponse.status,
125
+ headers: handoffResponse.headers,
126
+ });
127
+ if (handoffResponse.cache !== false) {
128
+ log(`caching ${handoffURL}`);
129
+ await cache.put(request, response.clone());
130
+ }
131
+ else {
132
+ log(`caching disabled on ${handoffURL}`);
133
+ }
134
+ return response;
135
+ }
136
+ // no idea what's going on now i'm a teapot i'm a teapot
137
+ return new Response("handler returned nothing", {
138
+ status: 418,
139
+ });
140
+ }
141
+ else {
142
+ // network first strategy for external requests
143
+ const response = await fetch(request);
144
+ if (response) {
145
+ if (response.ok && response.url.match(/^https?\:/)) {
146
+ await cache.put(request, response.clone());
147
+ }
148
+ return response;
149
+ }
150
+ if (match)
151
+ return match;
152
+ return new Response("couldnt fetch and no stale", { status: 503 });
153
+ }
154
+ }
155
+ catch (error) {
156
+ if (match)
157
+ return match;
158
+ // if something fucked up happens, serve a stale thing if there is one
159
+ // probably can do better error messaging here based on what was caught
160
+ return new Response(`yikes: ${error}`, { status: 555 });
161
+ }
162
+ })());
163
+ });
164
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { HandoffHandler, SetupServiceWorkerOptions } from "./types.js";
2
+ export declare function bumpServiceWorkerCache(sw?: ServiceWorker | null): void;
3
+ export default function setupServiceWorker(handler: HandoffHandler, options?: SetupServiceWorkerOptions): Promise<ServiceWorker | undefined>;
package/dist/setup.js ADDED
@@ -0,0 +1,81 @@
1
+ import debug from "debug";
2
+ const debugging = debug.enabled("patchwork:serviceworker");
3
+ const key = "patchworkServiceWorkerCacheVersion";
4
+ function bumpServiceWorkerCacheVersion() {
5
+ const version = new Date().valueOf().toString(36);
6
+ localStorage.setItem(key, version);
7
+ return getServiceWorkerCacheVersion();
8
+ }
9
+ function getServiceWorkerCacheVersion() {
10
+ return localStorage.getItem(key);
11
+ }
12
+ function getOrCreateServiceWorkerCacheVersion() {
13
+ const existing = getServiceWorkerCacheVersion();
14
+ if (existing)
15
+ return existing;
16
+ return bumpServiceWorkerCacheVersion();
17
+ }
18
+ function setServiceWorkerCacheName(sw) {
19
+ if (!sw) {
20
+ throw new Error("no service worker!");
21
+ }
22
+ sw.postMessage({
23
+ type: "cachename",
24
+ cachename: getOrCreateServiceWorkerCacheVersion(),
25
+ });
26
+ }
27
+ export function bumpServiceWorkerCache(sw = navigator.serviceWorker.controller) {
28
+ bumpServiceWorkerCacheVersion();
29
+ setServiceWorkerCacheName(sw);
30
+ }
31
+ export default async function setupServiceWorker(handler, options) {
32
+ navigator.serviceWorker.addEventListener("controllerchange", function () {
33
+ console.log("%cnew service worker, reloading", "color: pink; font-weight: bold");
34
+ bumpServiceWorkerCache(navigator.serviceWorker.controller);
35
+ location.reload();
36
+ });
37
+ navigator.serviceWorker.addEventListener("message", async (event) => {
38
+ if (event.data.type == "request") {
39
+ const requestMessage = event.data;
40
+ const source = event.source;
41
+ if (!source) {
42
+ throw new TypeError("can't operate without a source");
43
+ }
44
+ async function send(response, transfer) {
45
+ source.postMessage({
46
+ id: requestMessage.id,
47
+ type: "response",
48
+ response,
49
+ }, { transfer });
50
+ }
51
+ const handoffResponse = await handler(requestMessage.request.url, requestMessage.request);
52
+ if (!handoffResponse) {
53
+ return source?.postMessage({ id: requestMessage.id, type: "response" });
54
+ }
55
+ if (typeof handoffResponse == "string") {
56
+ return send({ body: handoffResponse }, [handoffResponse]);
57
+ }
58
+ if (handoffResponse instanceof Uint8Array) {
59
+ return send({ body: handoffResponse }, [handoffResponse.buffer]);
60
+ }
61
+ const { body: handoffBody, headers, status, cache } = handoffResponse;
62
+ const body = handoffBody;
63
+ send({ body, headers, status, cache }, body instanceof Uint8Array ? [body.buffer] : undefined);
64
+ }
65
+ });
66
+ const existingSw = await navigator.serviceWorker.getRegistration();
67
+ return navigator.serviceWorker
68
+ .register(options?.path ?? "/service-worker.js")
69
+ .then(async (sw) => {
70
+ sw.active?.postMessage({
71
+ type: "debug",
72
+ debug: debugging,
73
+ });
74
+ if (!existingSw?.active) {
75
+ bumpServiceWorkerCache(sw.installing);
76
+ queueMicrotask(() => location.reload());
77
+ return sw.active;
78
+ }
79
+ console.log("service worker alive, loading %c patchwork system ", "background: #fff8f0; border: 1px solid; border-radius: 4px");
80
+ });
81
+ }
@@ -0,0 +1,34 @@
1
+ export interface HandoffRequest {
2
+ url: string;
3
+ headers: Record<string, string>;
4
+ method: string;
5
+ destination: RequestDestination;
6
+ referrer: string;
7
+ }
8
+ export interface HandoffResponse {
9
+ body?: string | Uint8Array<ArrayBuffer> | ReadableStream;
10
+ /** defaults to 200 */
11
+ status?: number;
12
+ headers?: [string, string][] | Record<string, string>;
13
+ cache?: boolean;
14
+ }
15
+ export interface HandoffRequestMessage {
16
+ id: number;
17
+ type: "request";
18
+ /** the current name of the service worker cache */
19
+ cachename: string;
20
+ request: HandoffRequest;
21
+ }
22
+ export interface HandoffResponseMessage {
23
+ id: number;
24
+ type: "response";
25
+ response: HandoffResponse;
26
+ }
27
+ export type HandoffHandler = (href: string, request: HandoffRequest) => Promise<HandoffResponse | void | string | Uint8Array<ArrayBuffer>>;
28
+ export type SetupServiceWorkerOptions = {
29
+ /**
30
+ * The public path to the service worker file.
31
+ * Defaults to `/service-worker.js`
32
+ */
33
+ path?: string;
34
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "vite";
2
+ import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
3
+ export declare const builtins: Record<string, string>;
4
+ export declare function importmap(options?: PatchworkVitePluginOptions): Plugin;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * these dependencies will be built into the outdir,
3
+ * and injected into the importmap
4
+ */
5
+ import externals from "../externals.js";
6
+ export const builtins = externals.reduce((builtins, name) => ((builtins[name] = `/packages/${name}.js`), builtins), {});
7
+ /**
8
+ * merge the importmap option with our builtins
9
+ */
10
+ function createImportMap(options) {
11
+ const importmap = structuredClone(options?.importmap ?? { imports: {}, scopes: {} });
12
+ importmap.imports ??= {};
13
+ importmap.scopes ??= {};
14
+ Object.assign(importmap.imports, builtins);
15
+ return { importmap, builtins };
16
+ }
17
+ export function importmap(options) {
18
+ const { importmap, builtins } = createImportMap(options);
19
+ return {
20
+ name: "@patchwork/vite",
21
+ async buildStart() {
22
+ if (this.environment.mode == "build") {
23
+ for (const [id, fileName] of Object.entries(builtins)) {
24
+ this.emitFile({
25
+ type: "chunk",
26
+ fileName: fileName.slice(1),
27
+ id,
28
+ preserveSignature: "strict",
29
+ });
30
+ }
31
+ }
32
+ },
33
+ resolveId(id) {
34
+ if (id in importmap.imports && !(id in builtins)) {
35
+ return { id: importmap.imports[id], external: true };
36
+ }
37
+ },
38
+ transformIndexHtml: {
39
+ order: "pre",
40
+ handler(html, ctx) {
41
+ const map = structuredClone(importmap);
42
+ if (ctx.server) {
43
+ // serve builtins from dev server in dev mode
44
+ for (const id of Object.keys(builtins)) {
45
+ map.imports[id] = `/@id/${id}`;
46
+ }
47
+ }
48
+ return {
49
+ html,
50
+ tags: [
51
+ {
52
+ tag: "script",
53
+ attrs: { type: "importmap" },
54
+ children: JSON.stringify(map, null, 2),
55
+ },
56
+ ],
57
+ };
58
+ },
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,14 @@
1
+ export default function patchwork(options?: PatchworkVitePluginOptions): import("vite").Plugin<any>[];
2
+ type Imports = {
3
+ [name: string]: string;
4
+ };
5
+ export type ImportMap = {
6
+ imports: Imports;
7
+ scopes?: {
8
+ [scope: string]: Imports;
9
+ };
10
+ };
11
+ export interface PatchworkVitePluginOptions {
12
+ importmap?: ImportMap;
13
+ }
14
+ export {};
@@ -0,0 +1,8 @@
1
+ // todo this is now not patchwork-specific, it's just a fun little importmap thing
2
+ // and a separate thing that returns the service worker which doesn't really
3
+ // need to be a plugin at all
4
+ import { importmap } from "./importmap-plugin.js";
5
+ import { serviceworker } from "./service-worker-plugin.js";
6
+ export default function patchwork(options) {
7
+ return [importmap(options), serviceworker()];
8
+ }
@@ -0,0 +1,2 @@
1
+ import { type Plugin } from "vite";
2
+ export declare function serviceworker(): Plugin;
@@ -0,0 +1,39 @@
1
+ import { transformWithEsbuild } from "vite";
2
+ export function serviceworker() {
3
+ const moduleId = "service-worker.js";
4
+ const path = `/${moduleId}`;
5
+ const ids = [moduleId, path];
6
+ const serviceWorkerExport = "@inkandswitch/patchwork-bootloader/service-worker";
7
+ async function transform(resolve, fs) {
8
+ const exportPath = await resolve(serviceWorkerExport);
9
+ const file = await fs.readFile(exportPath.id, {
10
+ encoding: "utf8",
11
+ });
12
+ const transformation = await transformWithEsbuild(file, serviceWorkerExport, { format: "iife" });
13
+ return transformation;
14
+ }
15
+ return {
16
+ name: "@patchwork/vite",
17
+ async buildStart() {
18
+ if (this.environment.mode == "build") {
19
+ const trans = await transform(this.resolve.bind(this), this.fs);
20
+ this.emitFile({
21
+ type: "prebuilt-chunk",
22
+ fileName: path.slice(1),
23
+ code: trans.code,
24
+ map: trans.map,
25
+ });
26
+ }
27
+ },
28
+ resolveId(id) {
29
+ if (ids.includes(id)) {
30
+ return moduleId;
31
+ }
32
+ },
33
+ async load(id) {
34
+ if (ids.includes(id)) {
35
+ return transform(this.resolve.bind(this), this.fs);
36
+ }
37
+ },
38
+ };
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkandswitch/patchwork-bootloader",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "author": "chee",
5
5
  "type": "module",
6
6
  "license": "MIT",