@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.
Files changed (70) hide show
  1. package/build-head.js +10 -0
  2. package/dist/client.d.ts +38 -0
  3. package/dist/global.css +1 -0
  4. package/dist/head.d.ts +1 -0
  5. package/dist/head.js +52 -0
  6. package/dist/index.d.ts +37 -0
  7. package/dist/index.js +294 -0
  8. package/dist/loading.d.ts +5 -0
  9. package/dist/loading.js +125 -0
  10. package/dist/repo.d.ts +25 -0
  11. package/dist/repo.js +108 -0
  12. package/dist/router.d.ts +18 -0
  13. package/dist/router.js +132 -0
  14. package/dist/site-kit/html.d.ts +4 -0
  15. package/dist/site-kit/html.js +70 -0
  16. package/dist/site-kit/icons.d.ts +9 -0
  17. package/dist/site-kit/icons.js +36 -0
  18. package/dist/site-kit/index.d.ts +15 -0
  19. package/dist/site-kit/index.js +5 -0
  20. package/dist/site-kit/manifest.d.ts +3 -0
  21. package/dist/site-kit/manifest.js +29 -0
  22. package/dist/site-kit/netlify.d.ts +5 -0
  23. package/dist/site-kit/netlify.js +27 -0
  24. package/dist/site-kit/options.d.ts +67 -0
  25. package/dist/site-kit/options.js +9 -0
  26. package/dist/site-kit/sync-servers.d.ts +11 -0
  27. package/dist/site-kit/sync-servers.js +36 -0
  28. package/dist/types.d.ts +104 -0
  29. package/dist/types.js +1 -0
  30. package/dist/vite/config-plugin.d.ts +12 -0
  31. package/dist/vite/config-plugin.js +70 -0
  32. package/dist/vite/html-plugin.d.ts +11 -0
  33. package/dist/vite/html-plugin.js +51 -0
  34. package/dist/vite/icons.d.ts +4 -0
  35. package/dist/vite/icons.js +42 -0
  36. package/dist/vite/importmap-plugin.d.ts +4 -0
  37. package/dist/vite/importmap-plugin.js +83 -0
  38. package/dist/vite/manifest-plugin.d.ts +4 -0
  39. package/dist/vite/manifest-plugin.js +34 -0
  40. package/dist/vite/netlify-plugin.d.ts +9 -0
  41. package/dist/vite/netlify-plugin.js +29 -0
  42. package/dist/vite/patchwork-plugin.d.ts +43 -0
  43. package/dist/vite/patchwork-plugin.js +40 -0
  44. package/dist/vite/service-worker-plugin.d.ts +2 -0
  45. package/dist/vite/service-worker-plugin.js +49 -0
  46. package/package.json +55 -0
  47. package/src/client.d.ts +38 -0
  48. package/src/global.css +1 -0
  49. package/src/head.ts +14 -0
  50. package/src/index.ts +469 -0
  51. package/src/loading.ts +137 -0
  52. package/src/repo.ts +146 -0
  53. package/src/router.ts +195 -0
  54. package/src/site-kit/html.ts +100 -0
  55. package/src/site-kit/icons.ts +49 -0
  56. package/src/site-kit/index.ts +22 -0
  57. package/src/site-kit/manifest.ts +39 -0
  58. package/src/site-kit/netlify.ts +35 -0
  59. package/src/site-kit/options.ts +73 -0
  60. package/src/site-kit/sync-servers.ts +39 -0
  61. package/src/types.ts +135 -0
  62. package/src/vite/config-plugin.ts +88 -0
  63. package/src/vite/html-plugin.ts +60 -0
  64. package/src/vite/icons.ts +47 -0
  65. package/src/vite/importmap-plugin.ts +112 -0
  66. package/src/vite/manifest-plugin.ts +39 -0
  67. package/src/vite/netlify-plugin.ts +34 -0
  68. package/src/vite/patchwork-plugin.ts +67 -0
  69. package/src/vite/service-worker-plugin.ts +54 -0
  70. package/tsconfig.json +14 -0
