@inkandswitch/patchwork 0.0.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/build-head.js +10 -0
- package/dist/client.d.ts +38 -0
- package/dist/global.css +1 -0
- package/dist/head.d.ts +1 -0
- package/dist/head.js +52 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +294 -0
- package/dist/loading.d.ts +5 -0
- package/dist/loading.js +125 -0
- package/dist/repo.d.ts +25 -0
- package/dist/repo.js +108 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.js +132 -0
- package/dist/site-kit/html.d.ts +4 -0
- package/dist/site-kit/html.js +70 -0
- package/dist/site-kit/icons.d.ts +9 -0
- package/dist/site-kit/icons.js +36 -0
- package/dist/site-kit/index.d.ts +15 -0
- package/dist/site-kit/index.js +5 -0
- package/dist/site-kit/manifest.d.ts +3 -0
- package/dist/site-kit/manifest.js +29 -0
- package/dist/site-kit/netlify.d.ts +5 -0
- package/dist/site-kit/netlify.js +27 -0
- package/dist/site-kit/options.d.ts +67 -0
- package/dist/site-kit/options.js +9 -0
- package/dist/site-kit/sync-servers.d.ts +11 -0
- package/dist/site-kit/sync-servers.js +36 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.js +1 -0
- package/dist/vite/config-plugin.d.ts +12 -0
- package/dist/vite/config-plugin.js +70 -0
- package/dist/vite/html-plugin.d.ts +11 -0
- package/dist/vite/html-plugin.js +51 -0
- package/dist/vite/icons.d.ts +4 -0
- package/dist/vite/icons.js +42 -0
- package/dist/vite/importmap-plugin.d.ts +4 -0
- package/dist/vite/importmap-plugin.js +83 -0
- package/dist/vite/manifest-plugin.d.ts +4 -0
- package/dist/vite/manifest-plugin.js +34 -0
- package/dist/vite/netlify-plugin.d.ts +9 -0
- package/dist/vite/netlify-plugin.js +29 -0
- package/dist/vite/patchwork-plugin.d.ts +43 -0
- package/dist/vite/patchwork-plugin.js +40 -0
- package/dist/vite/service-worker-plugin.d.ts +2 -0
- package/dist/vite/service-worker-plugin.js +49 -0
- package/package.json +55 -0
- package/src/client.d.ts +38 -0
- package/src/global.css +1 -0
- package/src/head.ts +14 -0
- package/src/index.ts +469 -0
- package/src/loading.ts +137 -0
- package/src/repo.ts +146 -0
- package/src/router.ts +195 -0
- package/src/site-kit/html.ts +100 -0
- package/src/site-kit/icons.ts +49 -0
- package/src/site-kit/index.ts +22 -0
- package/src/site-kit/manifest.ts +39 -0
- package/src/site-kit/netlify.ts +35 -0
- package/src/site-kit/options.ts +73 -0
- package/src/site-kit/sync-servers.ts +39 -0
- package/src/types.ts +135 -0
- package/src/vite/config-plugin.ts +88 -0
- package/src/vite/html-plugin.ts +60 -0
- package/src/vite/icons.ts +47 -0
- package/src/vite/importmap-plugin.ts +112 -0
- package/src/vite/manifest-plugin.ts +39 -0
- package/src/vite/netlify-plugin.ts +34 -0
- package/src/vite/patchwork-plugin.ts +67 -0
- package/src/vite/service-worker-plugin.ts +54 -0
- package/tsconfig.json +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inkandswitch/patchwork",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"author": "Ink & Switch",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "One import for a Patchwork site: boot sequence, vite plugin, and ambient types.",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./vite": {
|
|
14
|
+
"import": "./dist/vite/patchwork-plugin.js",
|
|
15
|
+
"types": "./dist/vite/patchwork-plugin.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./head": {
|
|
18
|
+
"import": "./dist/head.js"
|
|
19
|
+
},
|
|
20
|
+
"./site-kit": {
|
|
21
|
+
"import": "./dist/site-kit/index.js",
|
|
22
|
+
"types": "./dist/site-kit/index.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./dist/client.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"./global.css": "./dist/global.css"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@automerge/automerge": "3.3.2",
|
|
31
|
+
"@automerge/automerge-repo": "2.6.0-subduction.46",
|
|
32
|
+
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.8b",
|
|
33
|
+
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.46",
|
|
34
|
+
"@automerge/automerge-subduction": "0.16.0",
|
|
35
|
+
"@automerge/vanillajs": "2.6.0-subduction.46",
|
|
36
|
+
"@types/debug": "^4.1.13",
|
|
37
|
+
"debug": "^4.4.3",
|
|
38
|
+
"sharp": "^0.35.3",
|
|
39
|
+
"vite-plugin-wasm": "^3.6.0",
|
|
40
|
+
"@inkandswitch/patchwork-bootloader": "^0.4.4",
|
|
41
|
+
"@inkandswitch/patchwork-filesystem": "^0.2.2",
|
|
42
|
+
"@inkandswitch/patchwork-providers": "^0.4.2",
|
|
43
|
+
"@inkandswitch/patchwork-elements": "^4.0.1",
|
|
44
|
+
"@inkandswitch/patchwork-plugins": "^1.0.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"esbuild": "^0.23.1",
|
|
48
|
+
"rollup": "^4.61.1",
|
|
49
|
+
"vite": "^7.3.5"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc && node build-head.js && cp src/global.css dist/global.css && cp src/client.d.ts dist/client.d.ts",
|
|
53
|
+
"dev": "tsc -w --preserveWatchOutput"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/client.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient types for a Patchwork site — reference from vite-env.d.ts:
|
|
3
|
+
*
|
|
4
|
+
* /// <reference types="@inkandswitch/patchwork/client" />
|
|
5
|
+
*
|
|
6
|
+
* Pulls in patchwork-elements'/-providers' HTMLElementTagNameMap and JSX
|
|
7
|
+
* augmentations, declares the vite `define`s every site gets from the
|
|
8
|
+
* `patchwork()` plugin, and extends ImportMetaEnv with the default-modules
|
|
9
|
+
* env var.
|
|
10
|
+
*/
|
|
11
|
+
import "@inkandswitch/patchwork-elements";
|
|
12
|
+
import "@inkandswitch/patchwork-providers";
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
const __SITE_NAME__: string;
|
|
16
|
+
const __KEYHIVE__: boolean;
|
|
17
|
+
const __KEYHIVE_SYNC_SERVER__: boolean;
|
|
18
|
+
|
|
19
|
+
interface ImportMetaEnv {
|
|
20
|
+
/**
|
|
21
|
+
* Comma-separated list of default tool-manifest sources the shell boots
|
|
22
|
+
* with. Each entry is an `automerge:` URL or a static `modules.json`
|
|
23
|
+
* URL. Overridable at runtime via `localStorage.systemPackageListURL`.
|
|
24
|
+
*/
|
|
25
|
+
readonly PATCHWORK_SYSTEM_PACKAGE_LIST_URL?: string;
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated Use {@link ImportMetaEnv.PATCHWORK_SYSTEM_PACKAGE_LIST_URL}.
|
|
28
|
+
* Retained for backwards compatibility with existing build configs.
|
|
29
|
+
*/
|
|
30
|
+
readonly VITE_DEFAULT_MODULES?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ImportMeta {
|
|
34
|
+
readonly env: ImportMetaEnv;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {};
|
package/src/global.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@inkandswitch/patchwork-bootloader/global.css";
|
package/src/head.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import externals from "@inkandswitch/patchwork-bootloader/externals-list";
|
|
2
|
+
|
|
3
|
+
const importmap: { imports: Record<string, string> } = { imports: {} };
|
|
4
|
+
|
|
5
|
+
for (const name of externals) {
|
|
6
|
+
importmap.imports[name] = `/packages/${name}.js`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const script = document.createElement("script");
|
|
10
|
+
script.type = "importmap";
|
|
11
|
+
script.textContent = JSON.stringify(importmap);
|
|
12
|
+
document.currentScript
|
|
13
|
+
? document.currentScript.after(script)
|
|
14
|
+
: document.head.appendChild(script);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One import for a Patchwork site.
|
|
3
|
+
*
|
|
4
|
+
* `setup(options)` constructs the Repo, wires up the automerge-worker port,
|
|
5
|
+
* loads plugins via the ModuleWatcher, resolves the user's account document,
|
|
6
|
+
* installs the router, and resolves with the site's runtime API — `repo`,
|
|
7
|
+
* `create`, `open`, `find`, `packages`, `plugins`, `sw` — which is what a
|
|
8
|
+
* site assigns to `window.patchwork`.
|
|
9
|
+
*
|
|
10
|
+
* Setup owns page-wide state — the `window.repo`/`window.Automerge`/
|
|
11
|
+
* `window.AutomergeRepo`/`window.hive` globals, custom-element registration,
|
|
12
|
+
* document listeners — so it may run only once per page; a second call
|
|
13
|
+
* throws.
|
|
14
|
+
*
|
|
15
|
+
* Pulls in DOM- and plugin-layer dependencies, so it is for a browser site's
|
|
16
|
+
* `main.ts` only. Non-UI consumers should import
|
|
17
|
+
* `@inkandswitch/patchwork-bootloader` directly, which does SW registration
|
|
18
|
+
* and the automerge-worker handoff and nothing else.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
type AutomergeUrl,
|
|
22
|
+
type DocHandle,
|
|
23
|
+
MessageChannelNetworkAdapter,
|
|
24
|
+
Repo,
|
|
25
|
+
isValidAutomergeUrl,
|
|
26
|
+
} from "@automerge/vanillajs/slim";
|
|
27
|
+
import * as Automerge from "@automerge/automerge/slim";
|
|
28
|
+
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
29
|
+
import type { AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
|
|
30
|
+
|
|
31
|
+
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
32
|
+
import { importAutomergePackageViaWorker } from "@inkandswitch/patchwork-bootloader/module-loader";
|
|
33
|
+
import { registerPatchworkViewElement } from "@inkandswitch/patchwork-elements";
|
|
34
|
+
import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
|
|
35
|
+
import {
|
|
36
|
+
type AccountDoc,
|
|
37
|
+
type DatatypeDescription,
|
|
38
|
+
type LoadedDatatype,
|
|
39
|
+
createDocOfDatatype2,
|
|
40
|
+
getRegistry,
|
|
41
|
+
registerPlugins,
|
|
42
|
+
resolveAccountHandle,
|
|
43
|
+
unregisterPlugins,
|
|
44
|
+
} from "@inkandswitch/patchwork-plugins";
|
|
45
|
+
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
46
|
+
import setupServiceWorker, {
|
|
47
|
+
lifecycleLog,
|
|
48
|
+
} from "@inkandswitch/patchwork-bootloader";
|
|
49
|
+
import debug from "debug";
|
|
50
|
+
|
|
51
|
+
import type {
|
|
52
|
+
OpenOptions,
|
|
53
|
+
Patchwork,
|
|
54
|
+
PatchworkOptions,
|
|
55
|
+
SignerIdentity,
|
|
56
|
+
} from "./types.js";
|
|
57
|
+
import {
|
|
58
|
+
createRepo,
|
|
59
|
+
firstRepoPort,
|
|
60
|
+
initWasm,
|
|
61
|
+
removeAdapterFor,
|
|
62
|
+
} from "./repo.js";
|
|
63
|
+
import { createRouter, type Router } from "./router.js";
|
|
64
|
+
|
|
65
|
+
const log = debug("patchwork:setup");
|
|
66
|
+
|
|
67
|
+
declare const __SITE_NAME__: string;
|
|
68
|
+
|
|
69
|
+
declare global {
|
|
70
|
+
interface Window {
|
|
71
|
+
patchwork: Patchwork;
|
|
72
|
+
repo: Repo;
|
|
73
|
+
Automerge: typeof import("@automerge/automerge");
|
|
74
|
+
AutomergeRepo: typeof import("@automerge/automerge-repo");
|
|
75
|
+
hive?: AutomergeRepoKeyhive;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Setup ────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
let setupCalled = false;
|
|
82
|
+
|
|
83
|
+
export function setup(options: PatchworkOptions = {}): Promise<Patchwork> {
|
|
84
|
+
if (setupCalled) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"patchwork.setup: already called — setup owns page-wide state and may run only once"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
setupCalled = true;
|
|
90
|
+
const done = doSetup(options);
|
|
91
|
+
void done.then(() => log("patchwork setup complete"));
|
|
92
|
+
|
|
93
|
+
const timeout = options.timeout ?? 30_000;
|
|
94
|
+
if (timeout === false) return done;
|
|
95
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
96
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
97
|
+
timer = setTimeout(
|
|
98
|
+
() =>
|
|
99
|
+
reject(
|
|
100
|
+
new Error(
|
|
101
|
+
`patchwork.setup: boot did not finish within ${timeout}ms`
|
|
102
|
+
)
|
|
103
|
+
),
|
|
104
|
+
timeout
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
done.then(
|
|
108
|
+
() => clearTimeout(timer),
|
|
109
|
+
() => clearTimeout(timer)
|
|
110
|
+
);
|
|
111
|
+
return Promise.race([done, deadline]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default setup;
|
|
115
|
+
|
|
116
|
+
async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
|
|
117
|
+
const siteName =
|
|
118
|
+
options.name ??
|
|
119
|
+
(typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "patchwork");
|
|
120
|
+
const moduleSources = resolveDefaultModules(options);
|
|
121
|
+
const routing = options.routing ?? "hash";
|
|
122
|
+
|
|
123
|
+
log("booting", options);
|
|
124
|
+
installLifecycleLogging();
|
|
125
|
+
|
|
126
|
+
if (!options.repo) await initWasm();
|
|
127
|
+
|
|
128
|
+
const sw = await setupServiceWorker();
|
|
129
|
+
if (!sw) throw new Error("Failed to set up service worker");
|
|
130
|
+
log("workers ready");
|
|
131
|
+
|
|
132
|
+
let hive: AutomergeRepoKeyhive | undefined;
|
|
133
|
+
let repo: Repo;
|
|
134
|
+
let signerIdentity: SignerIdentity | undefined;
|
|
135
|
+
// Called with a fresh port when the automerge worker dies and is recreated.
|
|
136
|
+
// Assigned once the repo exists.
|
|
137
|
+
let onWorkerPortRenewed: ((port: MessagePort) => void) | undefined;
|
|
138
|
+
|
|
139
|
+
if (options.repo) {
|
|
140
|
+
log("using provided Repo");
|
|
141
|
+
repo = options.repo;
|
|
142
|
+
hive = options.hive;
|
|
143
|
+
} else {
|
|
144
|
+
const workerPort = await firstRepoPort(sw, (port) => {
|
|
145
|
+
if (onWorkerPortRenewed) onWorkerPortRenewed(port);
|
|
146
|
+
else {
|
|
147
|
+
console.warn(
|
|
148
|
+
"automerge worker port renewed before the repo existed; dropping it"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
|
|
154
|
+
({ repo, hive, signerIdentity } = await createRepo(
|
|
155
|
+
options,
|
|
156
|
+
siteName,
|
|
157
|
+
workerAdapter
|
|
158
|
+
));
|
|
159
|
+
|
|
160
|
+
// The worker was recreated with cold state: wire the repo onto the fresh
|
|
161
|
+
// port and drop the adapter stranded on the dead one.
|
|
162
|
+
const bootHive = hive;
|
|
163
|
+
onWorkerPortRenewed = (port) => {
|
|
164
|
+
const fresh = new MessageChannelNetworkAdapter(port);
|
|
165
|
+
// Mirror the boot wiring: a keyhive repo talks to the worker through a
|
|
166
|
+
// keyhive adapter wrapped around the message channel.
|
|
167
|
+
const registered = bootHive
|
|
168
|
+
? bootHive.createKeyhiveNetworkAdapter(fresh, false, false, 2000)
|
|
169
|
+
: fresh;
|
|
170
|
+
repo.networkSubsystem.addNetworkAdapter(registered as any);
|
|
171
|
+
removeAdapterFor(repo, workerAdapter, registered);
|
|
172
|
+
workerAdapter = fresh;
|
|
173
|
+
lifecycleLog("repo re-wired to the recreated automerge worker");
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Dev-console / tool-runtime globals (e2e and loaded tools read these). The
|
|
178
|
+
// `window.patchwork` handle is deliberately not set here — the caller does
|
|
179
|
+
// `window.patchwork = await setup(...)`.
|
|
180
|
+
window.repo = repo;
|
|
181
|
+
window.Automerge = Automerge as typeof import("@automerge/automerge");
|
|
182
|
+
window.AutomergeRepo =
|
|
183
|
+
AutomergeRepo as typeof import("@automerge/automerge-repo");
|
|
184
|
+
if (hive) window.hive = hive;
|
|
185
|
+
|
|
186
|
+
await repo.networkSubsystem.whenReady();
|
|
187
|
+
log("networkSubsystem ready");
|
|
188
|
+
(hive?.networkAdapter as any)?.syncKeyhive?.();
|
|
189
|
+
|
|
190
|
+
registerRepoProviderElement(repo as any);
|
|
191
|
+
|
|
192
|
+
const rootElementId = options.rootElementId ?? "root";
|
|
193
|
+
const rootElement = document.getElementById(rootElementId);
|
|
194
|
+
if (!rootElement) {
|
|
195
|
+
throw new Error(`patchwork.setup: no element with id="${rootElementId}"`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// `<repo-provider>` sits above the root and answers `repo:handle-descriptor`
|
|
199
|
+
// for any view outside a remapper, resolving to the requested url unchanged.
|
|
200
|
+
// Generated site html already declares it around the root; wrap only
|
|
201
|
+
// hand-written markup that lacks one.
|
|
202
|
+
if (!rootElement.closest("repo-provider")) {
|
|
203
|
+
const repoProvider = document.createElement("repo-provider");
|
|
204
|
+
rootElement.parentElement!.insertBefore(repoProvider, rootElement);
|
|
205
|
+
repoProvider.appendChild(rootElement);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
registerPatchworkViewElement({ hive, repo });
|
|
209
|
+
|
|
210
|
+
// Started with the site bundle alone so resolveAccountHandle has something to
|
|
211
|
+
// await on — the `account` datatype lives there. The user's own
|
|
212
|
+
// module-settings URL is added lazily once it appears on the account doc.
|
|
213
|
+
const moduleWatcher = new ModuleWatcher(
|
|
214
|
+
repo,
|
|
215
|
+
nameSources(moduleSources),
|
|
216
|
+
onModuleLoaded,
|
|
217
|
+
unregisterPlugins,
|
|
218
|
+
// Discover an Automerge package's plugin descriptors off the main thread;
|
|
219
|
+
// each plugin's load() re-imports the package (at heads) on this thread.
|
|
220
|
+
importAutomergePackageViaWorker
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const accountDocHandle = (await resolveAccountHandle(repo, {
|
|
224
|
+
storageKey: options.accountKey ?? "patchworkAccountURL",
|
|
225
|
+
hive,
|
|
226
|
+
})) as DocHandle<AccountDoc>;
|
|
227
|
+
|
|
228
|
+
wireModuleSettings(accountDocHandle, moduleWatcher);
|
|
229
|
+
|
|
230
|
+
let router: Router | undefined;
|
|
231
|
+
if (routing !== false) {
|
|
232
|
+
rootElement.style.visibility = "hidden";
|
|
233
|
+
router = createRouter({
|
|
234
|
+
rootElement,
|
|
235
|
+
repo,
|
|
236
|
+
accountDocHandle,
|
|
237
|
+
siteName,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const toolsLoaded = moduleWatcher.doneLoading.then(
|
|
242
|
+
() =>
|
|
243
|
+
log(
|
|
244
|
+
"doneLoading, tools registered:",
|
|
245
|
+
getRegistry("patchwork:tool")
|
|
246
|
+
.all()
|
|
247
|
+
.map((t: any) => t.id)
|
|
248
|
+
),
|
|
249
|
+
(err: unknown) => console.error("doneLoading rejected:", err)
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
installReveal(rootElement, router, toolsLoaded);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
repo,
|
|
256
|
+
hive,
|
|
257
|
+
account: accountDocHandle,
|
|
258
|
+
signer: signerIdentity,
|
|
259
|
+
packages: moduleWatcher,
|
|
260
|
+
plugins,
|
|
261
|
+
sw: {
|
|
262
|
+
connectClassicSync: sw.connectClassicSync,
|
|
263
|
+
subscribeToRepoChannel: sw.subscribeToRepoChannel,
|
|
264
|
+
subscribeSyncState: sw.subscribeSyncState,
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
async create<D>(type: string, init?: (doc: D) => void) {
|
|
268
|
+
const datatype = await getRegistry<DatatypeDescription>(
|
|
269
|
+
"patchwork:datatype"
|
|
270
|
+
).load(type);
|
|
271
|
+
if (!datatype) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`patchwork.create: no datatype registered for "${type}"`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return createDocOfDatatype2(
|
|
277
|
+
datatype as unknown as LoadedDatatype<D>,
|
|
278
|
+
repo,
|
|
279
|
+
init,
|
|
280
|
+
hive
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
open(url: AutomergeUrl, openOptions: OpenOptions = {}) {
|
|
284
|
+
rootElement.dispatchEvent(
|
|
285
|
+
new CustomEvent("patchwork:open-document", {
|
|
286
|
+
detail: {
|
|
287
|
+
url,
|
|
288
|
+
toolId: openOptions.tool,
|
|
289
|
+
type: openOptions.type,
|
|
290
|
+
title: openOptions.title,
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
find<D>(url: AutomergeUrl) {
|
|
296
|
+
return repo.find<D>(url);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Reveal / mount ───────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
function installReveal(
|
|
304
|
+
rootElement: HTMLElement,
|
|
305
|
+
router: Router | undefined,
|
|
306
|
+
toolsLoaded: Promise<unknown>
|
|
307
|
+
): void {
|
|
308
|
+
let revealed = false;
|
|
309
|
+
const reveal = () => {
|
|
310
|
+
if (revealed) return;
|
|
311
|
+
revealed = true;
|
|
312
|
+
rootElement.style.visibility = "visible";
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
rootElement.addEventListener("patchwork:mounted", (event) => {
|
|
316
|
+
if (event.target !== rootElement) return;
|
|
317
|
+
log("root element mounted");
|
|
318
|
+
if (router) {
|
|
319
|
+
void router.route();
|
|
320
|
+
// Deep-links into tools that were still loading get routed again once
|
|
321
|
+
// every module has registered.
|
|
322
|
+
void toolsLoaded.then(() => router.route());
|
|
323
|
+
}
|
|
324
|
+
reveal();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// If nothing ever mounts, reveal anyway so the user sees something rather
|
|
328
|
+
// than a blank page.
|
|
329
|
+
setTimeout(reveal, 12_000);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Module sources ───────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
function isValidModuleSource(source: string): boolean {
|
|
335
|
+
return isValidAutomergeUrl(source) || /^(https?:\/\/|\.?\/)/.test(source);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The site's default module-list sources, honouring the
|
|
340
|
+
* `localStorage.systemPackageListURL` dev override, which replaces the entire
|
|
341
|
+
* built-in bundle.
|
|
342
|
+
*/
|
|
343
|
+
function resolveDefaultModules(options: PatchworkOptions): string[] {
|
|
344
|
+
const configured = options.packageListURL ?? [];
|
|
345
|
+
const builtin = (
|
|
346
|
+
Array.isArray(configured) ? configured : [configured]
|
|
347
|
+
).filter(Boolean);
|
|
348
|
+
|
|
349
|
+
const override = globalThis.localStorage?.getItem("systemPackageListURL");
|
|
350
|
+
|
|
351
|
+
if (override && isValidModuleSource(override)) {
|
|
352
|
+
console.info(`using systemPackageListURL from localStorage: ${override}`);
|
|
353
|
+
return [override];
|
|
354
|
+
}
|
|
355
|
+
if (override) {
|
|
356
|
+
console.warn(
|
|
357
|
+
`ignoring invalid systemPackageListURL in localStorage: ${override}`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (builtin.length === 0) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"patchwork.setup: no default module sources configured (set `packageListURL`)"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return builtin;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Name the sources for the ModuleWatcher. The first keeps the canonical
|
|
371
|
+
* `system` name; the rest get suffixed. None may be `user`, which is reserved
|
|
372
|
+
* for the per-account settings doc and has branch-override precedence.
|
|
373
|
+
*/
|
|
374
|
+
function nameSources(sources: string[]): Record<string, string> {
|
|
375
|
+
return Object.fromEntries(
|
|
376
|
+
sources.map((source, i) => [i === 0 ? "system" : `system-${i}`, source])
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function onModuleLoaded(name: string, mod: any): void {
|
|
381
|
+
if (!Array.isArray(mod.plugins)) {
|
|
382
|
+
console.warn(`module ${name} has no plugins array`, Object.keys(mod));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
log(
|
|
386
|
+
`registering ${mod.plugins.length} plugin(s) from ${name}`,
|
|
387
|
+
mod.plugins.map((p: any) => `${p.type}:${p.id}`)
|
|
388
|
+
);
|
|
389
|
+
registerPlugins(mod.plugins, name);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The frame lazy-creates `moduleSettingsUrl` on first mount, so watch for it to
|
|
394
|
+
* appear and feed it to the ModuleWatcher.
|
|
395
|
+
*/
|
|
396
|
+
function wireModuleSettings(
|
|
397
|
+
accountDocHandle: DocHandle<AccountDoc>,
|
|
398
|
+
moduleWatcher: ModuleWatcher
|
|
399
|
+
): void {
|
|
400
|
+
const wire = () => {
|
|
401
|
+
const url = accountDocHandle.doc()?.moduleSettingsUrl;
|
|
402
|
+
if (!url) return;
|
|
403
|
+
void moduleWatcher.addUrl("user", url);
|
|
404
|
+
accountDocHandle.off("change", wire);
|
|
405
|
+
};
|
|
406
|
+
wire();
|
|
407
|
+
if (!accountDocHandle.doc()?.moduleSettingsUrl) {
|
|
408
|
+
accountDocHandle.on("change", wire);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Page Lifecycle and connectivity transitions, to line up against the
|
|
413
|
+
* SharedWorker's sync-socket reaps. */
|
|
414
|
+
function installLifecycleLogging(): void {
|
|
415
|
+
if (typeof document === "undefined") return;
|
|
416
|
+
const opts = { capture: true } as const;
|
|
417
|
+
const persisted = (e: Event) => (e as PageTransitionEvent).persisted;
|
|
418
|
+
|
|
419
|
+
document.addEventListener(
|
|
420
|
+
"visibilitychange",
|
|
421
|
+
() => lifecycleLog("visibilitychange → %s", document.visibilityState),
|
|
422
|
+
opts
|
|
423
|
+
);
|
|
424
|
+
document.addEventListener(
|
|
425
|
+
"freeze",
|
|
426
|
+
() => lifecycleLog("freeze (tab suspended)"),
|
|
427
|
+
opts
|
|
428
|
+
);
|
|
429
|
+
document.addEventListener(
|
|
430
|
+
"resume",
|
|
431
|
+
() => lifecycleLog("resume (tab unsuspended)"),
|
|
432
|
+
opts
|
|
433
|
+
);
|
|
434
|
+
window.addEventListener(
|
|
435
|
+
"pageshow",
|
|
436
|
+
(e) => lifecycleLog("pageshow persisted=%s", persisted(e)),
|
|
437
|
+
opts
|
|
438
|
+
);
|
|
439
|
+
window.addEventListener(
|
|
440
|
+
"pagehide",
|
|
441
|
+
(e) => lifecycleLog("pagehide persisted=%s", persisted(e)),
|
|
442
|
+
opts
|
|
443
|
+
);
|
|
444
|
+
window.addEventListener("online", () => lifecycleLog("online"), opts);
|
|
445
|
+
window.addEventListener("offline", () => lifecycleLog("offline"), opts);
|
|
446
|
+
|
|
447
|
+
lifecycleLog(
|
|
448
|
+
"logging installed (visibilityState=%s, hasFocus=%s)",
|
|
449
|
+
document.visibilityState,
|
|
450
|
+
document.hasFocus()
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Named exports ────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
export { createRepo, initWasm } from "./repo.js";
|
|
457
|
+
export { createRouter } from "./router.js";
|
|
458
|
+
export {
|
|
459
|
+
showLoadingAnimation,
|
|
460
|
+
hideLoadingAnimation,
|
|
461
|
+
showErrorScreen,
|
|
462
|
+
} from "./loading.js";
|
|
463
|
+
export type {
|
|
464
|
+
OpenOptions,
|
|
465
|
+
Patchwork,
|
|
466
|
+
PatchworkOptions,
|
|
467
|
+
ServiceWorkerApi,
|
|
468
|
+
SignerIdentity,
|
|
469
|
+
} from "./types.js";
|