@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
|
@@ -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
|
+
}
|