@@ -0,0 +1,35 @@
1
+ import type { PatchworkSiteOptions } from "./options.js";
2
+ import { resolveSyncServers, PRELOAD_WASM_ASSETS } from "./sync-servers.js";
3
+
4
+ function buildLinkHeader(syncServers: string[]): string {
5
+ const parts = PRELOAD_WASM_ASSETS.map(
6
+ (asset) => `</${asset}>; rel=preload; as=fetch; crossorigin`
7
+ );
8
+ for (const server of syncServers) {
9
+ parts.push(`<${server}>; rel=preconnect`);
10
+ }
11
+ for (const server of syncServers) {
12
+ parts.push(`<${server}>; rel=dns-prefetch`);
13
+ }
14
+ return parts.join(", ");
15
+ }
16
+
17
+ /** Builds the generated Netlify _headers file content — no bundler involved. */
18
+ export function buildHeaders(options: PatchworkSiteOptions): string {
19
+ const syncServers = resolveSyncServers(options);
20
+ const immutableAssets =
21
+ options.netlify === false || options.netlify?.immutableAssets === false
22
+ ? null
23
+ : "/assets/*\n Cache-Control: public, max-age=31536000, immutable\n";
24
+
25
+ return [
26
+ "/*\n Access-Control-Allow-Origin: *",
27
+ immutableAssets,
28
+ `/\n Link: ${buildLinkHeader(syncServers)}`,
29
+ ]
30
+ .filter(Boolean)
31
+ .join("\n");
32
+ }
33
+
34
+ /** The generated Netlify _redirects file content — always the same SPA fallback. */
35
+ export const REDIRECTS = "/* /index.html 200\n";
@@ -0,0 +1,73 @@
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
+
10
+ export interface PatchworkIconsOptions {
11
+ /** Path (relative to the site root) to a source svg or raster image. Every icon size is rendered from it via sharp. */
12
+ source: string;
13
+ /** Path to a monochrome svg for Safari's pinned-tab mask icon. Omitted entirely if not set. */
14
+ maskIcon?: string;
15
+ maskIconColor?: string;
16
+ }
17
+
18
+ export interface PatchworkHtmlOptions {
19
+ lang?: string;
20
+ /** Raw HTML appended just before </html> — e.g. extra <link>/<meta> tags. */
21
+ extraHead?: string;
22
+ }
23
+
24
+ export interface PatchworkNetlifyOptions {
25
+ /** Cache-Control: immutable on /assets/*. Default true. */
26
+ immutableAssets?: boolean;
27
+ }
28
+
29
+ export interface PatchworkSyncServersOptions {
30
+ /** 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
+ 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
+ }
37
+
38
+ export interface PatchworkSiteOptions {
39
+ /** -> __SITE_NAME__ define */
40
+ siteName?: string;
41
+ /** <title>, apple-mobile-web-app-title, manifest name */
42
+ title?: string;
43
+ /** manifest short_name (defaults to title) */
44
+ shortName?: string;
45
+ /** manifest description, <meta name=description> */
46
+ description?: string;
47
+ /** default "/src/main.ts" — must be root-absolute, since the generated index.html doesn't live at the project root */
48
+ entry?: string;
49
+
50
+ /** -> __KEYHIVE__ define */
51
+ keyhive?: boolean;
52
+ /** -> __KEYHIVE_SYNC_SERVER__ define */
53
+ keyhiveSyncServer?: boolean;
54
+
55
+ themeColor?: string | { light: string; dark: string };
56
+ /** manifest background_color */
57
+ backgroundColor?: string;
58
+
59
+ /**
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.
66
+ */
67
+ syncServers?: false | PatchworkSyncServersOptions;
68
+
69
+ icons?: false | PatchworkIconsOptions;
70
+ html?: false | PatchworkHtmlOptions;
71
+ manifest?: false | Record<string, unknown>;
72
+ netlify?: false | PatchworkNetlifyOptions;
73
+ }
@@ -0,0 +1,39 @@
1
+ import type { PatchworkSiteOptions } from "./options.js";
2
+
3
+ // Mirrors core/bootloader/src/sync-config.ts's DEFAULT_CLASSIC_SYNC_SERVER
4
+ // and automerge-worker.ts's SUBDUCTION_SYNC_URL selection — kept here as
5
+ // plain constants (rather than importing those runtime modules) since this
6
+ // only needs the hostnames, not the browser-only logic that reads them.
7
+ const DEFAULT_SYNC_SERVERS = {
8
+ classic: "wss://sync3.automerge.org",
9
+ subduction: "wss://subduction.sync.inkandswitch.com",
10
+ keyhive: "wss://keyhive.sync.automerge.org",
11
+ };
12
+
13
+ function wsToHttpOrigin(wsUrl: string): string {
14
+ return wsUrl.replace(/^ws/, "http");
15
+ }
16
+
17
+ /**
18
+ * 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.
24
+ */
25
+ export function resolveSyncServers(options: PatchworkSiteOptions): string[] {
26
+ 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);
34
+ return origins.map(wsToHttpOrigin);
35
+ }
36
+
37
+ // Emitted by importmap-plugin.ts. keyhive_wasm.wasm is loaded lazily (only
38
+ // when keyhive is actually enabled), so it isn't worth an eager preload.
39
+ export const PRELOAD_WASM_ASSETS = ["automerge.wasm", "subduction.wasm"];
package/src/types.ts ADDED
@@ -0,0 +1,135 @@
1
+ import type {
2
+ AutomergeUrl,
3
+ DocHandle,
4
+ Repo,
5
+ } from "@automerge/vanillajs/slim";
6
+ import type { AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
7
+ import type {
8
+ ModuleWatcher,
9
+ HasPatchworkMetadata,
10
+ } from "@inkandswitch/patchwork-filesystem";
11
+ import type { AccountDoc } from "@inkandswitch/patchwork-plugins";
12
+ import type {
13
+ ServiceWorkerRepoChannelListener,
14
+ SyncStateDocMessage,
15
+ } from "@inkandswitch/patchwork-bootloader/types";
16
+ import type * as pluginsNS from "@inkandswitch/patchwork-plugins";
17
+
18
+ export type PluginsApi = typeof pluginsNS;
19
+
20
+ export type SignerIdentity = { peerId: string; verifyingKey: string };
21
+
22
+ export interface ServiceWorkerApi {
23
+ connectClassicSync: (server?: string) => Promise<void>;
24
+ subscribeToRepoChannel: (
25
+ listener: ServiceWorkerRepoChannelListener
26
+ ) => Promise<() => void>;
27
+ subscribeSyncState: (
28
+ documentId: string,
29
+ listener: (update: SyncStateDocMessage) => void
30
+ ) => () => void;
31
+ }
32
+
33
+ export interface OpenOptions {
34
+ tool?: string;
35
+ type?: string;
36
+ title?: string;
37
+ }
38
+
39
+ export interface PatchworkOptions {
40
+ /**
41
+ * The site's default tool bundle — the tools every user of this site gets
42
+ * out of the box. Must collectively contribute at least a
43
+ * `patchwork:datatype` registration for `"account"` (typically the one
44
+ * supplied by `@inkandswitch/patchwork-frame`).
45
+ *
46
+ * Each entry is a *module-list source* and may be either:
47
+ * - an Automerge module-settings doc URL (`automerge:...`), which is
48
+ * live-reloaded, or
49
+ * - an HTTP(S) URL (absolute or site-relative, e.g. `/modules.json`) to a
50
+ * static JSON manifest of the shape `{ modules: string[], branches? }`,
51
+ * fetched once at boot.
52
+ *
53
+ * The module URLs *inside* either kind of source may themselves be Automerge
54
+ * folder docs or plain HTTP(S) bundles, so deployment targets can be freely
55
+ * mixed.
56
+ *
57
+ * Overridable at runtime with `localStorage.systemPackageListURL`.
58
+ */
59
+ packageListURL?: string | string[];
60
+
61
+ /**
62
+ * `localStorage` key under which this site remembers which account document
63
+ * belongs to the current user. Defaults to `"patchworkAccountURL"`. Sites
64
+ * sharing an origin MUST use distinct keys so they do not clobber each
65
+ * other's accounts.
66
+ */
67
+ accountKey?: string;
68
+
69
+ /**
70
+ * Brand word for this site: appended to the document title as
71
+ * `"<doc> | <name>"` when a document is open (the separator is provided
72
+ * for you), and used to namespace this site's storage and peer ids.
73
+ *
74
+ * Defaults to the build-time `__SITE_NAME__` define, then `"patchwork"`.
75
+ */
76
+ name?: string;
77
+
78
+ /** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
79
+ rootElementId?: string;
80
+
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
+ /**
88
+ * Bring your own Repo. When provided, setup skips wasm initialization and
89
+ * repo creation entirely — you are responsible for having initialized
90
+ * automerge/subduction and wired the automerge-worker port yourself.
91
+ */
92
+ repo?: Repo;
93
+
94
+ /** The keyhive instance accompanying a provided {@link PatchworkOptions.repo}. */
95
+ hive?: AutomergeRepoKeyhive;
96
+
97
+ /**
98
+ * How document navigation maps onto the URL. `"hash"` (the default) drives
99
+ * the root view from `location.hash`. `false` installs no router — call
100
+ * `patchwork.open()` directly.
101
+ */
102
+ routing?: "hash" | false;
103
+
104
+ /**
105
+ * Milliseconds before the promise `setup()` returned rejects because boot
106
+ * hasn't finished — so a hang (service worker stuck, sync unreachable)
107
+ * surfaces as an error a site can show instead of blank space. The boot
108
+ * itself is not cancelled and may still complete in the background.
109
+ * Defaults to 30 seconds; `false` disables the deadline.
110
+ */
111
+ timeout?: number | false;
112
+ }
113
+
114
+ /**
115
+ * The site runtime `setup()` resolves with — what a site assigns to
116
+ * `window.patchwork`. Plain fields: it exists only once setup has finished,
117
+ * so every member is always usable.
118
+ */
119
+ export interface Patchwork {
120
+ readonly repo: Repo;
121
+ readonly hive?: AutomergeRepoKeyhive;
122
+ readonly account: DocHandle<AccountDoc>;
123
+ readonly signer?: SignerIdentity;
124
+
125
+ create<D>(
126
+ type: string,
127
+ init?: (doc: D) => void
128
+ ): Promise<DocHandle<D & HasPatchworkMetadata>>;
129
+ open(url: AutomergeUrl, options?: OpenOptions): void;
130
+ find<D>(url: AutomergeUrl): Promise<DocHandle<D>>;
131
+
132
+ readonly packages: ModuleWatcher;
133
+ readonly plugins: PluginsApi;
134
+ readonly sw: ServiceWorkerApi;
135
+ }
@@ -0,0 +1,88 @@
1
+ import type { Plugin } from "vite";
2
+ import wasm from "vite-plugin-wasm";
3
+ import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
4
+
5
+ const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
6
+
7
+ /**
8
+ * Owns envPrefix, define (__SITE_NAME__/__KEYHIVE__/__KEYHIVE_SYNC_SERVER__),
9
+ * server/preview CORS defaults, worker format + the wasm plugin, and build
10
+ * defaults (firefox150 target, unminified, sourcemapped) — everything a site
11
+ * used to hand-write in its own vite.config.ts. Each is switched off
12
+ * individually via the matching `false` option.
13
+ */
14
+ export function configPlugin(
15
+ options: PatchworkVitePluginOptions = {}
16
+ ): Plugin {
17
+ return {
18
+ name: "@patchwork/config",
19
+ config() {
20
+ const define: Record<string, string> = {
21
+ __KEYHIVE__: JSON.stringify(options.keyhive ?? false),
22
+ __KEYHIVE_SYNC_SERVER__: JSON.stringify(
23
+ options.keyhiveSyncServer ?? false
24
+ ),
25
+ };
26
+ if (options.siteName) {
27
+ define.__SITE_NAME__ = JSON.stringify(options.siteName);
28
+ }
29
+
30
+ return {
31
+ envPrefix: ["VITE_", "PATCHWORK_"],
32
+ define,
33
+ server:
34
+ options.server === false
35
+ ? undefined
36
+ : {
37
+ headers: CORS_HEADERS,
38
+ ...options.server,
39
+ },
40
+ preview:
41
+ options.preview === false
42
+ ? undefined
43
+ : {
44
+ port: process.env.PORT ? +process.env.PORT : 5173,
45
+ headers: CORS_HEADERS,
46
+ ...options.preview,
47
+ },
48
+ worker:
49
+ options.worker === false
50
+ ? undefined
51
+ : {
52
+ format: options.worker?.format ?? "es",
53
+ plugins: () => [wasm()],
54
+ },
55
+ build: {
56
+ target: "firefox150",
57
+ minify: false,
58
+ sourcemap: true,
59
+ ...options.build,
60
+ },
61
+ };
62
+ },
63
+ // The shared automerge-worker's chunk imports bypass the page's service
64
+ // worker, so offline boot needs the browser's HTTP cache to serve them
65
+ // without revalidating. Content hashes make that safe; a new build gets
66
+ // new URLs. Production gets this from the generated _headers file — vite
67
+ // preview doesn't read that, so mirror it here.
68
+ configurePreviewServer(server) {
69
+ if (
70
+ options.netlify === false ||
71
+ options.netlify?.immutableAssets === false
72
+ ) {
73
+ return;
74
+ }
75
+ server.middlewares.use((req, res, next) => {
76
+ if (req.url?.startsWith("/assets/")) {
77
+ res.setHeader(
78
+ "Cache-Control",
79
+ "public, max-age=31536000, immutable"
80
+ );
81
+ }
82
+ next();
83
+ });
84
+ },
85
+ };
86
+ }
87
+
88
+ export { wasm };
@@ -0,0 +1,60 @@
1
+ import type { Plugin } from "vite";
2
+ import { resolve } from "node:path";
3
+ import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
4
+ import { buildHtml } from "../site-kit/html.js";
5
+
6
+ const GENERATED_PATH = "index.html";
7
+
8
+ /**
9
+ * Vite's html build step (`vite:build-html`) is a normal `transform` hook
10
+ * filtered on `id: /\.html$/` — it doesn't require a real file, just an id
11
+ * that *looks like* one under root (that's all `path.relative(root, id)`,
12
+ * which decides the output filename, cares about). So this is a plain
13
+ * virtual module: `resolveId` claims the one id we care about, `load`
14
+ * returns the generated string — nothing ever touches disk.
15
+ */
16
+ export function htmlPlugin(
17
+ options: PatchworkVitePluginOptions = {}
18
+ ): Plugin | null {
19
+ if (options.html === false) return null;
20
+
21
+ const html = buildHtml(options);
22
+ let entryId: string;
23
+
24
+ return {
25
+ name: "@patchwork/html",
26
+ enforce: "pre",
27
+ config(config) {
28
+ const root = resolve(config.root ?? process.cwd());
29
+ entryId = resolve(root, GENERATED_PATH);
30
+ return {
31
+ build: { rollupOptions: { input: entryId } },
32
+ };
33
+ },
34
+ resolveId(source) {
35
+ if (source === entryId) return entryId;
36
+ },
37
+ load(id) {
38
+ if (id === entryId) return html;
39
+ },
40
+ configureServer: {
41
+ order: "pre",
42
+ handler(server) {
43
+ server.middlewares.use(async (req, res, next) => {
44
+ if (req.url !== "/" && req.url !== "/index.html") {
45
+ next();
46
+ return;
47
+ }
48
+ const transformed = await server.transformIndexHtml(
49
+ req.url,
50
+ html,
51
+ req.originalUrl
52
+ );
53
+ res.statusCode = 200;
54
+ res.setHeader("Content-Type", "text/html");
55
+ res.end(transformed);
56
+ });
57
+ },
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,47 @@
1
+ import type { Plugin } from "vite";
2
+ import { resolve } from "node:path";
3
+ import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
4
+ import { getIcons } from "../site-kit/icons.js";
5
+
6
+ /** Emits the rendered icon PNGs for build, and serves the same buffers for dev. */
7
+ export function iconsPlugin(
8
+ options: PatchworkVitePluginOptions = {}
9
+ ): Plugin | null {
10
+ if (!options.icons) return null;
11
+ const { source } = options.icons;
12
+
13
+ let sourcePath: string;
14
+ let isBuild = false;
15
+
16
+ return {
17
+ name: "@patchwork/icons",
18
+ configResolved(config) {
19
+ isBuild = config.command === "build";
20
+ sourcePath = resolve(config.root, source);
21
+ },
22
+ async buildStart() {
23
+ if (!isBuild) return;
24
+ const icons = await getIcons(sourcePath);
25
+ for (const [fileName, buffer] of icons) {
26
+ this.emitFile({ type: "asset", fileName, source: buffer });
27
+ }
28
+ },
29
+ configureServer(server) {
30
+ server.middlewares.use(async (req, res, next) => {
31
+ const fileName = req.url?.replace(/^\//, "");
32
+ if (!fileName) {
33
+ next();
34
+ return;
35
+ }
36
+ const icons = await getIcons(sourcePath);
37
+ const buffer = icons.get(fileName);
38
+ if (!buffer) {
39
+ next();
40
+ return;
41
+ }
42
+ res.setHeader("Content-Type", "image/png");
43
+ res.end(buffer);
44
+ });
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,112 @@
1
+ import type { Plugin } from "vite";
2
+ import type {
3
+ ImportMap,
4
+ PatchworkVitePluginOptions,
5
+ } from "./patchwork-plugin.js";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ /**
9
+ * bootloader owns resolving/emitting its own dependencies (it's the one that
10
+ * actually has automerge/keyhive/codemirror/solid/the other patchwork-*
11
+ * packages as real dependencies) — reused here rather than duplicated.
12
+ */
13
+ import externals, {
14
+ resolveExternal,
15
+ emitWasmAssets,
16
+ } from "@inkandswitch/patchwork-bootloader/externals";
17
+
18
+ export const builtins = externals.reduce(
19
+ (builtins, name) => ((builtins[name] = `/packages/${name}.js`), builtins),
20
+ {} as Record<string, string>
21
+ );
22
+
23
+ // This package importmaps itself too, so tool code loaded at runtime can
24
+ // bare-import it just like the other @inkandswitch/patchwork-* packages.
25
+ builtins["@inkandswitch/patchwork"] = "/packages/@inkandswitch/patchwork.js";
26
+
27
+ /**
28
+ * Node resolves a package's own name from within its own source when its
29
+ * package.json has "exports" (self-reference) — the same mechanism
30
+ * @inkandswitch/patchwork-bootloader already relies on to list itself among
31
+ * its own externals. Anchoring at *this* file (which lives inside
32
+ * @inkandswitch/patchwork's own installed directory) makes that
33
+ * self-reference resolve to this package instead of bootloader's.
34
+ */
35
+ const self = fileURLToPath(import.meta.url);
36
+ async function resolveSelf(
37
+ this: import("rollup").PluginContext,
38
+ name: string
39
+ ): Promise<string> {
40
+ const resolved = await this.resolve(name, self, { skipSelf: true });
41
+ if (!resolved) {
42
+ throw new Error(
43
+ `@inkandswitch/patchwork: couldn't resolve "${name}" from its own package.`
44
+ );
45
+ }
46
+ return resolved.id;
47
+ }
48
+
49
+ function resolveBuiltin(
50
+ context: import("rollup").PluginContext,
51
+ id: string
52
+ ): Promise<string> {
53
+ return id === "@inkandswitch/patchwork"
54
+ ? resolveSelf.call(context, id)
55
+ : resolveExternal.call(context, id);
56
+ }
57
+
58
+ function createImportMap(options?: PatchworkVitePluginOptions) {
59
+ const importmap: ImportMap = structuredClone(
60
+ options?.importmap ?? { imports: {}, scopes: {} }
61
+ );
62
+ importmap.imports ??= {};
63
+ importmap.scopes ??= {};
64
+ Object.assign(importmap.imports, builtins);
65
+ return { importmap, builtins };
66
+ }
67
+
68
+ export function importmap(options?: PatchworkVitePluginOptions): Plugin {
69
+ const { importmap, builtins } = createImportMap(options);
70
+ return {
71
+ name: "@patchwork/vite",
72
+ async buildStart() {
73
+ for (const [id, fileName] of Object.entries(builtins)) {
74
+ this.emitFile({
75
+ type: "chunk",
76
+ fileName: fileName.slice(1),
77
+ id: await resolveBuiltin(this, id),
78
+ preserveSignature: "strict",
79
+ });
80
+ }
81
+
82
+ // Emitted so the service worker can fetch them
83
+ emitWasmAssets.call(this);
84
+ },
85
+ async resolveId(id) {
86
+ if (id in builtins) {
87
+ // point the site's own imports at the same copy we emit as a chunk,
88
+ // otherwise rollup bundles a second one out of the site's node_modules
89
+ // and you end up with two automerges racing to init the same wasm
90
+ return resolveBuiltin(this, id);
91
+ }
92
+ if (id in importmap.imports) {
93
+ return { id: importmap.imports[id], external: true };
94
+ }
95
+ },
96
+ transformIndexHtml: {
97
+ order: "pre",
98
+ handler(html) {
99
+ return {
100
+ html,
101
+ tags: [
102
+ {
103
+ tag: "script",
104
+ attrs: { type: "importmap" },
105
+ children: JSON.stringify(importmap, null, 2),
106
+ },
107
+ ],
108
+ };
109
+ },
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,39 @@
1
+ import type { Plugin } from "vite";
2
+ import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
3
+ import { buildManifest } from "../site-kit/manifest.js";
4
+
5
+ /** Emits manifest.webmanifest for build and serves it for dev, from the same generated object. */
6
+ export function manifestPlugin(
7
+ options: PatchworkVitePluginOptions = {}
8
+ ): Plugin | null {
9
+ if (options.manifest === false) return null;
10
+
11
+ const manifest = buildManifest(options);
12
+ const source = JSON.stringify(manifest, null, 2);
13
+ let isBuild = false;
14
+
15
+ return {
16
+ name: "@patchwork/manifest",
17
+ configResolved(config) {
18
+ isBuild = config.command === "build";
19
+ },
20
+ buildStart() {
21
+ if (!isBuild) return;
22
+ this.emitFile({
23
+ type: "asset",
24
+ fileName: "manifest.webmanifest",
25
+ source,
26
+ });
27
+ },
28
+ configureServer(server) {
29
+ server.middlewares.use((req, res, next) => {
30
+ if (req.url !== "/manifest.webmanifest") {
31
+ next();
32
+ return;
33
+ }
34
+ res.setHeader("Content-Type", "application/manifest+json");
35
+ res.end(source);
36
+ });
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,34 @@
1
+ import type { Plugin } from "vite";
2
+ import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
3
+ import { buildHeaders, REDIRECTS } from "../site-kit/netlify.js";
4
+
5
+ /**
6
+ * Netlify's _headers/_redirects are only meaningful on an actual Netlify
7
+ * deploy, so this only runs for the production build — no dev middleware.
8
+ * The Link header shares its sync-server/wasm-asset lists with html-plugin
9
+ * so the two never drift out of sync with each other.
10
+ */
11
+ export function netlifyPlugin(
12
+ options: PatchworkVitePluginOptions = {}
13
+ ): Plugin | null {
14
+ if (options.netlify === false) return null;
15
+
16
+ const headers = buildHeaders(options);
17
+ let isBuild = false;
18
+
19
+ return {
20
+ name: "@patchwork/netlify",
21
+ configResolved(config) {
22
+ isBuild = config.command === "build";
23
+ },
24
+ buildStart() {
25
+ if (!isBuild) return;
26
+ this.emitFile({ type: "asset", fileName: "_headers", source: headers });
27
+ this.emitFile({
28
+ type: "asset",
29
+ fileName: "_redirects",
30
+ source: REDIRECTS,
31
+ });
32
+ },
33
+ };
34
+ }