@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/build-head.js
ADDED
package/dist/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/dist/global.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@inkandswitch/patchwork-bootloader/global.css";
|
package/dist/head.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/head.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// ../bootloader/dist/externals-list.js
|
|
4
|
+
var externals = [
|
|
5
|
+
"@automerge/automerge",
|
|
6
|
+
"@automerge/automerge/slim",
|
|
7
|
+
"@automerge/automerge-repo",
|
|
8
|
+
"@automerge/automerge-repo/slim",
|
|
9
|
+
// Port-donation plumbing for WorkerWebSocketEndpoint: tabs spawn the shared
|
|
10
|
+
// proxy entry and donate its port to the automerge worker (Chrome can't
|
|
11
|
+
// spawn workers from inside a SharedWorker). See setup.ts/automerge-worker.ts.
|
|
12
|
+
"@automerge/automerge-repo/worker-port",
|
|
13
|
+
"@automerge/automerge-repo/subduction-websocket-worker-shared",
|
|
14
|
+
"@automerge/automerge-repo-network-messagechannel",
|
|
15
|
+
"@automerge/automerge-repo-network-websocket",
|
|
16
|
+
"@automerge/automerge-repo-storage-indexeddb",
|
|
17
|
+
"@automerge/automerge-repo-keyhive",
|
|
18
|
+
"@automerge/automerge-subduction",
|
|
19
|
+
"@automerge/automerge-subduction/slim",
|
|
20
|
+
"@keyhive/keyhive",
|
|
21
|
+
"@keyhive/keyhive/slim",
|
|
22
|
+
"@inkandswitch/patchwork-bootloader",
|
|
23
|
+
"@inkandswitch/patchwork-elements",
|
|
24
|
+
"@inkandswitch/patchwork-filesystem",
|
|
25
|
+
"@inkandswitch/patchwork-plugins",
|
|
26
|
+
"@inkandswitch/patchwork-providers",
|
|
27
|
+
"@inkandswitch/patchwork",
|
|
28
|
+
// sad
|
|
29
|
+
"@codemirror/state",
|
|
30
|
+
"@codemirror/view",
|
|
31
|
+
"@codemirror/language",
|
|
32
|
+
"@codemirror/commands",
|
|
33
|
+
// rip
|
|
34
|
+
"solid-js",
|
|
35
|
+
"solid-js/html",
|
|
36
|
+
"solid-js/web",
|
|
37
|
+
"solid-js/h",
|
|
38
|
+
"solid-js/store",
|
|
39
|
+
"solid-js/jsx-runtime"
|
|
40
|
+
];
|
|
41
|
+
var externals_list_default = externals;
|
|
42
|
+
|
|
43
|
+
// src/head.ts
|
|
44
|
+
var importmap = { imports: {} };
|
|
45
|
+
for (const name of externals_list_default) {
|
|
46
|
+
importmap.imports[name] = `/packages/${name}.js`;
|
|
47
|
+
}
|
|
48
|
+
var script = document.createElement("script");
|
|
49
|
+
script.type = "importmap";
|
|
50
|
+
script.textContent = JSON.stringify(importmap);
|
|
51
|
+
document.currentScript ? document.currentScript.after(script) : document.head.appendChild(script);
|
|
52
|
+
})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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 { Repo } from "@automerge/vanillajs/slim";
|
|
21
|
+
import type { AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
|
|
22
|
+
import type { Patchwork, PatchworkOptions } from "./types.js";
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
patchwork: Patchwork;
|
|
26
|
+
repo: Repo;
|
|
27
|
+
Automerge: typeof import("@automerge/automerge");
|
|
28
|
+
AutomergeRepo: typeof import("@automerge/automerge-repo");
|
|
29
|
+
hive?: AutomergeRepoKeyhive;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export declare function setup(options?: PatchworkOptions): Promise<Patchwork>;
|
|
33
|
+
export default setup;
|
|
34
|
+
export { createRepo, initWasm } from "./repo.js";
|
|
35
|
+
export { createRouter } from "./router.js";
|
|
36
|
+
export { showLoadingAnimation, hideLoadingAnimation, showErrorScreen, } from "./loading.js";
|
|
37
|
+
export type { OpenOptions, Patchwork, PatchworkOptions, ServiceWorkerApi, SignerIdentity, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
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 { MessageChannelNetworkAdapter, isValidAutomergeUrl, } from "@automerge/vanillajs/slim";
|
|
21
|
+
import * as Automerge from "@automerge/automerge/slim";
|
|
22
|
+
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
23
|
+
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
24
|
+
import { importAutomergePackageViaWorker } from "@inkandswitch/patchwork-bootloader/module-loader";
|
|
25
|
+
import { registerPatchworkViewElement } from "@inkandswitch/patchwork-elements";
|
|
26
|
+
import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
|
|
27
|
+
import { createDocOfDatatype2, getRegistry, registerPlugins, resolveAccountHandle, unregisterPlugins, } from "@inkandswitch/patchwork-plugins";
|
|
28
|
+
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
29
|
+
import setupServiceWorker, { lifecycleLog, } from "@inkandswitch/patchwork-bootloader";
|
|
30
|
+
import debug from "debug";
|
|
31
|
+
import { createRepo, firstRepoPort, initWasm, removeAdapterFor, } from "./repo.js";
|
|
32
|
+
import { createRouter } from "./router.js";
|
|
33
|
+
const log = debug("patchwork:setup");
|
|
34
|
+
// ── Setup ────────────────────────────────────────────────────────────────
|
|
35
|
+
let setupCalled = false;
|
|
36
|
+
export function setup(options = {}) {
|
|
37
|
+
if (setupCalled) {
|
|
38
|
+
throw new Error("patchwork.setup: already called — setup owns page-wide state and may run only once");
|
|
39
|
+
}
|
|
40
|
+
setupCalled = true;
|
|
41
|
+
const done = doSetup(options);
|
|
42
|
+
void done.then(() => log("patchwork setup complete"));
|
|
43
|
+
const timeout = options.timeout ?? 30_000;
|
|
44
|
+
if (timeout === false)
|
|
45
|
+
return done;
|
|
46
|
+
let timer;
|
|
47
|
+
const deadline = new Promise((_, reject) => {
|
|
48
|
+
timer = setTimeout(() => reject(new Error(`patchwork.setup: boot did not finish within ${timeout}ms`)), timeout);
|
|
49
|
+
});
|
|
50
|
+
done.then(() => clearTimeout(timer), () => clearTimeout(timer));
|
|
51
|
+
return Promise.race([done, deadline]);
|
|
52
|
+
}
|
|
53
|
+
export default setup;
|
|
54
|
+
async function doSetup(options) {
|
|
55
|
+
const siteName = options.name ??
|
|
56
|
+
(typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "patchwork");
|
|
57
|
+
const moduleSources = resolveDefaultModules(options);
|
|
58
|
+
const routing = options.routing ?? "hash";
|
|
59
|
+
log("booting", options);
|
|
60
|
+
installLifecycleLogging();
|
|
61
|
+
if (!options.repo)
|
|
62
|
+
await initWasm();
|
|
63
|
+
const sw = await setupServiceWorker();
|
|
64
|
+
if (!sw)
|
|
65
|
+
throw new Error("Failed to set up service worker");
|
|
66
|
+
log("workers ready");
|
|
67
|
+
let hive;
|
|
68
|
+
let repo;
|
|
69
|
+
let signerIdentity;
|
|
70
|
+
// Called with a fresh port when the automerge worker dies and is recreated.
|
|
71
|
+
// Assigned once the repo exists.
|
|
72
|
+
let onWorkerPortRenewed;
|
|
73
|
+
if (options.repo) {
|
|
74
|
+
log("using provided Repo");
|
|
75
|
+
repo = options.repo;
|
|
76
|
+
hive = options.hive;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const workerPort = await firstRepoPort(sw, (port) => {
|
|
80
|
+
if (onWorkerPortRenewed)
|
|
81
|
+
onWorkerPortRenewed(port);
|
|
82
|
+
else {
|
|
83
|
+
console.warn("automerge worker port renewed before the repo existed; dropping it");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
|
|
87
|
+
({ repo, hive, signerIdentity } = await createRepo(options, siteName, workerAdapter));
|
|
88
|
+
// The worker was recreated with cold state: wire the repo onto the fresh
|
|
89
|
+
// port and drop the adapter stranded on the dead one.
|
|
90
|
+
const bootHive = hive;
|
|
91
|
+
onWorkerPortRenewed = (port) => {
|
|
92
|
+
const fresh = new MessageChannelNetworkAdapter(port);
|
|
93
|
+
// Mirror the boot wiring: a keyhive repo talks to the worker through a
|
|
94
|
+
// keyhive adapter wrapped around the message channel.
|
|
95
|
+
const registered = bootHive
|
|
96
|
+
? bootHive.createKeyhiveNetworkAdapter(fresh, false, false, 2000)
|
|
97
|
+
: fresh;
|
|
98
|
+
repo.networkSubsystem.addNetworkAdapter(registered);
|
|
99
|
+
removeAdapterFor(repo, workerAdapter, registered);
|
|
100
|
+
workerAdapter = fresh;
|
|
101
|
+
lifecycleLog("repo re-wired to the recreated automerge worker");
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Dev-console / tool-runtime globals (e2e and loaded tools read these). The
|
|
105
|
+
// `window.patchwork` handle is deliberately not set here — the caller does
|
|
106
|
+
// `window.patchwork = await setup(...)`.
|
|
107
|
+
window.repo = repo;
|
|
108
|
+
window.Automerge = Automerge;
|
|
109
|
+
window.AutomergeRepo =
|
|
110
|
+
AutomergeRepo;
|
|
111
|
+
if (hive)
|
|
112
|
+
window.hive = hive;
|
|
113
|
+
await repo.networkSubsystem.whenReady();
|
|
114
|
+
log("networkSubsystem ready");
|
|
115
|
+
hive?.networkAdapter?.syncKeyhive?.();
|
|
116
|
+
registerRepoProviderElement(repo);
|
|
117
|
+
const rootElementId = options.rootElementId ?? "root";
|
|
118
|
+
const rootElement = document.getElementById(rootElementId);
|
|
119
|
+
if (!rootElement) {
|
|
120
|
+
throw new Error(`patchwork.setup: no element with id="${rootElementId}"`);
|
|
121
|
+
}
|
|
122
|
+
// `<repo-provider>` sits above the root and answers `repo:handle-descriptor`
|
|
123
|
+
// for any view outside a remapper, resolving to the requested url unchanged.
|
|
124
|
+
// Generated site html already declares it around the root; wrap only
|
|
125
|
+
// hand-written markup that lacks one.
|
|
126
|
+
if (!rootElement.closest("repo-provider")) {
|
|
127
|
+
const repoProvider = document.createElement("repo-provider");
|
|
128
|
+
rootElement.parentElement.insertBefore(repoProvider, rootElement);
|
|
129
|
+
repoProvider.appendChild(rootElement);
|
|
130
|
+
}
|
|
131
|
+
registerPatchworkViewElement({ hive, repo });
|
|
132
|
+
// Started with the site bundle alone so resolveAccountHandle has something to
|
|
133
|
+
// await on — the `account` datatype lives there. The user's own
|
|
134
|
+
// module-settings URL is added lazily once it appears on the account doc.
|
|
135
|
+
const moduleWatcher = new ModuleWatcher(repo, nameSources(moduleSources), onModuleLoaded, unregisterPlugins,
|
|
136
|
+
// Discover an Automerge package's plugin descriptors off the main thread;
|
|
137
|
+
// each plugin's load() re-imports the package (at heads) on this thread.
|
|
138
|
+
importAutomergePackageViaWorker);
|
|
139
|
+
const accountDocHandle = (await resolveAccountHandle(repo, {
|
|
140
|
+
storageKey: options.accountKey ?? "patchworkAccountURL",
|
|
141
|
+
hive,
|
|
142
|
+
}));
|
|
143
|
+
wireModuleSettings(accountDocHandle, moduleWatcher);
|
|
144
|
+
let router;
|
|
145
|
+
if (routing !== false) {
|
|
146
|
+
rootElement.style.visibility = "hidden";
|
|
147
|
+
router = createRouter({
|
|
148
|
+
rootElement,
|
|
149
|
+
repo,
|
|
150
|
+
accountDocHandle,
|
|
151
|
+
siteName,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const toolsLoaded = moduleWatcher.doneLoading.then(() => log("doneLoading, tools registered:", getRegistry("patchwork:tool")
|
|
155
|
+
.all()
|
|
156
|
+
.map((t) => t.id)), (err) => console.error("doneLoading rejected:", err));
|
|
157
|
+
installReveal(rootElement, router, toolsLoaded);
|
|
158
|
+
return {
|
|
159
|
+
repo,
|
|
160
|
+
hive,
|
|
161
|
+
account: accountDocHandle,
|
|
162
|
+
signer: signerIdentity,
|
|
163
|
+
packages: moduleWatcher,
|
|
164
|
+
plugins,
|
|
165
|
+
sw: {
|
|
166
|
+
connectClassicSync: sw.connectClassicSync,
|
|
167
|
+
subscribeToRepoChannel: sw.subscribeToRepoChannel,
|
|
168
|
+
subscribeSyncState: sw.subscribeSyncState,
|
|
169
|
+
},
|
|
170
|
+
async create(type, init) {
|
|
171
|
+
const datatype = await getRegistry("patchwork:datatype").load(type);
|
|
172
|
+
if (!datatype) {
|
|
173
|
+
throw new Error(`patchwork.create: no datatype registered for "${type}"`);
|
|
174
|
+
}
|
|
175
|
+
return createDocOfDatatype2(datatype, repo, init, hive);
|
|
176
|
+
},
|
|
177
|
+
open(url, openOptions = {}) {
|
|
178
|
+
rootElement.dispatchEvent(new CustomEvent("patchwork:open-document", {
|
|
179
|
+
detail: {
|
|
180
|
+
url,
|
|
181
|
+
toolId: openOptions.tool,
|
|
182
|
+
type: openOptions.type,
|
|
183
|
+
title: openOptions.title,
|
|
184
|
+
},
|
|
185
|
+
}));
|
|
186
|
+
},
|
|
187
|
+
find(url) {
|
|
188
|
+
return repo.find(url);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// ── Reveal / mount ───────────────────────────────────────────────────────
|
|
193
|
+
function installReveal(rootElement, router, toolsLoaded) {
|
|
194
|
+
let revealed = false;
|
|
195
|
+
const reveal = () => {
|
|
196
|
+
if (revealed)
|
|
197
|
+
return;
|
|
198
|
+
revealed = true;
|
|
199
|
+
rootElement.style.visibility = "visible";
|
|
200
|
+
};
|
|
201
|
+
rootElement.addEventListener("patchwork:mounted", (event) => {
|
|
202
|
+
if (event.target !== rootElement)
|
|
203
|
+
return;
|
|
204
|
+
log("root element mounted");
|
|
205
|
+
if (router) {
|
|
206
|
+
void router.route();
|
|
207
|
+
// Deep-links into tools that were still loading get routed again once
|
|
208
|
+
// every module has registered.
|
|
209
|
+
void toolsLoaded.then(() => router.route());
|
|
210
|
+
}
|
|
211
|
+
reveal();
|
|
212
|
+
});
|
|
213
|
+
// If nothing ever mounts, reveal anyway so the user sees something rather
|
|
214
|
+
// than a blank page.
|
|
215
|
+
setTimeout(reveal, 12_000);
|
|
216
|
+
}
|
|
217
|
+
// ── Module sources ───────────────────────────────────────────────────────
|
|
218
|
+
function isValidModuleSource(source) {
|
|
219
|
+
return isValidAutomergeUrl(source) || /^(https?:\/\/|\.?\/)/.test(source);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The site's default module-list sources, honouring the
|
|
223
|
+
* `localStorage.systemPackageListURL` dev override, which replaces the entire
|
|
224
|
+
* built-in bundle.
|
|
225
|
+
*/
|
|
226
|
+
function resolveDefaultModules(options) {
|
|
227
|
+
const configured = options.packageListURL ?? [];
|
|
228
|
+
const builtin = (Array.isArray(configured) ? configured : [configured]).filter(Boolean);
|
|
229
|
+
const override = globalThis.localStorage?.getItem("systemPackageListURL");
|
|
230
|
+
if (override && isValidModuleSource(override)) {
|
|
231
|
+
console.info(`using systemPackageListURL from localStorage: ${override}`);
|
|
232
|
+
return [override];
|
|
233
|
+
}
|
|
234
|
+
if (override) {
|
|
235
|
+
console.warn(`ignoring invalid systemPackageListURL in localStorage: ${override}`);
|
|
236
|
+
}
|
|
237
|
+
if (builtin.length === 0) {
|
|
238
|
+
throw new Error("patchwork.setup: no default module sources configured (set `packageListURL`)");
|
|
239
|
+
}
|
|
240
|
+
return builtin;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Name the sources for the ModuleWatcher. The first keeps the canonical
|
|
244
|
+
* `system` name; the rest get suffixed. None may be `user`, which is reserved
|
|
245
|
+
* for the per-account settings doc and has branch-override precedence.
|
|
246
|
+
*/
|
|
247
|
+
function nameSources(sources) {
|
|
248
|
+
return Object.fromEntries(sources.map((source, i) => [i === 0 ? "system" : `system-${i}`, source]));
|
|
249
|
+
}
|
|
250
|
+
function onModuleLoaded(name, mod) {
|
|
251
|
+
if (!Array.isArray(mod.plugins)) {
|
|
252
|
+
console.warn(`module ${name} has no plugins array`, Object.keys(mod));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
log(`registering ${mod.plugins.length} plugin(s) from ${name}`, mod.plugins.map((p) => `${p.type}:${p.id}`));
|
|
256
|
+
registerPlugins(mod.plugins, name);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The frame lazy-creates `moduleSettingsUrl` on first mount, so watch for it to
|
|
260
|
+
* appear and feed it to the ModuleWatcher.
|
|
261
|
+
*/
|
|
262
|
+
function wireModuleSettings(accountDocHandle, moduleWatcher) {
|
|
263
|
+
const wire = () => {
|
|
264
|
+
const url = accountDocHandle.doc()?.moduleSettingsUrl;
|
|
265
|
+
if (!url)
|
|
266
|
+
return;
|
|
267
|
+
void moduleWatcher.addUrl("user", url);
|
|
268
|
+
accountDocHandle.off("change", wire);
|
|
269
|
+
};
|
|
270
|
+
wire();
|
|
271
|
+
if (!accountDocHandle.doc()?.moduleSettingsUrl) {
|
|
272
|
+
accountDocHandle.on("change", wire);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Page Lifecycle and connectivity transitions, to line up against the
|
|
276
|
+
* SharedWorker's sync-socket reaps. */
|
|
277
|
+
function installLifecycleLogging() {
|
|
278
|
+
if (typeof document === "undefined")
|
|
279
|
+
return;
|
|
280
|
+
const opts = { capture: true };
|
|
281
|
+
const persisted = (e) => e.persisted;
|
|
282
|
+
document.addEventListener("visibilitychange", () => lifecycleLog("visibilitychange → %s", document.visibilityState), opts);
|
|
283
|
+
document.addEventListener("freeze", () => lifecycleLog("freeze (tab suspended)"), opts);
|
|
284
|
+
document.addEventListener("resume", () => lifecycleLog("resume (tab unsuspended)"), opts);
|
|
285
|
+
window.addEventListener("pageshow", (e) => lifecycleLog("pageshow persisted=%s", persisted(e)), opts);
|
|
286
|
+
window.addEventListener("pagehide", (e) => lifecycleLog("pagehide persisted=%s", persisted(e)), opts);
|
|
287
|
+
window.addEventListener("online", () => lifecycleLog("online"), opts);
|
|
288
|
+
window.addEventListener("offline", () => lifecycleLog("offline"), opts);
|
|
289
|
+
lifecycleLog("logging installed (visibilityState=%s, hasFocus=%s)", document.visibilityState, document.hasFocus());
|
|
290
|
+
}
|
|
291
|
+
// ── Named exports ────────────────────────────────────────────────────────
|
|
292
|
+
export { createRepo, initWasm } from "./repo.js";
|
|
293
|
+
export { createRouter } from "./router.js";
|
|
294
|
+
export { showLoadingAnimation, hideLoadingAnimation, showErrorScreen, } from "./loading.js";
|
package/dist/loading.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const LOADING_ELEMENT_ID = "pw-loading";
|
|
2
|
+
const LOADING_STYLE_ID = "pw-loading-styles";
|
|
3
|
+
const LOADING_CSS = `
|
|
4
|
+
@keyframes pw-loading-pulse {
|
|
5
|
+
0%, 100% { opacity: 0.25; }
|
|
6
|
+
50% { opacity: 0.95; }
|
|
7
|
+
}
|
|
8
|
+
#${LOADING_ELEMENT_ID} {
|
|
9
|
+
position: fixed;
|
|
10
|
+
inset: 0;
|
|
11
|
+
z-index: 0;
|
|
12
|
+
pointer-events: none;
|
|
13
|
+
background-color: #fff;
|
|
14
|
+
background-image:
|
|
15
|
+
radial-gradient(ellipse 55% 45% at 28% 35%, #fde4ec, transparent 70%),
|
|
16
|
+
radial-gradient(ellipse 50% 55% at 72% 65%, #e0f0fb, transparent 70%),
|
|
17
|
+
radial-gradient(ellipse 65% 55% at 50% 50%, #f1e6f6, transparent 80%);
|
|
18
|
+
animation: pw-loading-pulse 3.5s ease-in-out infinite;
|
|
19
|
+
transition: opacity 0.6s ease-out;
|
|
20
|
+
}
|
|
21
|
+
@media (prefers-color-scheme: dark) {
|
|
22
|
+
#${LOADING_ELEMENT_ID} {
|
|
23
|
+
background-color: #000;
|
|
24
|
+
background-image:
|
|
25
|
+
radial-gradient(ellipse 55% 45% at 28% 35%, #2a1d33, transparent 70%),
|
|
26
|
+
radial-gradient(ellipse 50% 55% at 72% 65%, #1a2738, transparent 70%),
|
|
27
|
+
radial-gradient(ellipse 65% 55% at 50% 50%, #221a2e, transparent 80%);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
#${LOADING_ELEMENT_ID}.pw-loading-fading {
|
|
31
|
+
opacity: 0;
|
|
32
|
+
animation: none;
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
export function showLoadingAnimation() {
|
|
36
|
+
if (!document.getElementById(LOADING_STYLE_ID)) {
|
|
37
|
+
const style = document.createElement("style");
|
|
38
|
+
style.id = LOADING_STYLE_ID;
|
|
39
|
+
style.textContent = LOADING_CSS;
|
|
40
|
+
document.head.appendChild(style);
|
|
41
|
+
}
|
|
42
|
+
if (document.getElementById(LOADING_ELEMENT_ID))
|
|
43
|
+
return;
|
|
44
|
+
const el = document.createElement("div");
|
|
45
|
+
el.id = LOADING_ELEMENT_ID;
|
|
46
|
+
document.body.appendChild(el);
|
|
47
|
+
}
|
|
48
|
+
export function hideLoadingAnimation() {
|
|
49
|
+
const el = document.getElementById(LOADING_ELEMENT_ID);
|
|
50
|
+
if (!el)
|
|
51
|
+
return;
|
|
52
|
+
el.classList.add("pw-loading-fading");
|
|
53
|
+
setTimeout(() => el.remove(), 700);
|
|
54
|
+
}
|
|
55
|
+
const ERROR_ELEMENT_ID = "pw-error";
|
|
56
|
+
const ERROR_CSS = `
|
|
57
|
+
#${ERROR_ELEMENT_ID} {
|
|
58
|
+
position: fixed;
|
|
59
|
+
inset: 0;
|
|
60
|
+
z-index: 1;
|
|
61
|
+
overflow: auto;
|
|
62
|
+
padding: 3rem 1.5rem;
|
|
63
|
+
background: #fff;
|
|
64
|
+
color: #1a1a1a;
|
|
65
|
+
font: 1rem/1.5 system-ui, sans-serif;
|
|
66
|
+
}
|
|
67
|
+
#${ERROR_ELEMENT_ID} > div {
|
|
68
|
+
max-width: 38rem;
|
|
69
|
+
margin: 0 auto;
|
|
70
|
+
}
|
|
71
|
+
#${ERROR_ELEMENT_ID} h1 {
|
|
72
|
+
font-size: 1.25rem;
|
|
73
|
+
margin: 0 0 1rem;
|
|
74
|
+
}
|
|
75
|
+
#${ERROR_ELEMENT_ID} pre {
|
|
76
|
+
padding: 1rem;
|
|
77
|
+
border-radius: 0.5rem;
|
|
78
|
+
background: #f4f0f2;
|
|
79
|
+
overflow: auto;
|
|
80
|
+
white-space: pre-wrap;
|
|
81
|
+
font-size: 0.8125rem;
|
|
82
|
+
}
|
|
83
|
+
@media (prefers-color-scheme: dark) {
|
|
84
|
+
#${ERROR_ELEMENT_ID} {
|
|
85
|
+
background: #111;
|
|
86
|
+
color: #eee;
|
|
87
|
+
}
|
|
88
|
+
#${ERROR_ELEMENT_ID} pre {
|
|
89
|
+
background: #221a2e;
|
|
90
|
+
}
|
|
91
|
+
#${ERROR_ELEMENT_ID} a {
|
|
92
|
+
color: #9cc8f0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
export function showErrorScreen(error, options = {}) {
|
|
97
|
+
hideLoadingAnimation();
|
|
98
|
+
if (document.getElementById(ERROR_ELEMENT_ID))
|
|
99
|
+
return;
|
|
100
|
+
const style = document.createElement("style");
|
|
101
|
+
style.textContent = ERROR_CSS;
|
|
102
|
+
document.head.appendChild(style);
|
|
103
|
+
const screen = document.createElement("div");
|
|
104
|
+
screen.id = ERROR_ELEMENT_ID;
|
|
105
|
+
const inner = document.createElement("div");
|
|
106
|
+
screen.appendChild(inner);
|
|
107
|
+
const heading = document.createElement("h1");
|
|
108
|
+
heading.textContent = "Something went wrong starting this site";
|
|
109
|
+
inner.appendChild(heading);
|
|
110
|
+
const detail = document.createElement("pre");
|
|
111
|
+
detail.textContent =
|
|
112
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
113
|
+
inner.appendChild(detail);
|
|
114
|
+
const advice = document.createElement("p");
|
|
115
|
+
advice.append("Reloading the page may help. If it keeps happening");
|
|
116
|
+
if (options.contact) {
|
|
117
|
+
const link = document.createElement("a");
|
|
118
|
+
link.href = `mailto:${options.contact}`;
|
|
119
|
+
link.textContent = options.contact;
|
|
120
|
+
advice.append(", email ", link);
|
|
121
|
+
}
|
|
122
|
+
advice.append(".");
|
|
123
|
+
inner.appendChild(advice);
|
|
124
|
+
document.body.appendChild(screen);
|
|
125
|
+
}
|
package/dist/repo.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MessageChannelNetworkAdapter, Repo } from "@automerge/vanillajs/slim";
|
|
2
|
+
import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
|
|
3
|
+
import setupServiceWorker from "@inkandswitch/patchwork-bootloader";
|
|
4
|
+
import type { SignerIdentity } from "./types.js";
|
|
5
|
+
export declare function initWasm(): Promise<void>;
|
|
6
|
+
export declare function createRepo(config: {
|
|
7
|
+
keyhive?: boolean;
|
|
8
|
+
}, siteName: string, workerAdapter: MessageChannelNetworkAdapter): Promise<{
|
|
9
|
+
repo: Repo;
|
|
10
|
+
hive?: AutomergeRepoKeyhive;
|
|
11
|
+
signerIdentity?: SignerIdentity;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve with the first repo port the worker delivers, calling `onRenewed` for
|
|
15
|
+
* every later one.
|
|
16
|
+
*
|
|
17
|
+
* subscribeToRepoChannel is deliberately not awaited: it resolves only after
|
|
18
|
+
* the boot channel's port-ready handshake, which can take its full 30s timeout
|
|
19
|
+
* against a stranded worker connection. Boot blocks on the first *delivered*
|
|
20
|
+
* port instead — if the boot channel stalls, worker recovery hands the listener
|
|
21
|
+
* a good port long before that timeout.
|
|
22
|
+
*/
|
|
23
|
+
export declare function firstRepoPort(sw: Awaited<ReturnType<typeof setupServiceWorker>>, onRenewed: (port: MessagePort) => void): Promise<MessagePort>;
|
|
24
|
+
/** Drop the adapter sitting on the dead worker port, leaving `keep` in place. */
|
|
25
|
+
export declare function removeAdapterFor(repo: Repo, stale: MessageChannelNetworkAdapter, keep: unknown): void;
|