@inkandswitch/patchwork 0.7.4 → 0.8.1
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.
- package/CHANGELOG.md +49 -0
- package/dist/head.js +1 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +37 -56
- package/dist/repo.d.ts +9 -17
- package/dist/repo.js +27 -56
- package/dist/site-kit/index.d.ts +1 -1
- package/dist/site-kit/options.d.ts +24 -15
- package/dist/site-kit/sync-servers.d.ts +1 -0
- package/dist/site-kit/sync-servers.js +19 -17
- package/dist/types.d.ts +14 -3
- package/dist/vite/config-plugin.js +5 -5
- package/dist/vite/patches-plugin.d.ts +2 -0
- package/dist/vite/patches-plugin.js +149 -0
- package/dist/vite/patchwork-plugin.d.ts +2 -1
- package/dist/vite/patchwork-plugin.js +3 -0
- package/dist/vite/service-worker-plugin.js +2 -2
- package/package.json +5 -5
- package/src/index.ts +56 -48
- package/src/repo.ts +36 -72
- package/src/site-kit/index.ts +1 -0
- package/src/site-kit/options.ts +26 -11
- package/src/site-kit/sync-servers.ts +20 -17
- package/src/types.ts +15 -8
- package/src/vite/config-plugin.ts +7 -12
- package/src/vite/patches-plugin.ts +184 -0
- package/src/vite/patchwork-plugin.ts +4 -0
- package/src/vite/service-worker-plugin.ts +3 -2
|
@@ -2,9 +2,10 @@ import type { PatchworkSiteOptions } from "./options.js";
|
|
|
2
2
|
import type { SyncServerSelection } from "@automerge/automerge-repo-keyhive";
|
|
3
3
|
|
|
4
4
|
// Mirrors core/bootloader/src/sync-config.ts's DEFAULT_CLASSIC_SYNC_SERVER
|
|
5
|
-
// and automerge-worker.ts's SUBDUCTION_SYNC_URL selection —
|
|
6
|
-
// plain constants (rather than importing those runtime modules)
|
|
7
|
-
// only needs the hostnames, not the browser-only logic that reads
|
|
5
|
+
// and automerge-protocol-handler-worker.ts's SUBDUCTION_SYNC_URL selection —
|
|
6
|
+
// kept here as plain constants (rather than importing those runtime modules)
|
|
7
|
+
// since this only needs the hostnames, not the browser-only logic that reads
|
|
8
|
+
// them.
|
|
8
9
|
export const DEFAULT_SYNC_SERVERS = {
|
|
9
10
|
classic: "wss://sync3.automerge.org",
|
|
10
11
|
subduction: "wss://subduction.sync.inkandswitch.com",
|
|
@@ -14,24 +15,26 @@ export const DEFAULT_SYNC_SERVERS = {
|
|
|
14
15
|
export function resolvePrimarySyncServer(options: PatchworkSiteOptions): {
|
|
15
16
|
url: string;
|
|
16
17
|
keyhive?: SyncServerSelection;
|
|
18
|
+
useIdFactory?: boolean;
|
|
17
19
|
} {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const { url, ...identity } = servers.keyhive;
|
|
20
|
+
const subduction = (options.syncServers || undefined)?.subduction;
|
|
21
|
+
if (!options.keyhive) {
|
|
22
|
+
return { url: subduction ?? DEFAULT_SYNC_SERVERS.subduction };
|
|
23
|
+
}
|
|
24
|
+
const keyhive = options.keyhive === true ? {} : options.keyhive;
|
|
25
|
+
const useIdFactory = keyhive.useIdFactory ?? true;
|
|
26
|
+
const syncServer = keyhive.syncServer ?? "subduction";
|
|
27
|
+
if (typeof syncServer === "string") {
|
|
27
28
|
return {
|
|
28
|
-
keyhive:
|
|
29
|
-
url,
|
|
29
|
+
keyhive: syncServer,
|
|
30
|
+
url: subduction ?? DEFAULT_SYNC_SERVERS[syncServer],
|
|
31
|
+
useIdFactory,
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
34
|
+
// A custom relay carries the only URL it can be reached on, so it wins over
|
|
35
|
+
// `syncServers.subduction`.
|
|
36
|
+
const { url, ...identity } = syncServer;
|
|
37
|
+
return { keyhive: identity, url, useIdFactory };
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
function wsToHttpOrigin(wsUrl: string): string {
|
package/src/types.ts
CHANGED
|
@@ -8,21 +8,27 @@ import type {
|
|
|
8
8
|
AccountCreator,
|
|
9
9
|
AccountDoc,
|
|
10
10
|
} from "@inkandswitch/patchwork-plugins";
|
|
11
|
-
import type {
|
|
12
|
-
ServiceWorkerRepoChannelListener,
|
|
13
|
-
SyncStateDocMessage,
|
|
14
|
-
} from "@inkandswitch/patchwork-bootloader/types";
|
|
15
11
|
import type * as pluginsNS from "@inkandswitch/patchwork-plugins";
|
|
16
12
|
|
|
17
13
|
export type PluginsApi = typeof pluginsNS;
|
|
18
14
|
|
|
19
15
|
export type SignerIdentity = { peerId: string; verifyingKey: string };
|
|
20
16
|
|
|
17
|
+
/** The sync server's last-known heads for one document, as it reports them. */
|
|
18
|
+
export interface SyncStateDocMessage {
|
|
19
|
+
type: "sync-state";
|
|
20
|
+
documentId: string;
|
|
21
|
+
storageId: string;
|
|
22
|
+
heads: string[];
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export interface ServiceWorkerApi {
|
|
22
27
|
connectClassicSync: (server?: string) => Promise<void>;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Watch one document's heads at the sync server. Calls `listener` with
|
|
30
|
+
* what the server holds now, then on every update. Returns unsubscribe.
|
|
31
|
+
*/
|
|
26
32
|
subscribeSyncState: (
|
|
27
33
|
documentId: string,
|
|
28
34
|
listener: (update: SyncStateDocMessage) => void
|
|
@@ -91,7 +97,8 @@ export interface PatchworkOptions {
|
|
|
91
97
|
/**
|
|
92
98
|
* Bring your own Repo. When provided, setup skips wasm initialization and
|
|
93
99
|
* repo creation entirely — you are responsible for having initialized
|
|
94
|
-
* automerge/subduction and wired the automerge-worker
|
|
100
|
+
* automerge/subduction and wired the automerge-protocol-handler-worker
|
|
101
|
+
* port yourself.
|
|
95
102
|
*/
|
|
96
103
|
repo?: Repo;
|
|
97
104
|
|
|
@@ -40,9 +40,7 @@ export function buildDefines(
|
|
|
40
40
|
* used to hand-write in its own vite.config.ts. Each is switched off
|
|
41
41
|
* individually via the matching `false` option.
|
|
42
42
|
*/
|
|
43
|
-
export function config(
|
|
44
|
-
options: PatchworkVitePluginOptions = {}
|
|
45
|
-
): Plugin {
|
|
43
|
+
export function config(options: PatchworkVitePluginOptions = {}): Plugin {
|
|
46
44
|
return {
|
|
47
45
|
name: "@patchwork/config",
|
|
48
46
|
config() {
|
|
@@ -87,11 +85,11 @@ export function config(
|
|
|
87
85
|
},
|
|
88
86
|
};
|
|
89
87
|
},
|
|
90
|
-
// The shared automerge-worker's chunk imports bypass the
|
|
91
|
-
// worker, so offline boot needs the browser's HTTP cache to
|
|
92
|
-
// without revalidating. Content hashes make that safe; a new
|
|
93
|
-
// new URLs. Production gets this from the generated _headers
|
|
94
|
-
// preview doesn't read that, so mirror it here.
|
|
88
|
+
// The shared automerge-protocol-handler-worker's chunk imports bypass the
|
|
89
|
+
// page's service worker, so offline boot needs the browser's HTTP cache to
|
|
90
|
+
// serve them without revalidating. Content hashes make that safe; a new
|
|
91
|
+
// build gets new URLs. Production gets this from the generated _headers
|
|
92
|
+
// file — vite preview doesn't read that, so mirror it here.
|
|
95
93
|
configurePreviewServer(server) {
|
|
96
94
|
if (
|
|
97
95
|
options.netlify === false ||
|
|
@@ -101,10 +99,7 @@ export function config(
|
|
|
101
99
|
}
|
|
102
100
|
server.middlewares.use((req, res, next) => {
|
|
103
101
|
if (req.url?.startsWith("/assets/")) {
|
|
104
|
-
res.setHeader(
|
|
105
|
-
"Cache-Control",
|
|
106
|
-
"public, max-age=31536000, immutable"
|
|
107
|
-
);
|
|
102
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
108
103
|
}
|
|
109
104
|
next();
|
|
110
105
|
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Plugin } from "vite";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Source patches to @automerge/automerge-repo, applied as it passes through
|
|
7
|
+
* the bundler.
|
|
8
|
+
*
|
|
9
|
+
* The same edits live in this repo's `patches/` as a pnpm patch, which is what
|
|
10
|
+
* makes our own typecheck see the widened `role` type. A pnpm patch only
|
|
11
|
+
* exists in this repo's node_modules, though: a site that installs
|
|
12
|
+
* @inkandswitch/patchwork resolves automerge-repo out of its own tree and
|
|
13
|
+
* would bundle it unpatched. These run there too.
|
|
14
|
+
*
|
|
15
|
+
* Both edits are upstream-shaped and meant to be deleted once subduction takes
|
|
16
|
+
* them. Until then they are pinned to one automerge-repo version and every
|
|
17
|
+
* anchor has to match, so a dependency bump fails the build instead of quietly
|
|
18
|
+
* un-patching it.
|
|
19
|
+
*/
|
|
20
|
+
const AUTOMERGE_REPO = "@automerge/automerge-repo";
|
|
21
|
+
const VERSION = "2.6.0-subduction.48";
|
|
22
|
+
|
|
23
|
+
type Edit = { find: string; replace: string };
|
|
24
|
+
|
|
25
|
+
const PATCHES: Record<string, Edit[]> = {
|
|
26
|
+
"dist/subduction/AdapterConnections.js": [
|
|
27
|
+
{
|
|
28
|
+
find: ` if (role === "accept") {
|
|
29
|
+
await subduction.acceptTransport(transport, serviceName);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
await subduction.connectTransport(transport, serviceName);
|
|
33
|
+
}`,
|
|
34
|
+
replace: ` const initiate = role === "mesh" ? this.#localPeerId < peerId : role !== "accept";
|
|
35
|
+
if (initiate) {
|
|
36
|
+
await subduction.connectTransport(transport, serviceName);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
await subduction.acceptTransport(transport, serviceName);
|
|
40
|
+
}`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
"dist/subduction/SubductionConnections.js": [
|
|
44
|
+
{
|
|
45
|
+
find: ` if (state === "connecting")
|
|
46
|
+
return true;`,
|
|
47
|
+
replace: ` // "awaiting-reconnect" counts: the loop is between attempts, not
|
|
48
|
+
// given up, so a query should wait rather than report unavailable.
|
|
49
|
+
if (state === "connecting" || state === "awaiting-reconnect")
|
|
50
|
+
return true;`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function match(id: string): { file: string; root: string } | undefined {
|
|
56
|
+
const path = id.replace(/\\/g, "/").split("?")[0];
|
|
57
|
+
for (const file of Object.keys(PATCHES)) {
|
|
58
|
+
const suffix = `/${AUTOMERGE_REPO}/${file}`;
|
|
59
|
+
if (path.endsWith(suffix)) {
|
|
60
|
+
return {
|
|
61
|
+
file,
|
|
62
|
+
root: path.slice(0, -suffix.length) + `/${AUTOMERGE_REPO}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const versions = new Map<string, Promise<string>>();
|
|
69
|
+
|
|
70
|
+
async function assertVersion(root: string, file: string): Promise<void> {
|
|
71
|
+
let version = versions.get(root);
|
|
72
|
+
if (!version) {
|
|
73
|
+
version = readFile(join(root, "package.json"), "utf8").then(
|
|
74
|
+
(json) => JSON.parse(json).version,
|
|
75
|
+
(error) => {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`@inkandswitch/patchwork: couldn't read ${root}/package.json to ` +
|
|
78
|
+
`check the version the source patches are written against. ` +
|
|
79
|
+
`Has the package's layout changed? (${error})`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
versions.set(root, version);
|
|
84
|
+
}
|
|
85
|
+
if ((await version) !== VERSION) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`@inkandswitch/patchwork: ${AUTOMERGE_REPO} is ${await version}, and the ` +
|
|
88
|
+
`source patches in patches-plugin.ts are written against ${VERSION}. ` +
|
|
89
|
+
`Re-check them against the new version (${file} is one of the files ` +
|
|
90
|
+
`they edit), then bump VERSION.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function apply(code: string, file: string): string {
|
|
96
|
+
return PATCHES[file].reduce((code, { find, replace }) => {
|
|
97
|
+
if (code.includes(replace)) return code;
|
|
98
|
+
const matches = code.split(find).length - 1;
|
|
99
|
+
if (matches !== 1) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`@inkandswitch/patchwork: the source patch for ${AUTOMERGE_REPO}'s ` +
|
|
102
|
+
`${file} matched ${matches} times, expected 1. The file is the ` +
|
|
103
|
+
`version it says it is, so the patch needs rewriting against it.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return code.replace(find, replace);
|
|
107
|
+
}, code);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function patchFile(id: string): Promise<string | undefined> {
|
|
111
|
+
const found = match(id);
|
|
112
|
+
if (!found) return;
|
|
113
|
+
await assertVersion(found.root, found.file);
|
|
114
|
+
return apply(await readFile(id, "utf8"), found.file);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function patches(): Plugin {
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
let serve = false;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
name: "@patchwork/patches",
|
|
123
|
+
enforce: "pre",
|
|
124
|
+
|
|
125
|
+
// Dep pre-bundling runs esbuild directly, outside the plugin pipeline that
|
|
126
|
+
// applies `transform`. Without this the dev server would serve an
|
|
127
|
+
// unpatched automerge-repo while the build patched it.
|
|
128
|
+
config() {
|
|
129
|
+
return {
|
|
130
|
+
optimizeDeps: {
|
|
131
|
+
esbuildOptions: {
|
|
132
|
+
plugins: [
|
|
133
|
+
{
|
|
134
|
+
name: "patchwork-automerge-repo-patches",
|
|
135
|
+
setup(build: {
|
|
136
|
+
onLoad(
|
|
137
|
+
options: { filter: RegExp },
|
|
138
|
+
callback: (args: {
|
|
139
|
+
path: string;
|
|
140
|
+
}) => Promise<
|
|
141
|
+
{ contents: string; loader: "js" } | undefined
|
|
142
|
+
>
|
|
143
|
+
): void;
|
|
144
|
+
}) {
|
|
145
|
+
build.onLoad(
|
|
146
|
+
{ filter: /automerge-repo[\\/]dist[\\/]subduction[\\/]/ },
|
|
147
|
+
async ({ path }) => {
|
|
148
|
+
const contents = await patchFile(path);
|
|
149
|
+
return contents ? { contents, loader: "js" } : undefined;
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
configResolved(config) {
|
|
161
|
+
serve = config.command === "serve";
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async transform(code, id) {
|
|
165
|
+
const found = match(id);
|
|
166
|
+
if (!found) return;
|
|
167
|
+
await assertVersion(found.root, found.file);
|
|
168
|
+
seen.add(found.file);
|
|
169
|
+
return apply(code, found.file);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
buildEnd() {
|
|
173
|
+
if (serve) return;
|
|
174
|
+
const missing = Object.keys(PATCHES).filter((file) => !seen.has(file));
|
|
175
|
+
if (missing.length) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`@inkandswitch/patchwork: ${AUTOMERGE_REPO}'s ${missing.join(", ")} ` +
|
|
178
|
+
`never reached the bundler, so the source patches for them didn't ` +
|
|
179
|
+
`apply. Has the package's layout changed?`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Plugin, ServerOptions, PreviewOptions, BuildOptions } from "vite";
|
|
2
2
|
|
|
3
3
|
import { importmap } from "./importmap-plugin.js";
|
|
4
|
+
import { patches } from "./patches-plugin.js";
|
|
4
5
|
import { serviceworker } from "./service-worker-plugin.js";
|
|
5
6
|
import { config, wasm } from "./config-plugin.js";
|
|
6
7
|
import { dev } from "./dev-plugin.js";
|
|
@@ -42,6 +43,7 @@ export default function patchwork(options?: PatchworkVitePluginOptions) {
|
|
|
42
43
|
manifest(options),
|
|
43
44
|
netlify(options),
|
|
44
45
|
importmap(options),
|
|
46
|
+
patches(),
|
|
45
47
|
serviceworker(),
|
|
46
48
|
dev(options),
|
|
47
49
|
statics(options),
|
|
@@ -50,6 +52,7 @@ export default function patchwork(options?: PatchworkVitePluginOptions) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export { importmap, builtins, devDependencyId } from "./importmap-plugin.js";
|
|
55
|
+
export { patches } from "./patches-plugin.js";
|
|
53
56
|
export { serviceworker, workers } from "./service-worker-plugin.js";
|
|
54
57
|
export { config, buildDefines, wasm } from "./config-plugin.js";
|
|
55
58
|
export { dev } from "./dev-plugin.js";
|
|
@@ -74,6 +77,7 @@ export type {
|
|
|
74
77
|
PatchworkHtmlOptions,
|
|
75
78
|
PatchworkNetlifyOptions,
|
|
76
79
|
PatchworkKeyhiveSyncServer,
|
|
80
|
+
PatchworkKeyhiveOptions,
|
|
77
81
|
PatchworkSyncServersOptions,
|
|
78
82
|
} from "../site-kit/options.js";
|
|
79
83
|
|
|
@@ -19,8 +19,9 @@ export const workers = [
|
|
|
19
19
|
fileName: "service-worker.js",
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
specifier:
|
|
23
|
-
|
|
22
|
+
specifier:
|
|
23
|
+
"@inkandswitch/patchwork-bootloader/automerge-protocol-handler-worker",
|
|
24
|
+
fileName: "automerge-protocol-handler-worker.js",
|
|
24
25
|
},
|
|
25
26
|
{
|
|
26
27
|
specifier: "@inkandswitch/patchwork-bootloader/module-loader-worker",
|