@inkandswitch/patchwork 0.0.1 → 0.2.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 ADDED
@@ -0,0 +1,57 @@
1
+ # @inkandswitch/patchwork
2
+
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f00dcb8: Add `repository` metadata pointing at inkandswitch/patchwork-system, so npm links each package to its source directory and can attest provenance when published from CI.
8
+ - Updated dependencies [f00dcb8]
9
+ - @inkandswitch/patchwork-bootloader@0.5.2
10
+ - @inkandswitch/patchwork-filesystem@0.2.5
11
+ - @inkandswitch/patchwork-elements@4.0.4
12
+ - @inkandswitch/patchwork-plugins@1.0.3
13
+
14
+ ## 0.2.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 2fffabe: `setup()` now uses `packageListURL` as given instead of letting `localStorage.systemPackageListURL` silently replace it. A site that wants a dev override resolves it itself and passes the result in:
19
+
20
+ ```ts
21
+ const packageListURL =
22
+ new URLSearchParams(location.search).get("system-package-list") ||
23
+ localStorage.getItem("systemPackageListURL") ||
24
+ DEFAULT_PACKAGE_LIST;
25
+ ```
26
+
27
+ This keeps the precedence in one place — the site — so a site can add its own override sources without fighting the library for priority.
28
+
29
+ ### Patch Changes
30
+
31
+ - 6be3922: Wait for configured modules to load before routing the root view so registered frame tools are available for the initial route.
32
+ - 77bd37c: Expose the account document handle as `window.accountDocHandle` alongside `window.patchwork.account`.
33
+ - 5f70c14: Add `repository` metadata pointing at inkandswitch/patchwork-next, so npm links each package to its source directory and can attest provenance when published from CI.
34
+ - Updated dependencies [5f70c14]
35
+ - @inkandswitch/patchwork-bootloader@0.5.1
36
+ - @inkandswitch/patchwork-filesystem@0.2.4
37
+ - @inkandswitch/patchwork-elements@4.0.3
38
+ - @inkandswitch/patchwork-plugins@1.0.2
39
+
40
+ ## 0.1.0
41
+
42
+ ### Minor Changes
43
+
44
+ - 0aa315d: Configure Subduction or Keyhive with exclusive `syncServers` configuration. `syncServers.keyhive` replaces the `keyhive` and `keyhiveSyncServer` site options and the runtime `setup({ keyhive })` option. Selecting a named ARK relay or providing a custom relay identity and URL enables Keyhive, and configured server URLs now control worker connections as well as connection hints.
45
+ - bd63259: New package: one import for a Patchwork site. It owns the boot sequence (`repo`, `router`, `loading`), the vite plugin (config, html, importmap, manifest, netlify, icons, service worker), the site-kit config helpers, and the ambient client types — all previously spread across the bootloader and each site's own `index.html`, `vite.config.ts`, and `public/` directory.
46
+
47
+ A site is now a `package.json` dependency, a `vite.config.ts` with `patchwork({...})`, and a `main.ts` that imports `@inkandswitch/patchwork`.
48
+
49
+ ### Patch Changes
50
+
51
+ - Updated dependencies [0aa315d]
52
+ - Updated dependencies [bd63259]
53
+ - Updated dependencies [bd63259]
54
+ - Updated dependencies [bd63259]
55
+ - @inkandswitch/patchwork-bootloader@0.5.0
56
+ - @inkandswitch/patchwork-elements@4.0.2
57
+ - @inkandswitch/patchwork-filesystem@0.2.3
package/dist/client.d.ts CHANGED
@@ -13,14 +13,12 @@ import "@inkandswitch/patchwork-providers";
13
13
 
14
14
  declare global {
15
15
  const __SITE_NAME__: string;
16
- const __KEYHIVE__: boolean;
17
- const __KEYHIVE_SYNC_SERVER__: boolean;
18
16
 
19
17
  interface ImportMetaEnv {
20
18
  /**
21
19
  * Comma-separated list of default tool-manifest sources the shell boots
22
20
  * with. Each entry is an `automerge:` URL or a static `modules.json`
23
- * URL. Overridable at runtime via `localStorage.systemPackageListURL`.
21
+ * URL.
24
22
  */
25
23
  readonly PATCHWORK_SYSTEM_PACKAGE_LIST_URL?: string;
26
24
  /**
package/dist/index.d.ts CHANGED
@@ -17,12 +17,14 @@
17
17
  * `@inkandswitch/patchwork-bootloader` directly, which does SW registration
18
18
  * and the automerge-worker handoff and nothing else.
19
19
  */
20
- import { Repo } from "@automerge/vanillajs/slim";
20
+ import { type DocHandle, Repo } from "@automerge/vanillajs/slim";
21
21
  import type { AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
22
+ import { type AccountDoc } from "@inkandswitch/patchwork-plugins";
22
23
  import type { Patchwork, PatchworkOptions } from "./types.js";
23
24
  declare global {
24
25
  interface Window {
25
26
  patchwork: Patchwork;
27
+ accountDocHandle: DocHandle<AccountDoc>;
26
28
  repo: Repo;
27
29
  Automerge: typeof import("@automerge/automerge");
28
30
  AutomergeRepo: typeof import("@automerge/automerge-repo");
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * `@inkandswitch/patchwork-bootloader` directly, which does SW registration
18
18
  * and the automerge-worker handoff and nothing else.
19
19
  */
20
- import { MessageChannelNetworkAdapter, isValidAutomergeUrl, } from "@automerge/vanillajs/slim";
20
+ import { MessageChannelNetworkAdapter, } from "@automerge/vanillajs/slim";
21
21
  import * as Automerge from "@automerge/automerge/slim";
22
22
  import * as AutomergeRepo from "@automerge/automerge-repo/slim";
23
23
  import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
@@ -84,7 +84,7 @@ async function doSetup(options) {
84
84
  }
85
85
  });
86
86
  let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
87
- ({ repo, hive, signerIdentity } = await createRepo(options, siteName, workerAdapter));
87
+ ({ repo, hive, signerIdentity } = await createRepo(siteName, workerAdapter));
88
88
  // The worker was recreated with cold state: wire the repo onto the fresh
89
89
  // port and drop the adapter stranded on the dead one.
90
90
  const bootHive = hive;
@@ -140,10 +140,15 @@ async function doSetup(options) {
140
140
  storageKey: options.accountKey ?? "patchworkAccountURL",
141
141
  hive,
142
142
  }));
143
+ window.accountDocHandle = accountDocHandle;
143
144
  wireModuleSettings(accountDocHandle, moduleWatcher);
145
+ const toolsLoaded = moduleWatcher.doneLoading.then(() => log("doneLoading, tools registered:", getRegistry("patchwork:tool")
146
+ .all()
147
+ .map((t) => t.id)), (err) => console.error("doneLoading rejected:", err));
144
148
  let router;
145
149
  if (routing !== false) {
146
150
  rootElement.style.visibility = "hidden";
151
+ await toolsLoaded;
147
152
  router = createRouter({
148
153
  rootElement,
149
154
  repo,
@@ -151,9 +156,6 @@ async function doSetup(options) {
151
156
  siteName,
152
157
  });
153
158
  }
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
159
  installReveal(rootElement, router, toolsLoaded);
158
160
  return {
159
161
  repo,
@@ -215,25 +217,13 @@ function installReveal(rootElement, router, toolsLoaded) {
215
217
  setTimeout(reveal, 12_000);
216
218
  }
217
219
  // ── Module sources ───────────────────────────────────────────────────────
218
- function isValidModuleSource(source) {
219
- return isValidAutomergeUrl(source) || /^(https?:\/\/|\.?\/)/.test(source);
220
- }
221
220
  /**
222
- * The site's default module-list sources, honouring the
223
- * `localStorage.systemPackageListURL` dev override, which replaces the entire
224
- * built-in bundle.
221
+ * The site's module-list sources. The site owns any dev overrides and passes
222
+ * the result in as `packageListURL`.
225
223
  */
226
224
  function resolveDefaultModules(options) {
227
225
  const configured = options.packageListURL ?? [];
228
226
  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
227
  if (builtin.length === 0) {
238
228
  throw new Error("patchwork.setup: no default module sources configured (set `packageListURL`)");
239
229
  }
package/dist/repo.d.ts CHANGED
@@ -3,9 +3,7 @@ import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
3
3
  import setupServiceWorker from "@inkandswitch/patchwork-bootloader";
4
4
  import type { SignerIdentity } from "./types.js";
5
5
  export declare function initWasm(): Promise<void>;
6
- export declare function createRepo(config: {
7
- keyhive?: boolean;
8
- }, siteName: string, workerAdapter: MessageChannelNetworkAdapter): Promise<{
6
+ export declare function createRepo(siteName: string, workerAdapter: MessageChannelNetworkAdapter): Promise<{
9
7
  repo: Repo;
10
8
  hive?: AutomergeRepoKeyhive;
11
9
  signerIdentity?: SignerIdentity;
package/dist/repo.js CHANGED
@@ -7,7 +7,9 @@ import { initSync as initSubductionSync } from "@automerge/automerge-subduction/
7
7
  import { MemorySigner } from "@automerge/automerge-subduction/slim";
8
8
  import debug from "debug";
9
9
  const log = debug("patchwork:setup:repo");
10
- const useKeyhiveSyncServer = typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
10
+ const syncServer = typeof __SYNC_SERVER__ !== "undefined"
11
+ ? __SYNC_SERVER__
12
+ : { url: "wss://subduction.sync.inkandswitch.com" };
11
13
  // Fetch and initialize automerge + subduction wasm. Memoized: the fetches start
12
14
  // on the first call and every later caller awaits the same init. Skipped
13
15
  // entirely when the site brings its own Repo (it did this itself).
@@ -25,8 +27,8 @@ export function initWasm() {
25
27
  }
26
28
  return wasmReady;
27
29
  }
28
- export async function createRepo(config, siteName, workerAdapter) {
29
- if (config.keyhive) {
30
+ export async function createRepo(siteName, workerAdapter) {
31
+ if (syncServer.keyhive) {
30
32
  log("setting up keyhive");
31
33
  initKeyhiveWasm();
32
34
  const { hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
@@ -38,7 +40,7 @@ export async function createRepo(config, siteName, workerAdapter) {
38
40
  cachingMode: "periodic",
39
41
  onlyShareWithHardcodedServerPeerId: false,
40
42
  // ARK selects the relay via `syncServer`, defaulting to "subduction".
41
- ...(useKeyhiveSyncServer ? { syncServer: "keyhive" } : {}),
43
+ syncServer: syncServer.keyhive,
42
44
  repo: {
43
45
  storage: new IndexedDBWorkerStorageAdapter(),
44
46
  enableRemoteHeadsGossiping: true,
@@ -7,7 +7,7 @@
7
7
  * wires these into vite's plugin hooks (dev middleware, emitFile, virtual
8
8
  * modules, config()).
9
9
  */
10
- export type { PatchworkSiteOptions, PatchworkIconsOptions, PatchworkHtmlOptions, PatchworkNetlifyOptions, PatchworkSyncServersOptions, } from "./options.js";
10
+ export type { PatchworkSiteOptions, PatchworkIconsOptions, PatchworkHtmlOptions, PatchworkNetlifyOptions, PatchworkKeyhiveSyncServer, PatchworkSyncServersOptions, } from "./options.js";
11
11
  export { resolveSyncServers, PRELOAD_WASM_ASSETS } from "./sync-servers.js";
12
12
  export { getIcons, ICON_SPECS, type IconSpec } from "./icons.js";
13
13
  export { buildHtml, escapeHtml } from "./html.js";
@@ -1,3 +1,4 @@
1
+ import type { SyncServerIdentity } from "@automerge/automerge-repo-keyhive";
1
2
  /**
2
3
  * The bundler-agnostic option shape for a Patchwork site's generated
3
4
  * static assets (icons, index.html, manifest.webmanifest, Netlify config).
@@ -22,14 +23,20 @@ export interface PatchworkNetlifyOptions {
22
23
  /** Cache-Control: immutable on /assets/*. Default true. */
23
24
  immutableAssets?: boolean;
24
25
  }
25
- export interface PatchworkSyncServersOptions {
26
+ type PatchworkPrimarySyncServerOptions = {
27
+ subduction?: string;
28
+ keyhive?: never;
29
+ } | {
30
+ subduction?: never;
31
+ keyhive: PatchworkKeyhiveSyncServer;
32
+ };
33
+ export type PatchworkKeyhiveSyncServer = "keyhive" | "subduction" | ({
34
+ url: string;
35
+ } & SyncServerIdentity);
36
+ export type PatchworkSyncServersOptions = {
26
37
  /** wss:// URL for the legacy automerge-repo sync-server channel (connected on demand via connectClassicSync). Default: wss://sync3.automerge.org. Pass false to skip its preconnect hint. */
27
38
  classic?: string | false;
28
- /** wss:// URL for subduction sync — live when keyhiveSyncServer is false. Default: wss://subduction.sync.inkandswitch.com */
29
- subduction?: string;
30
- /** wss:// URL for keyhive sync — live when keyhiveSyncServer is true. Default: wss://keyhive.sync.automerge.org */
31
- keyhive?: string;
32
- }
39
+ } & PatchworkPrimarySyncServerOptions;
33
40
  export interface PatchworkSiteOptions {
34
41
  /** -> __SITE_NAME__ define */
35
42
  siteName?: string;
@@ -41,10 +48,6 @@ export interface PatchworkSiteOptions {
41
48
  description?: string;
42
49
  /** default "/src/main.ts" — must be root-absolute, since the generated index.html doesn't live at the project root */
43
50
  entry?: string;
44
- /** -> __KEYHIVE__ define */
45
- keyhive?: boolean;
46
- /** -> __KEYHIVE_SYNC_SERVER__ define */
47
- keyhiveSyncServer?: boolean;
48
51
  themeColor?: string | {
49
52
  light: string;
50
53
  dark: string;
@@ -52,12 +55,12 @@ export interface PatchworkSiteOptions {
52
55
  /** manifest background_color */
53
56
  backgroundColor?: string;
54
57
  /**
55
- * Which sync-server hosts to preconnect/dns-prefetch from the html and
56
- * list in the Netlify Link header. Only the channel actually live for
57
- * this build is included subduction xor keyhive, picked by
58
- * `keyhiveSyncServer`, matching automerge-worker.ts's own selection
59
- * plus `classic` (on-demand, but cheap to hint) unless set to `false`.
60
- * Pass `false` to skip all sync-server hints.
58
+ * Sync-server configuration for this build. Providing `keyhive` enables
59
+ * keyhive and selects the relay identity ARK grants access to. Custom
60
+ * identities also require their WebSocket URL. `subduction` and `keyhive`
61
+ * are mutually exclusive. The live server and `classic` are also emitted as
62
+ * connection hints. Pass `false` to keep the default servers but skip those
63
+ * hints.
61
64
  */
62
65
  syncServers?: false | PatchworkSyncServersOptions;
63
66
  icons?: false | PatchworkIconsOptions;
@@ -65,3 +68,4 @@ export interface PatchworkSiteOptions {
65
68
  manifest?: false | Record<string, unknown>;
66
69
  netlify?: false | PatchworkNetlifyOptions;
67
70
  }
71
+ export {};
@@ -1,9 +1 @@
1
- /**
2
- * The bundler-agnostic option shape for a Patchwork site's generated
3
- * static assets (icons, index.html, manifest.webmanifest, Netlify config).
4
- * `../vite/patchwork-plugin.ts` extends this with vite-only config
5
- * (importmap, server/preview/worker/build) — a different bundler adapter
6
- * can reuse this same shape and the pure builders in this directory without
7
- * pulling in anything vite-specific.
8
- */
9
1
  export {};
@@ -1,11 +1,18 @@
1
1
  import type { PatchworkSiteOptions } from "./options.js";
2
+ import type { SyncServerSelection } from "@automerge/automerge-repo-keyhive";
3
+ export declare const DEFAULT_SYNC_SERVERS: {
4
+ classic: string;
5
+ subduction: string;
6
+ keyhive: string;
7
+ };
8
+ export declare function resolvePrimarySyncServer(options: PatchworkSiteOptions): {
9
+ url: string;
10
+ keyhive?: SyncServerSelection;
11
+ };
2
12
  /**
3
13
  * Resolves which sync-server origins are actually live for this build: the
4
- * channel that's live is subduction xor keyhive (picked by
5
- * `keyhiveSyncServer`, matching automerge-worker.ts's own selection) plus
6
- * classic (on-demand, but still worth a preconnect hint) unless disabled.
7
- * A flat list would drift from `keyhiveSyncServer` — e.g. always hinting
8
- * subduction even on a build that actually connects to keyhive.
14
+ * channel that's live is subduction xor keyhive plus classic (on-demand,
15
+ * but still worth a preconnect hint) unless disabled.
9
16
  */
10
17
  export declare function resolveSyncServers(options: PatchworkSiteOptions): string[];
11
18
  export declare const PRELOAD_WASM_ASSETS: string[];
@@ -2,33 +2,46 @@
2
2
  // and automerge-worker.ts's SUBDUCTION_SYNC_URL selection — kept here as
3
3
  // plain constants (rather than importing those runtime modules) since this
4
4
  // only needs the hostnames, not the browser-only logic that reads them.
5
- const DEFAULT_SYNC_SERVERS = {
5
+ export const DEFAULT_SYNC_SERVERS = {
6
6
  classic: "wss://sync3.automerge.org",
7
7
  subduction: "wss://subduction.sync.inkandswitch.com",
8
8
  keyhive: "wss://keyhive.sync.automerge.org",
9
9
  };
10
+ export function resolvePrimarySyncServer(options) {
11
+ const servers = options.syncServers || undefined;
12
+ if (servers?.keyhive) {
13
+ if (typeof servers.keyhive === "string") {
14
+ return {
15
+ keyhive: servers.keyhive,
16
+ url: DEFAULT_SYNC_SERVERS[servers.keyhive],
17
+ };
18
+ }
19
+ const { url, ...identity } = servers.keyhive;
20
+ return {
21
+ keyhive: identity,
22
+ url,
23
+ };
24
+ }
25
+ return {
26
+ url: servers?.subduction ?? DEFAULT_SYNC_SERVERS.subduction,
27
+ };
28
+ }
10
29
  function wsToHttpOrigin(wsUrl) {
11
30
  return wsUrl.replace(/^ws/, "http");
12
31
  }
13
32
  /**
14
33
  * Resolves which sync-server origins are actually live for this build: the
15
- * channel that's live is subduction xor keyhive (picked by
16
- * `keyhiveSyncServer`, matching automerge-worker.ts's own selection) plus
17
- * classic (on-demand, but still worth a preconnect hint) unless disabled.
18
- * A flat list would drift from `keyhiveSyncServer` — e.g. always hinting
19
- * subduction even on a build that actually connects to keyhive.
34
+ * channel that's live is subduction xor keyhive plus classic (on-demand,
35
+ * but still worth a preconnect hint) unless disabled.
20
36
  */
21
37
  export function resolveSyncServers(options) {
22
38
  if (options.syncServers === false)
23
39
  return [];
24
- const servers = { ...DEFAULT_SYNC_SERVERS, ...options.syncServers };
25
- const origins = [];
26
- const primary = options.keyhiveSyncServer
27
- ? servers.keyhive
28
- : servers.subduction;
29
- origins.push(primary);
30
- if (servers.classic)
31
- origins.push(servers.classic);
40
+ const primary = resolvePrimarySyncServer(options);
41
+ const classic = options.syncServers?.classic ?? DEFAULT_SYNC_SERVERS.classic;
42
+ const origins = [primary.url];
43
+ if (classic)
44
+ origins.push(classic);
32
45
  return origins.map(wsToHttpOrigin);
33
46
  }
34
47
  // Emitted by importmap-plugin.ts. keyhive_wasm.wasm is loaded lazily (only
package/dist/types.d.ts CHANGED
@@ -36,8 +36,6 @@ export interface PatchworkOptions {
36
36
  * The module URLs *inside* either kind of source may themselves be Automerge
37
37
  * folder docs or plain HTTP(S) bundles, so deployment targets can be freely
38
38
  * mixed.
39
- *
40
- * Overridable at runtime with `localStorage.systemPackageListURL`.
41
39
  */
42
40
  packageListURL?: string | string[];
43
41
  /**
@@ -57,11 +55,6 @@ export interface PatchworkOptions {
57
55
  name?: string;
58
56
  /** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
59
57
  rootElementId?: string;
60
- /**
61
- * Initialize keyhive for access control. The Repo then uses keyhive's network
62
- * adapter, peerId and idFactory instead of a sharePolicy.
63
- */
64
- keyhive?: boolean;
65
58
  /**
66
59
  * Bring your own Repo. When provided, setup skips wasm initialization and
67
60
  * repo creation entirely — you are responsible for having initialized
@@ -2,7 +2,7 @@ import type { Plugin } from "vite";
2
2
  import wasm from "vite-plugin-wasm";
3
3
  import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
4
4
  /**
5
- * Owns envPrefix, define (__SITE_NAME__/__KEYHIVE__/__KEYHIVE_SYNC_SERVER__),
5
+ * Owns envPrefix, define (__SITE_NAME__/sync-server configuration),
6
6
  * server/preview CORS defaults, worker format + the wasm plugin, and build
7
7
  * defaults (firefox150 target, unminified, sourcemapped) — everything a site
8
8
  * used to hand-write in its own vite.config.ts. Each is switched off
@@ -1,7 +1,8 @@
1
1
  import wasm from "vite-plugin-wasm";
2
+ import { DEFAULT_SYNC_SERVERS, resolvePrimarySyncServer, } from "../site-kit/sync-servers.js";
2
3
  const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
3
4
  /**
4
- * Owns envPrefix, define (__SITE_NAME__/__KEYHIVE__/__KEYHIVE_SYNC_SERVER__),
5
+ * Owns envPrefix, define (__SITE_NAME__/sync-server configuration),
5
6
  * server/preview CORS defaults, worker format + the wasm plugin, and build
6
7
  * defaults (firefox150 target, unminified, sourcemapped) — everything a site
7
8
  * used to hand-write in its own vite.config.ts. Each is switched off
@@ -11,9 +12,13 @@ export function configPlugin(options = {}) {
11
12
  return {
12
13
  name: "@patchwork/config",
13
14
  config() {
15
+ const primarySyncServer = resolvePrimarySyncServer(options);
16
+ const classicSyncServer = options.syncServers && typeof options.syncServers.classic === "string"
17
+ ? options.syncServers.classic
18
+ : DEFAULT_SYNC_SERVERS.classic;
14
19
  const define = {
15
- __KEYHIVE__: JSON.stringify(options.keyhive ?? false),
16
- __KEYHIVE_SYNC_SERVER__: JSON.stringify(options.keyhiveSyncServer ?? false),
20
+ __SYNC_SERVER__: JSON.stringify(primarySyncServer),
21
+ __CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
17
22
  };
18
23
  if (options.siteName) {
19
24
  define.__SITE_NAME__ = JSON.stringify(options.siteName);
@@ -31,7 +31,7 @@ export type ImportMap = {
31
31
  [scope: string]: Imports;
32
32
  };
33
33
  };
34
- export type { PatchworkSiteOptions, PatchworkIconsOptions, PatchworkHtmlOptions, PatchworkNetlifyOptions, PatchworkSyncServersOptions, } from "../site-kit/options.js";
34
+ export type { PatchworkSiteOptions, PatchworkIconsOptions, PatchworkHtmlOptions, PatchworkNetlifyOptions, PatchworkKeyhiveSyncServer, PatchworkSyncServersOptions, } from "../site-kit/options.js";
35
35
  export interface PatchworkVitePluginOptions extends PatchworkSiteOptions {
36
36
  importmap?: ImportMap;
37
37
  server?: false | ServerOptions;
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@inkandswitch/patchwork",
3
- "version": "0.0.1",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git+https://github.com/inkandswitch/patchwork-system.git",
6
+ "directory": "core/patchwork"
7
+ },
8
+ "version": "0.2.1",
4
9
  "author": "Ink & Switch",
5
10
  "type": "module",
6
11
  "license": "MIT",
@@ -37,11 +42,11 @@
37
42
  "debug": "^4.4.3",
38
43
  "sharp": "^0.35.3",
39
44
  "vite-plugin-wasm": "^3.6.0",
40
- "@inkandswitch/patchwork-bootloader": "^0.4.4",
41
- "@inkandswitch/patchwork-filesystem": "^0.2.2",
45
+ "@inkandswitch/patchwork-bootloader": "^0.5.2",
46
+ "@inkandswitch/patchwork-filesystem": "^0.2.5",
47
+ "@inkandswitch/patchwork-elements": "^4.0.4",
42
48
  "@inkandswitch/patchwork-providers": "^0.4.2",
43
- "@inkandswitch/patchwork-elements": "^4.0.1",
44
- "@inkandswitch/patchwork-plugins": "^1.0.1"
49
+ "@inkandswitch/patchwork-plugins": "^1.0.3"
45
50
  },
46
51
  "devDependencies": {
47
52
  "esbuild": "^0.23.1",
package/src/client.d.ts CHANGED
@@ -13,14 +13,12 @@ import "@inkandswitch/patchwork-providers";
13
13
 
14
14
  declare global {
15
15
  const __SITE_NAME__: string;
16
- const __KEYHIVE__: boolean;
17
- const __KEYHIVE_SYNC_SERVER__: boolean;
18
16
 
19
17
  interface ImportMetaEnv {
20
18
  /**
21
19
  * Comma-separated list of default tool-manifest sources the shell boots
22
20
  * with. Each entry is an `automerge:` URL or a static `modules.json`
23
- * URL. Overridable at runtime via `localStorage.systemPackageListURL`.
21
+ * URL.
24
22
  */
25
23
  readonly PATCHWORK_SYSTEM_PACKAGE_LIST_URL?: string;
26
24
  /**
package/src/index.ts CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  type DocHandle,
23
23
  MessageChannelNetworkAdapter,
24
24
  Repo,
25
- isValidAutomergeUrl,
26
25
  } from "@automerge/vanillajs/slim";
27
26
  import * as Automerge from "@automerge/automerge/slim";
28
27
  import * as AutomergeRepo from "@automerge/automerge-repo/slim";
@@ -69,6 +68,7 @@ declare const __SITE_NAME__: string;
69
68
  declare global {
70
69
  interface Window {
71
70
  patchwork: Patchwork;
71
+ accountDocHandle: DocHandle<AccountDoc>;
72
72
  repo: Repo;
73
73
  Automerge: typeof import("@automerge/automerge");
74
74
  AutomergeRepo: typeof import("@automerge/automerge-repo");
@@ -152,7 +152,6 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
152
152
 
153
153
  let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
154
154
  ({ repo, hive, signerIdentity } = await createRepo(
155
- options,
156
155
  siteName,
157
156
  workerAdapter
158
157
  ));
@@ -225,19 +224,9 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
225
224
  hive,
226
225
  })) as DocHandle<AccountDoc>;
227
226
 
227
+ window.accountDocHandle = accountDocHandle;
228
228
  wireModuleSettings(accountDocHandle, moduleWatcher);
229
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
230
  const toolsLoaded = moduleWatcher.doneLoading.then(
242
231
  () =>
243
232
  log(
@@ -249,6 +238,18 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
249
238
  (err: unknown) => console.error("doneLoading rejected:", err)
250
239
  );
251
240
 
241
+ let router: Router | undefined;
242
+ if (routing !== false) {
243
+ rootElement.style.visibility = "hidden";
244
+ await toolsLoaded;
245
+ router = createRouter({
246
+ rootElement,
247
+ repo,
248
+ accountDocHandle,
249
+ siteName,
250
+ });
251
+ }
252
+
252
253
  installReveal(rootElement, router, toolsLoaded);
253
254
 
254
255
  return {
@@ -331,14 +332,9 @@ function installReveal(
331
332
 
332
333
  // ── Module sources ───────────────────────────────────────────────────────
333
334
 
334
- function isValidModuleSource(source: string): boolean {
335
- return isValidAutomergeUrl(source) || /^(https?:\/\/|\.?\/)/.test(source);
336
- }
337
-
338
335
  /**
339
- * The site's default module-list sources, honouring the
340
- * `localStorage.systemPackageListURL` dev override, which replaces the entire
341
- * built-in bundle.
336
+ * The site's module-list sources. The site owns any dev overrides and passes
337
+ * the result in as `packageListURL`.
342
338
  */
343
339
  function resolveDefaultModules(options: PatchworkOptions): string[] {
344
340
  const configured = options.packageListURL ?? [];
@@ -346,18 +342,6 @@ function resolveDefaultModules(options: PatchworkOptions): string[] {
346
342
  Array.isArray(configured) ? configured : [configured]
347
343
  ).filter(Boolean);
348
344
 
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
345
  if (builtin.length === 0) {
362
346
  throw new Error(
363
347
  "patchwork.setup: no default module sources configured (set `packageListURL`)"
package/src/repo.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  initKeyhiveWasm,
11
11
  initializeAutomergeRepoKeyhiveWithRepo,
12
12
  type AutomergeRepoKeyhive,
13
+ type SyncServerSelection,
13
14
  } from "@automerge/automerge-repo-keyhive";
14
15
  // eslint-disable-next-line
15
16
  // @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
@@ -21,11 +22,14 @@ import debug from "debug";
21
22
 
22
23
  const log = debug("patchwork:setup:repo");
23
24
 
24
- // Must match the automerge-worker's selection, or the tab and the SW grant
25
- // relay access to different servers.
26
- declare const __KEYHIVE_SYNC_SERVER__: boolean;
27
- const useKeyhiveSyncServer =
28
- typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
25
+ declare const __SYNC_SERVER__: {
26
+ url: string;
27
+ keyhive?: SyncServerSelection;
28
+ };
29
+ const syncServer =
30
+ typeof __SYNC_SERVER__ !== "undefined"
31
+ ? __SYNC_SERVER__
32
+ : { url: "wss://subduction.sync.inkandswitch.com" };
29
33
 
30
34
  // Fetch and initialize automerge + subduction wasm. Memoized: the fetches start
31
35
  // on the first call and every later caller awaits the same init. Skipped
@@ -46,7 +50,6 @@ export function initWasm(): Promise<void> {
46
50
  }
47
51
 
48
52
  export async function createRepo(
49
- config: { keyhive?: boolean },
50
53
  siteName: string,
51
54
  workerAdapter: MessageChannelNetworkAdapter
52
55
  ): Promise<{
@@ -54,7 +57,7 @@ export async function createRepo(
54
57
  hive?: AutomergeRepoKeyhive;
55
58
  signerIdentity?: SignerIdentity;
56
59
  }> {
57
- if (config.keyhive) {
60
+ if (syncServer.keyhive) {
58
61
  log("setting up keyhive");
59
62
  initKeyhiveWasm();
60
63
  const { hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
@@ -66,7 +69,7 @@ export async function createRepo(
66
69
  cachingMode: "periodic",
67
70
  onlyShareWithHardcodedServerPeerId: false,
68
71
  // ARK selects the relay via `syncServer`, defaulting to "subduction".
69
- ...(useKeyhiveSyncServer ? { syncServer: "keyhive" as const } : {}),
72
+ syncServer: syncServer.keyhive,
70
73
  repo: {
71
74
  storage: new IndexedDBWorkerStorageAdapter(),
72
75
  enableRemoteHeadsGossiping: true,
@@ -12,6 +12,7 @@ export type {
12
12
  PatchworkIconsOptions,
13
13
  PatchworkHtmlOptions,
14
14
  PatchworkNetlifyOptions,
15
+ PatchworkKeyhiveSyncServer,
15
16
  PatchworkSyncServersOptions,
16
17
  } from "./options.js";
17
18
 
@@ -1,3 +1,5 @@
1
+ import type { SyncServerIdentity } from "@automerge/automerge-repo-keyhive";
2
+
1
3
  /**
2
4
  * The bundler-agnostic option shape for a Patchwork site's generated
3
5
  * static assets (icons, index.html, manifest.webmanifest, Netlify config).
@@ -26,14 +28,19 @@ export interface PatchworkNetlifyOptions {
26
28
  immutableAssets?: boolean;
27
29
  }
28
30
 
29
- export interface PatchworkSyncServersOptions {
31
+ type PatchworkPrimarySyncServerOptions =
32
+ | { subduction?: string; keyhive?: never }
33
+ | { subduction?: never; keyhive: PatchworkKeyhiveSyncServer };
34
+
35
+ export type PatchworkKeyhiveSyncServer =
36
+ | "keyhive"
37
+ | "subduction"
38
+ | ({ url: string } & SyncServerIdentity);
39
+
40
+ export type PatchworkSyncServersOptions = {
30
41
  /** wss:// URL for the legacy automerge-repo sync-server channel (connected on demand via connectClassicSync). Default: wss://sync3.automerge.org. Pass false to skip its preconnect hint. */
31
42
  classic?: string | false;
32
- /** wss:// URL for subduction sync — live when keyhiveSyncServer is false. Default: wss://subduction.sync.inkandswitch.com */
33
- subduction?: string;
34
- /** wss:// URL for keyhive sync — live when keyhiveSyncServer is true. Default: wss://keyhive.sync.automerge.org */
35
- keyhive?: string;
36
- }
43
+ } & PatchworkPrimarySyncServerOptions;
37
44
 
38
45
  export interface PatchworkSiteOptions {
39
46
  /** -> __SITE_NAME__ define */
@@ -47,22 +54,17 @@ export interface PatchworkSiteOptions {
47
54
  /** default "/src/main.ts" — must be root-absolute, since the generated index.html doesn't live at the project root */
48
55
  entry?: string;
49
56
 
50
- /** -> __KEYHIVE__ define */
51
- keyhive?: boolean;
52
- /** -> __KEYHIVE_SYNC_SERVER__ define */
53
- keyhiveSyncServer?: boolean;
54
-
55
57
  themeColor?: string | { light: string; dark: string };
56
58
  /** manifest background_color */
57
59
  backgroundColor?: string;
58
60
 
59
61
  /**
60
- * Which sync-server hosts to preconnect/dns-prefetch from the html and
61
- * list in the Netlify Link header. Only the channel actually live for
62
- * this build is included subduction xor keyhive, picked by
63
- * `keyhiveSyncServer`, matching automerge-worker.ts's own selection
64
- * plus `classic` (on-demand, but cheap to hint) unless set to `false`.
65
- * Pass `false` to skip all sync-server hints.
62
+ * Sync-server configuration for this build. Providing `keyhive` enables
63
+ * keyhive and selects the relay identity ARK grants access to. Custom
64
+ * identities also require their WebSocket URL. `subduction` and `keyhive`
65
+ * are mutually exclusive. The live server and `classic` are also emitted as
66
+ * connection hints. Pass `false` to keep the default servers but skip those
67
+ * hints.
66
68
  */
67
69
  syncServers?: false | PatchworkSyncServersOptions;
68
70
 
@@ -1,36 +1,54 @@
1
1
  import type { PatchworkSiteOptions } from "./options.js";
2
+ import type { SyncServerSelection } from "@automerge/automerge-repo-keyhive";
2
3
 
3
4
  // Mirrors core/bootloader/src/sync-config.ts's DEFAULT_CLASSIC_SYNC_SERVER
4
5
  // and automerge-worker.ts's SUBDUCTION_SYNC_URL selection — kept here as
5
6
  // plain constants (rather than importing those runtime modules) since this
6
7
  // only needs the hostnames, not the browser-only logic that reads them.
7
- const DEFAULT_SYNC_SERVERS = {
8
+ export const DEFAULT_SYNC_SERVERS = {
8
9
  classic: "wss://sync3.automerge.org",
9
10
  subduction: "wss://subduction.sync.inkandswitch.com",
10
11
  keyhive: "wss://keyhive.sync.automerge.org",
11
12
  };
12
13
 
14
+ export function resolvePrimarySyncServer(options: PatchworkSiteOptions): {
15
+ url: string;
16
+ keyhive?: SyncServerSelection;
17
+ } {
18
+ const servers = options.syncServers || undefined;
19
+ if (servers?.keyhive) {
20
+ if (typeof servers.keyhive === "string") {
21
+ return {
22
+ keyhive: servers.keyhive,
23
+ url: DEFAULT_SYNC_SERVERS[servers.keyhive],
24
+ };
25
+ }
26
+ const { url, ...identity } = servers.keyhive;
27
+ return {
28
+ keyhive: identity,
29
+ url,
30
+ };
31
+ }
32
+ return {
33
+ url: servers?.subduction ?? DEFAULT_SYNC_SERVERS.subduction,
34
+ };
35
+ }
36
+
13
37
  function wsToHttpOrigin(wsUrl: string): string {
14
38
  return wsUrl.replace(/^ws/, "http");
15
39
  }
16
40
 
17
41
  /**
18
42
  * Resolves which sync-server origins are actually live for this build: the
19
- * channel that's live is subduction xor keyhive (picked by
20
- * `keyhiveSyncServer`, matching automerge-worker.ts's own selection) plus
21
- * classic (on-demand, but still worth a preconnect hint) unless disabled.
22
- * A flat list would drift from `keyhiveSyncServer` — e.g. always hinting
23
- * subduction even on a build that actually connects to keyhive.
43
+ * channel that's live is subduction xor keyhive plus classic (on-demand,
44
+ * but still worth a preconnect hint) unless disabled.
24
45
  */
25
46
  export function resolveSyncServers(options: PatchworkSiteOptions): string[] {
26
47
  if (options.syncServers === false) return [];
27
- const servers = { ...DEFAULT_SYNC_SERVERS, ...options.syncServers };
28
- const origins: string[] = [];
29
- const primary = options.keyhiveSyncServer
30
- ? servers.keyhive
31
- : servers.subduction;
32
- origins.push(primary);
33
- if (servers.classic) origins.push(servers.classic);
48
+ const primary = resolvePrimarySyncServer(options);
49
+ const classic = options.syncServers?.classic ?? DEFAULT_SYNC_SERVERS.classic;
50
+ const origins = [primary.url];
51
+ if (classic) origins.push(classic);
34
52
  return origins.map(wsToHttpOrigin);
35
53
  }
36
54
 
package/src/types.ts CHANGED
@@ -53,8 +53,6 @@ export interface PatchworkOptions {
53
53
  * The module URLs *inside* either kind of source may themselves be Automerge
54
54
  * folder docs or plain HTTP(S) bundles, so deployment targets can be freely
55
55
  * mixed.
56
- *
57
- * Overridable at runtime with `localStorage.systemPackageListURL`.
58
56
  */
59
57
  packageListURL?: string | string[];
60
58
 
@@ -78,12 +76,6 @@ export interface PatchworkOptions {
78
76
  /** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
79
77
  rootElementId?: string;
80
78
 
81
- /**
82
- * Initialize keyhive for access control. The Repo then uses keyhive's network
83
- * adapter, peerId and idFactory instead of a sharePolicy.
84
- */
85
- keyhive?: boolean;
86
-
87
79
  /**
88
80
  * Bring your own Repo. When provided, setup skips wasm initialization and
89
81
  * repo creation entirely — you are responsible for having initialized
@@ -1,11 +1,15 @@
1
1
  import type { Plugin } from "vite";
2
2
  import wasm from "vite-plugin-wasm";
3
3
  import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
4
+ import {
5
+ DEFAULT_SYNC_SERVERS,
6
+ resolvePrimarySyncServer,
7
+ } from "../site-kit/sync-servers.js";
4
8
 
5
9
  const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
6
10
 
7
11
  /**
8
- * Owns envPrefix, define (__SITE_NAME__/__KEYHIVE__/__KEYHIVE_SYNC_SERVER__),
12
+ * Owns envPrefix, define (__SITE_NAME__/sync-server configuration),
9
13
  * server/preview CORS defaults, worker format + the wasm plugin, and build
10
14
  * defaults (firefox150 target, unminified, sourcemapped) — everything a site
11
15
  * used to hand-write in its own vite.config.ts. Each is switched off
@@ -17,11 +21,14 @@ export function configPlugin(
17
21
  return {
18
22
  name: "@patchwork/config",
19
23
  config() {
24
+ const primarySyncServer = resolvePrimarySyncServer(options);
25
+ const classicSyncServer =
26
+ options.syncServers && typeof options.syncServers.classic === "string"
27
+ ? options.syncServers.classic
28
+ : DEFAULT_SYNC_SERVERS.classic;
20
29
  const define: Record<string, string> = {
21
- __KEYHIVE__: JSON.stringify(options.keyhive ?? false),
22
- __KEYHIVE_SYNC_SERVER__: JSON.stringify(
23
- options.keyhiveSyncServer ?? false
24
- ),
30
+ __SYNC_SERVER__: JSON.stringify(primarySyncServer),
31
+ __CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
25
32
  };
26
33
  if (options.siteName) {
27
34
  define.__SITE_NAME__ = JSON.stringify(options.siteName);
@@ -54,6 +54,7 @@ export type {
54
54
  PatchworkIconsOptions,
55
55
  PatchworkHtmlOptions,
56
56
  PatchworkNetlifyOptions,
57
+ PatchworkKeyhiveSyncServer,
57
58
  PatchworkSyncServersOptions,
58
59
  } from "../site-kit/options.js";
59
60