@inkandswitch/patchwork 0.4.0 → 0.6.0
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 +65 -0
- package/dist/client.d.ts +2 -1
- package/dist/createAccount.js +0 -1
- package/dist/index.js +5 -4
- package/dist/repo.d.ts +1 -1
- package/dist/repo.js +5 -4
- package/dist/router.d.ts +3 -2
- package/dist/router.js +18 -7
- package/dist/site-kit/html.d.ts +1 -1
- package/dist/site-kit/html.js +2 -1
- package/dist/site-kit/manifest.d.ts +1 -1
- package/dist/site-kit/manifest.js +2 -1
- package/dist/site-kit/options.d.ts +16 -3
- package/dist/site-kit/options.js +1 -1
- package/dist/types.d.ts +16 -5
- package/dist/vite/config-plugin.d.ts +8 -1
- package/dist/vite/config-plugin.js +29 -13
- package/dist/vite/dev-plugin.d.ts +3 -0
- package/dist/vite/dev-plugin.js +129 -0
- package/dist/vite/importmap-plugin.d.ts +1 -0
- package/dist/vite/importmap-plugin.js +1 -1
- package/dist/vite/patchwork-plugin.js +2 -0
- package/dist/vite/service-worker-plugin.d.ts +4 -0
- package/dist/vite/service-worker-plugin.js +8 -1
- package/package.json +8 -8
- package/src/client.d.ts +2 -1
- package/src/createAccount.ts +0 -1
- package/src/index.ts +7 -9
- package/src/repo.ts +8 -4
- package/src/router.ts +23 -7
- package/src/site-kit/html.ts +2 -2
- package/src/site-kit/manifest.ts +2 -2
- package/src/site-kit/options.ts +17 -3
- package/src/types.ts +17 -5
- package/src/vite/config-plugin.ts +35 -15
- package/src/vite/dev-plugin.ts +154 -0
- package/src/vite/importmap-plugin.ts +1 -1
- package/src/vite/patchwork-plugin.ts +2 -0
- package/src/vite/service-worker-plugin.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# @inkandswitch/patchwork
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 61a152a: Add a `frameToolId` option to `setup`, and stop seeding a frame tool into new accounts.
|
|
8
|
+
|
|
9
|
+
`createDefaultAccount` wrote `frameToolId: "threepane"` into every account it created — a tool id belonging to one particular tool bundle, hardcoded in core. A site whose `packageListURL` didn't ship `threepane` gave every new user an account pointing at a tool that never registers, so the root view never mounted.
|
|
10
|
+
|
|
11
|
+
New accounts now leave `frameToolId` unset, and the router resolves the frame each boot:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
#frame= → the account's frameToolId → setup({frameToolId}) → first tool tagged frame-tool
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The field is still written to the account when a user picks a frame, so it stays a user preference; it is no longer decided for them at signup. Existing accounts already have it set and are unaffected.
|
|
18
|
+
|
|
19
|
+
Sites relying on the old seeded default should pass `frameToolId: "threepane"` to `setup`.
|
|
20
|
+
|
|
21
|
+
- 61a152a: Make `vite dev` work without a site hand-rolling the dev server.
|
|
22
|
+
|
|
23
|
+
The build emits the service worker, the automerge shared worker, the module-loader worker, and three wasm binaries. None of them existed in serve mode, so every one 404'd and dev only worked if a site served a previous production build's `dist/` behind vite. The plugin now serves all of them itself:
|
|
24
|
+
|
|
25
|
+
- The three worker entries are bundled on demand with esbuild and rebuilt per request, so they always reflect the source on disk. Their heavy imports resolve to the dev server's optimized-dep URLs, mirroring how the build rewrites them to `/packages/...` — import maps don't apply to `type: "module"` workers, so the URLs have to be real either way.
|
|
26
|
+
- `automerge.wasm`, `keyhive_wasm.wasm`, and `subduction.wasm` are served from the bootloader's own node_modules. `@inkandswitch/patchwork-bootloader/externals` gains `wasmAssets()`, which `emitWasmAssets` now uses too.
|
|
27
|
+
- `global.css` 404'd in dev: the generated `index.html` links it by bare specifier, which only the build resolves. Both patchwork's and the bootloader's stylesheets are now served under root-absolute paths, and the link points at them.
|
|
28
|
+
- `@patchwork/service-worker` called `emitFile` from `buildStart` unconditionally, which throws in serve mode — it logged "This plugin is likely not vite-compatible" three times on every dev-server start. It now skips emission when serving.
|
|
29
|
+
- Dep pre-bundling runs esbuild outside the plugin pipeline that applies `define`, so in dev the page fell back to the default storage prefix while the workers used the configured one — the two would have opened different IndexedDB databases. The defines are now passed to the optimizer as well.
|
|
30
|
+
|
|
31
|
+
Sites no longer need to filter `@patchwork/service-worker` out of the plugin list, serve stylesheets themselves, or keep a built `dist/` around for `vite dev`.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 846cfac: Update the pinned `@automerge/*` versions to `2.6.0-subduction.47`. These are exact pins in `dependencies` and `peerDependencies`, so both packages need to ship the new version together — installing a `.46` and a `.47` package side by side loads two copies of automerge-repo, and document handles from one are not recognised by the other.
|
|
36
|
+
- Updated dependencies [846cfac]
|
|
37
|
+
- Updated dependencies [61a152a]
|
|
38
|
+
- @inkandswitch/patchwork-bootloader@0.6.1
|
|
39
|
+
|
|
40
|
+
## 0.5.0
|
|
41
|
+
|
|
42
|
+
### Minor Changes
|
|
43
|
+
|
|
44
|
+
- 98be594: Collapse `siteName`, `title`, and `setup({name})` into a single `title`.
|
|
45
|
+
|
|
46
|
+
A site's name was three options across two files: `siteName` and `title` in the vite config, `name` at `setup`. They fed the same handful of strings and could disagree with each other.
|
|
47
|
+
|
|
48
|
+
`title` is now the only one. It names the html `<title>`, `apple-mobile-web-app-title`, and the manifest's `name`/`short_name` as before, and is also emitted as the `__SITE_TITLE__` define, which supplies the brand word the router appends to the document title as `"<doc> | <title>"`. It defaults to `"Patchwork"`.
|
|
49
|
+
|
|
50
|
+
- `siteName` and the `__SITE_NAME__` define are removed. If you used `siteName` only for display, rename it to `title`; if you relied on it to namespace storage, see `storagePrefix`.
|
|
51
|
+
- `setup({name})` is now `setup({title})`, and is only needed to override the build-time value.
|
|
52
|
+
|
|
53
|
+
- 98be594: Namespace IndexedDB and peer ids with a new build-time `storagePrefix` option.
|
|
54
|
+
|
|
55
|
+
The tab and the shared automerge worker are separate bundles that must open the same databases. Both now read the name from one place, `@inkandswitch/patchwork-bootloader/storage`, resolved from the `__STORAGE_PREFIX__` define the vite plugin emits unconditionally.
|
|
56
|
+
|
|
57
|
+
Previously each side resolved `__SITE_NAME__` itself with a different fallback — `"patchwork.inkandswitch.com"` in the worker, `"patchwork"` in the tab — so a site that never set `siteName` had its tab and worker on two different keyhive databases, and one that passed `setup({name})` split them the same way, since a runtime option never reaches the worker.
|
|
58
|
+
|
|
59
|
+
- `storagePrefix` defaults to `"patchwork"` and is settable only in the build config. Sites sharing an origin must use distinct prefixes. It is deliberately not derived from any display name: changing it points a site at empty storage, so a rebrand must not be able to change it by accident.
|
|
60
|
+
- Sites that relied on `siteName` to namespace their storage must now set `storagePrefix` explicitly to that same value to keep their existing databases.
|
|
61
|
+
- `createRepo` in `@inkandswitch/patchwork` no longer takes a site name argument.
|
|
62
|
+
|
|
63
|
+
### Patch Changes
|
|
64
|
+
|
|
65
|
+
- Updated dependencies [98be594]
|
|
66
|
+
- @inkandswitch/patchwork-bootloader@0.6.0
|
|
67
|
+
|
|
3
68
|
## 0.4.0
|
|
4
69
|
|
|
5
70
|
### Minor Changes
|
package/dist/client.d.ts
CHANGED
package/dist/createAccount.js
CHANGED
|
@@ -20,7 +20,6 @@ export const createDefaultAccount = async (accountHandle, repo) => {
|
|
|
20
20
|
}),
|
|
21
21
|
]);
|
|
22
22
|
accountHandle.change((doc) => {
|
|
23
|
-
doc.frameToolId = "threepane";
|
|
24
23
|
doc.rootFolderUrl = rootFolder.url;
|
|
25
24
|
doc.moduleSettingsUrl = moduleSettings.url;
|
|
26
25
|
doc.contactUrl = contact.url;
|
package/dist/index.js
CHANGED
|
@@ -53,8 +53,8 @@ export function setup(options = {}) {
|
|
|
53
53
|
}
|
|
54
54
|
export default setup;
|
|
55
55
|
async function doSetup(options) {
|
|
56
|
-
const
|
|
57
|
-
(typeof
|
|
56
|
+
const siteTitle = options.title ??
|
|
57
|
+
(typeof __SITE_TITLE__ !== "undefined" ? __SITE_TITLE__ : "Patchwork");
|
|
58
58
|
const moduleSources = resolveDefaultModules(options);
|
|
59
59
|
const routing = options.routing ?? "hash";
|
|
60
60
|
log("booting", options);
|
|
@@ -85,7 +85,7 @@ async function doSetup(options) {
|
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
87
|
let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
|
|
88
|
-
({ repo, hive, signerIdentity } = await createRepo(
|
|
88
|
+
({ repo, hive, signerIdentity } = await createRepo(workerAdapter));
|
|
89
89
|
// The worker was recreated with cold state: wire the repo onto the fresh
|
|
90
90
|
// port and drop the adapter stranded on the dead one.
|
|
91
91
|
const bootHive = hive;
|
|
@@ -154,7 +154,8 @@ async function doSetup(options) {
|
|
|
154
154
|
rootElement,
|
|
155
155
|
repo,
|
|
156
156
|
accountDocHandle,
|
|
157
|
-
|
|
157
|
+
siteTitle,
|
|
158
|
+
frameToolId: options.frameToolId,
|
|
158
159
|
});
|
|
159
160
|
}
|
|
160
161
|
installReveal(rootElement, router, toolsLoaded);
|
package/dist/repo.d.ts
CHANGED
|
@@ -3,7 +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(
|
|
6
|
+
export declare function createRepo(workerAdapter: MessageChannelNetworkAdapter): Promise<{
|
|
7
7
|
repo: Repo;
|
|
8
8
|
hive?: AutomergeRepoKeyhive;
|
|
9
9
|
signerIdentity?: SignerIdentity;
|
package/dist/repo.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initKeyhiveWasm, initializeAutomergeRepoKeyhiveWithRepo, } from "@autom
|
|
|
5
5
|
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
6
6
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
7
7
|
import { MemorySigner } from "@automerge/automerge-subduction/slim";
|
|
8
|
+
import { keyhiveStorageName, storagePrefix, } from "@inkandswitch/patchwork-bootloader/storage";
|
|
8
9
|
import debug from "debug";
|
|
9
10
|
const log = debug("patchwork:setup:repo");
|
|
10
11
|
const syncServer = typeof __SYNC_SERVER__ !== "undefined"
|
|
@@ -27,14 +28,14 @@ export function initWasm() {
|
|
|
27
28
|
}
|
|
28
29
|
return wasmReady;
|
|
29
30
|
}
|
|
30
|
-
export async function createRepo(
|
|
31
|
+
export async function createRepo(workerAdapter) {
|
|
31
32
|
if (syncServer.keyhive) {
|
|
32
33
|
log("setting up keyhive");
|
|
33
34
|
initKeyhiveWasm();
|
|
34
35
|
const { hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
|
|
35
36
|
createRepo: (repoConfig) => new Repo(repoConfig),
|
|
36
|
-
storage: new IndexedDBWorkerStorageAdapter(
|
|
37
|
-
peerIdSuffix:
|
|
37
|
+
storage: new IndexedDBWorkerStorageAdapter(keyhiveStorageName),
|
|
38
|
+
peerIdSuffix: storagePrefix + Math.random().toString(36).slice(2),
|
|
38
39
|
networkAdapter: workerAdapter,
|
|
39
40
|
automaticArchiveIngestion: true,
|
|
40
41
|
cachingMode: "periodic",
|
|
@@ -61,7 +62,7 @@ export async function createRepo(siteName, workerAdapter) {
|
|
|
61
62
|
return peerId.includes("automerge-worker");
|
|
62
63
|
},
|
|
63
64
|
enableRemoteHeadsGossiping: true,
|
|
64
|
-
peerId: `${
|
|
65
|
+
peerId: `${storagePrefix}-tab-${crypto.randomUUID()}`,
|
|
65
66
|
});
|
|
66
67
|
const signerIdentity = {
|
|
67
68
|
peerId: signer.peerId().toString(),
|
package/dist/router.d.ts
CHANGED
|
@@ -9,10 +9,11 @@ export interface RouterParams {
|
|
|
9
9
|
rootElement: HTMLElement;
|
|
10
10
|
repo: Repo;
|
|
11
11
|
accountDocHandle: DocHandle<AccountDoc>;
|
|
12
|
-
|
|
12
|
+
siteTitle: string;
|
|
13
|
+
frameToolId?: string;
|
|
13
14
|
}
|
|
14
15
|
export interface Router {
|
|
15
16
|
/** Apply `location.hash` to the view. */
|
|
16
17
|
route(): Promise<void>;
|
|
17
18
|
}
|
|
18
|
-
export declare function createRouter({ rootElement, repo, accountDocHandle,
|
|
19
|
+
export declare function createRouter({ rootElement, repo, accountDocHandle, siteTitle, frameToolId, }: RouterParams): Router;
|
package/dist/router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isValidAutomergeUrl, isValidDocumentId, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
|
|
1
|
+
import { isValidAutomergeUrl, isValidDocumentId, parseAutomergeUrl, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
|
|
2
2
|
import { openDocument } from "@inkandswitch/patchwork-elements";
|
|
3
3
|
import { getRegistry, } from "@inkandswitch/patchwork-plugins";
|
|
4
4
|
// Legacy big-patchwork hash shape: `<slug>--<documentId>[?…]`. The slug can
|
|
@@ -6,12 +6,12 @@ import { getRegistry, } from "@inkandswitch/patchwork-plugins";
|
|
|
6
6
|
// anchor on the `--` before the base58 document id rather than a strict slug
|
|
7
7
|
// charset.
|
|
8
8
|
const BIG_PATCHWORK_HASH_REGEX = /^(?<title>[^=&?/#]*)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)/;
|
|
9
|
-
// The `doc=`
|
|
9
|
+
// The `doc=` and `draft=` values are automerge URLs, kept literal rather than
|
|
10
10
|
// percent-encoded so links stay readable.
|
|
11
|
-
const RAW_HASH_KEYS = new Set(["doc"]);
|
|
11
|
+
const RAW_HASH_KEYS = new Set(["doc", "draft"]);
|
|
12
12
|
// A stable order means re-serializing the same logical params is
|
|
13
13
|
// byte-identical, avoiding spurious `hashchange` round-trips.
|
|
14
|
-
const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame"];
|
|
14
|
+
const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame", "draft"];
|
|
15
15
|
function serializeHashParams(params) {
|
|
16
16
|
const keys = [...HASH_KEY_ORDER, ...params.keys()];
|
|
17
17
|
const parts = [];
|
|
@@ -47,14 +47,17 @@ function registeredFrameToolId() {
|
|
|
47
47
|
.filter((tool) => !!tool.tags?.includes("frame-tool") && !tool.unlisted)
|
|
48
48
|
.at(0)?.id;
|
|
49
49
|
}
|
|
50
|
-
export function createRouter({ rootElement, repo, accountDocHandle,
|
|
50
|
+
export function createRouter({ rootElement, repo, accountDocHandle, siteTitle, frameToolId, }) {
|
|
51
51
|
const route = async () => {
|
|
52
52
|
// The first call seeds the root view's tool/doc so it can mount; later
|
|
53
53
|
// calls reconcile the mounted view with the hash.
|
|
54
54
|
if (!rootElement.hasAttribute("tool-id")) {
|
|
55
55
|
const params = new URLSearchParams(location.hash.slice(1));
|
|
56
56
|
const frame = params.get("frame");
|
|
57
|
-
const toolId = frame ??
|
|
57
|
+
const toolId = frame ??
|
|
58
|
+
accountDocHandle.doc().frameToolId ??
|
|
59
|
+
frameToolId ??
|
|
60
|
+
registeredFrameToolId();
|
|
58
61
|
if (!toolId) {
|
|
59
62
|
console.error("patchwork: no frame tool registered, nothing to mount");
|
|
60
63
|
return;
|
|
@@ -107,6 +110,14 @@ export function createRouter({ rootElement, repo, accountDocHandle, siteName, })
|
|
|
107
110
|
// `doc` is the full automerge URL, so heads live inside it and the separate
|
|
108
111
|
// `heads=` param is gone.
|
|
109
112
|
params.delete("heads");
|
|
113
|
+
// `draft` is doc-scoped (owned by the drafts plugin, opaque here):
|
|
114
|
+
// navigating to a different document invalidates the selection, while
|
|
115
|
+
// same-doc navigation (tool switches, heads changes) keeps it.
|
|
116
|
+
const prevDoc = docParamToUrl(params.get("doc"));
|
|
117
|
+
if (prevDoc &&
|
|
118
|
+
parseAutomergeUrl(prevDoc).documentId !== parseAutomergeUrl(url).documentId) {
|
|
119
|
+
params.delete("draft");
|
|
120
|
+
}
|
|
110
121
|
params.set("doc", url);
|
|
111
122
|
for (const [key, value] of [
|
|
112
123
|
["tool", toolId],
|
|
@@ -130,7 +141,7 @@ export function createRouter({ rootElement, repo, accountDocHandle, siteName, })
|
|
|
130
141
|
return;
|
|
131
142
|
const docTitle = datatype.module.getTitle(doc);
|
|
132
143
|
if (docTitle)
|
|
133
|
-
document.title = `${docTitle} | ${
|
|
144
|
+
document.title = `${docTitle} | ${siteTitle}`;
|
|
134
145
|
}
|
|
135
146
|
catch (e) {
|
|
136
147
|
console.error("Failed to update document title", e);
|
package/dist/site-kit/html.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PatchworkSiteOptions } from "./options.js";
|
|
2
2
|
export declare function escapeHtml(value: string): string;
|
|
3
3
|
/** Builds the generated index.html as a plain string — no bundler involved. */
|
|
4
4
|
export declare function buildHtml(options: PatchworkSiteOptions): string;
|
package/dist/site-kit/html.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_TITLE } from "./options.js";
|
|
1
2
|
import { resolveSyncServers, PRELOAD_WASM_ASSETS } from "./sync-servers.js";
|
|
2
3
|
import { ICON_SPECS } from "./icons.js";
|
|
3
4
|
const HTML_ESCAPES = {
|
|
@@ -12,7 +13,7 @@ export function escapeHtml(value) {
|
|
|
12
13
|
}
|
|
13
14
|
/** Builds the generated index.html as a plain string — no bundler involved. */
|
|
14
15
|
export function buildHtml(options) {
|
|
15
|
-
const title = options.title ??
|
|
16
|
+
const title = options.title ?? DEFAULT_TITLE;
|
|
16
17
|
const lang = (options.html && options.html.lang) || "en";
|
|
17
18
|
const entry = options.entry ?? "/src/main.ts";
|
|
18
19
|
const syncServers = resolveSyncServers(options);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PatchworkSiteOptions } from "./options.js";
|
|
2
2
|
/** Builds the generated manifest.webmanifest object — no bundler involved. */
|
|
3
3
|
export declare function buildManifest(options: PatchworkSiteOptions): Record<string, unknown>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_TITLE } from "./options.js";
|
|
1
2
|
import { ICON_SPECS } from "./icons.js";
|
|
2
3
|
/** Builds the generated manifest.webmanifest object — no bundler involved. */
|
|
3
4
|
export function buildManifest(options) {
|
|
4
|
-
const title = options.title ??
|
|
5
|
+
const title = options.title ?? DEFAULT_TITLE;
|
|
5
6
|
const icons = !options.icons
|
|
6
7
|
? []
|
|
7
8
|
: ICON_SPECS.filter((spec) => spec.fileName === "apple-touch-icon.png" || spec.manifestPurpose).map((spec) => ({
|
|
@@ -37,10 +37,23 @@ export type PatchworkSyncServersOptions = {
|
|
|
37
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. */
|
|
38
38
|
classic?: string | false;
|
|
39
39
|
} & PatchworkPrimarySyncServerOptions;
|
|
40
|
+
export declare const DEFAULT_TITLE = "Patchwork";
|
|
40
41
|
export interface PatchworkSiteOptions {
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Namespace for this site's IndexedDB databases and peer ids
|
|
44
|
+
* (-> __STORAGE_PREFIX__ define). Defaults to `"patchwork"`. Sites sharing
|
|
45
|
+
* an origin MUST use distinct prefixes.
|
|
46
|
+
*
|
|
47
|
+
* The tab and the shared automerge worker are separate bundles that have to
|
|
48
|
+
* open the same databases, so this is settable only here, where both of them
|
|
49
|
+
* receive it. Changing it on an existing site points it at empty storage.
|
|
50
|
+
*/
|
|
51
|
+
storagePrefix?: string;
|
|
52
|
+
/**
|
|
53
|
+
* This site's name: `<title>`, apple-mobile-web-app-title, manifest name,
|
|
54
|
+
* and — via the __SITE_TITLE__ define — the brand word the router appends to
|
|
55
|
+
* the document title as `"<doc> | <title>"`. Defaults to `"Patchwork"`.
|
|
56
|
+
*/
|
|
44
57
|
title?: string;
|
|
45
58
|
/** manifest short_name (defaults to title) */
|
|
46
59
|
shortName?: string;
|
package/dist/site-kit/options.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const DEFAULT_TITLE = "Patchwork";
|
package/dist/types.d.ts
CHANGED
|
@@ -47,13 +47,24 @@ export interface PatchworkOptions {
|
|
|
47
47
|
accountKey?: string;
|
|
48
48
|
createAccount?: AccountCreator;
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* for you), and used to namespace this site's storage and peer ids.
|
|
50
|
+
* This site's name, appended to the document title as `"<doc> | <title>"`
|
|
51
|
+
* when a document is open (the separator is provided for you).
|
|
53
52
|
*
|
|
54
|
-
* Defaults to the build-time `
|
|
53
|
+
* Defaults to the build-time `__SITE_TITLE__` define — the vite plugin's
|
|
54
|
+
* `title` option, which also names the html `<title>` and the manifest —
|
|
55
|
+
* then `"Patchwork"`.
|
|
55
56
|
*/
|
|
56
|
-
|
|
57
|
+
title?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Id of the tool that frames this site — what mounts in the root view when
|
|
60
|
+
* the user hasn't chosen one on their account and the URL doesn't name one
|
|
61
|
+
* via `#frame=`. Must be a tool one of the {@link
|
|
62
|
+
* PatchworkOptions.packageListURL} sources registers.
|
|
63
|
+
*
|
|
64
|
+
* Falls back to the first registered tool tagged `frame-tool`, which with
|
|
65
|
+
* more than one of them depends on module load order.
|
|
66
|
+
*/
|
|
67
|
+
frameToolId?: string;
|
|
57
68
|
/** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
|
|
58
69
|
rootElementId?: string;
|
|
59
70
|
/**
|
|
@@ -2,7 +2,14 @@ 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
|
-
*
|
|
5
|
+
* The build-time constants both the page bundle and the workers are compiled
|
|
6
|
+
* against. Values are already JSON — the shape vite's `define` and esbuild's
|
|
7
|
+
* `define` both take.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildDefines(options?: PatchworkVitePluginOptions): Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
|
|
12
|
+
* configuration),
|
|
6
13
|
* server/preview CORS defaults, worker format + the wasm plugin, and build
|
|
7
14
|
* defaults (firefox150 target, unminified, sourcemapped) — everything a site
|
|
8
15
|
* used to hand-write in its own vite.config.ts. Each is switched off
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import wasm from "vite-plugin-wasm";
|
|
2
|
+
import { DEFAULT_STORAGE_PREFIX } from "@inkandswitch/patchwork-bootloader/storage";
|
|
3
|
+
import { DEFAULT_TITLE } from "../site-kit/options.js";
|
|
2
4
|
import { DEFAULT_SYNC_SERVERS, resolvePrimarySyncServer, } from "../site-kit/sync-servers.js";
|
|
3
5
|
const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
7
|
+
* The build-time constants both the page bundle and the workers are compiled
|
|
8
|
+
* against. Values are already JSON — the shape vite's `define` and esbuild's
|
|
9
|
+
* `define` both take.
|
|
10
|
+
*/
|
|
11
|
+
export function buildDefines(options = {}) {
|
|
12
|
+
const classicSyncServer = options.syncServers && typeof options.syncServers.classic === "string"
|
|
13
|
+
? options.syncServers.classic
|
|
14
|
+
: DEFAULT_SYNC_SERVERS.classic;
|
|
15
|
+
return {
|
|
16
|
+
__SYNC_SERVER__: JSON.stringify(resolvePrimarySyncServer(options)),
|
|
17
|
+
__CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
|
|
18
|
+
__SITE_TITLE__: JSON.stringify(options.title ?? DEFAULT_TITLE),
|
|
19
|
+
__STORAGE_PREFIX__: JSON.stringify(options.storagePrefix ?? DEFAULT_STORAGE_PREFIX),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
|
|
24
|
+
* configuration),
|
|
6
25
|
* server/preview CORS defaults, worker format + the wasm plugin, and build
|
|
7
26
|
* defaults (firefox150 target, unminified, sourcemapped) — everything a site
|
|
8
27
|
* used to hand-write in its own vite.config.ts. Each is switched off
|
|
@@ -12,20 +31,17 @@ export function configPlugin(options = {}) {
|
|
|
12
31
|
return {
|
|
13
32
|
name: "@patchwork/config",
|
|
14
33
|
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;
|
|
19
|
-
const define = {
|
|
20
|
-
__SYNC_SERVER__: JSON.stringify(primarySyncServer),
|
|
21
|
-
__CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
|
|
22
|
-
};
|
|
23
|
-
if (options.siteName) {
|
|
24
|
-
define.__SITE_NAME__ = JSON.stringify(options.siteName);
|
|
25
|
-
}
|
|
26
34
|
return {
|
|
27
35
|
envPrefix: ["VITE_", "PATCHWORK_"],
|
|
28
|
-
define,
|
|
36
|
+
define: buildDefines(options),
|
|
37
|
+
optimizeDeps: {
|
|
38
|
+
exclude: ["@automerge/automerge-repo-storage-indexeddb"],
|
|
39
|
+
// Dep pre-bundling runs esbuild directly, outside the plugin
|
|
40
|
+
// pipeline that applies `define`. Without this the page would fall
|
|
41
|
+
// back to the built-in storage prefix while the workers used the
|
|
42
|
+
// configured one, and they have to open the same databases.
|
|
43
|
+
esbuildOptions: { define: buildDefines(options) },
|
|
44
|
+
},
|
|
29
45
|
server: options.server === false
|
|
30
46
|
? undefined
|
|
31
47
|
: {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import * as esbuild from "esbuild";
|
|
4
|
+
import { wasmAssets } from "@inkandswitch/patchwork-bootloader/externals";
|
|
5
|
+
import { buildDefines } from "./config-plugin.js";
|
|
6
|
+
import { builtins, devDependencyId } from "./importmap-plugin.js";
|
|
7
|
+
import { workers } from "./service-worker-plugin.js";
|
|
8
|
+
const PATCHWORK_CSS = "/@inkandswitch/patchwork/global.css";
|
|
9
|
+
const BOOTLOADER_CSS = "/@inkandswitch/patchwork-bootloader/global.css";
|
|
10
|
+
// The generated index.html links the stylesheet by bare specifier, which the
|
|
11
|
+
// build resolves and the dev server does not. Serving the two files under
|
|
12
|
+
// these paths, and pointing the link at the first one, covers the gap.
|
|
13
|
+
const stylesheets = {
|
|
14
|
+
[PATCHWORK_CSS]: fileURLToPath(import.meta.resolve("@inkandswitch/patchwork/global.css")),
|
|
15
|
+
[BOOTLOADER_CSS]: fileURLToPath(import.meta.resolve("@inkandswitch/patchwork-bootloader/global.css")),
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Workers are `type: "module"` scripts the browser fetches directly, so import
|
|
19
|
+
* maps don't apply to them and their heavy imports have to resolve to real
|
|
20
|
+
* URLs. The build rewrites those to the `/packages/...` chunks it emits; here
|
|
21
|
+
* they become the dev server's own optimized-dep URLs.
|
|
22
|
+
*/
|
|
23
|
+
function externalBuiltins() {
|
|
24
|
+
return {
|
|
25
|
+
name: "patchwork-dev-externals",
|
|
26
|
+
setup(build) {
|
|
27
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
28
|
+
if (!(args.path in builtins))
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
path: `/@id/${devDependencyId(args.path)}`,
|
|
32
|
+
external: true,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function workerContexts(options) {
|
|
39
|
+
const contexts = new Map();
|
|
40
|
+
for (const { specifier, fileName } of workers) {
|
|
41
|
+
contexts.set(`/${fileName}`, esbuild.context({
|
|
42
|
+
entryPoints: [fileURLToPath(import.meta.resolve(specifier))],
|
|
43
|
+
bundle: true,
|
|
44
|
+
write: false,
|
|
45
|
+
format: "esm",
|
|
46
|
+
platform: "browser",
|
|
47
|
+
target: "firefox115",
|
|
48
|
+
sourcemap: "inline",
|
|
49
|
+
define: buildDefines(options),
|
|
50
|
+
plugins: [externalBuiltins()],
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
return contexts;
|
|
54
|
+
}
|
|
55
|
+
export function devPlugin(options = {}) {
|
|
56
|
+
let serve = false;
|
|
57
|
+
let contexts;
|
|
58
|
+
const wasm = new Map(wasmAssets().map(({ fileName, path }) => [`/${fileName}`, path]));
|
|
59
|
+
return {
|
|
60
|
+
name: "@patchwork/dev",
|
|
61
|
+
configResolved(config) {
|
|
62
|
+
serve = config.command === "serve";
|
|
63
|
+
},
|
|
64
|
+
transformIndexHtml(html) {
|
|
65
|
+
if (!serve)
|
|
66
|
+
return html;
|
|
67
|
+
return html.replace(`href="@inkandswitch/patchwork/global.css"`, `href="${PATCHWORK_CSS}"`);
|
|
68
|
+
},
|
|
69
|
+
async buildEnd() {
|
|
70
|
+
if (!contexts)
|
|
71
|
+
return;
|
|
72
|
+
for (const context of contexts.values())
|
|
73
|
+
(await context).dispose();
|
|
74
|
+
contexts = undefined;
|
|
75
|
+
},
|
|
76
|
+
configureServer(server) {
|
|
77
|
+
contexts = workerContexts(options);
|
|
78
|
+
server.middlewares.use(async (request, response, next) => {
|
|
79
|
+
const pathname = request.url?.split("?")[0] ?? "";
|
|
80
|
+
const stylesheet = stylesheets[pathname];
|
|
81
|
+
if (stylesheet) {
|
|
82
|
+
try {
|
|
83
|
+
const css = await readFile(stylesheet, "utf8");
|
|
84
|
+
response.setHeader("Content-Type", "text/css");
|
|
85
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
86
|
+
response.end(pathname === PATCHWORK_CSS
|
|
87
|
+
? css.replace(`"@inkandswitch/patchwork-bootloader/global.css"`, `"${BOOTLOADER_CSS}"`)
|
|
88
|
+
: css);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
next();
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const binary = wasm.get(pathname);
|
|
96
|
+
if (binary) {
|
|
97
|
+
try {
|
|
98
|
+
response.setHeader("Content-Type", "application/wasm");
|
|
99
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
100
|
+
response.end(await readFile(binary));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
next();
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const context = contexts?.get(pathname);
|
|
108
|
+
if (!context)
|
|
109
|
+
return next();
|
|
110
|
+
try {
|
|
111
|
+
// Rebuilt per request rather than watched: a worker is fetched once
|
|
112
|
+
// when the browser starts it, and esbuild's incremental rebuild is
|
|
113
|
+
// cheaper than keeping a watcher per entry.
|
|
114
|
+
const result = await (await context).rebuild();
|
|
115
|
+
response.setHeader("Content-Type", "text/javascript");
|
|
116
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
117
|
+
response.end(result.outputFiles[0].text);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// A worker that fails to build is otherwise a silent 500 in a
|
|
121
|
+
// context with no console of its own.
|
|
122
|
+
server.config.logger.error(`[patchwork] failed to bundle ${pathname}: ${error}`);
|
|
123
|
+
response.statusCode = 500;
|
|
124
|
+
response.end(`console.error(${JSON.stringify(String(error))})`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
3
3
|
export declare const builtins: Record<string, string>;
|
|
4
|
+
export declare function devDependencyId(id: string): string;
|
|
4
5
|
export declare function importmap(options?: PatchworkVitePluginOptions): Plugin;
|
|
@@ -37,7 +37,7 @@ function createImportMap(options) {
|
|
|
37
37
|
Object.assign(importmap.imports, builtins);
|
|
38
38
|
return { importmap, builtins };
|
|
39
39
|
}
|
|
40
|
-
function devDependencyId(id) {
|
|
40
|
+
export function devDependencyId(id) {
|
|
41
41
|
if (id === "@inkandswitch/patchwork")
|
|
42
42
|
return id;
|
|
43
43
|
if (id === "@inkandswitch/patchwork-bootloader") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { importmap } from "./importmap-plugin.js";
|
|
2
2
|
import { serviceworker } from "./service-worker-plugin.js";
|
|
3
3
|
import { configPlugin, wasm } from "./config-plugin.js";
|
|
4
|
+
import { devPlugin } from "./dev-plugin.js";
|
|
4
5
|
import { iconsPlugin } from "./icons.js";
|
|
5
6
|
import { htmlPlugin } from "./html-plugin.js";
|
|
6
7
|
import { manifestPlugin } from "./manifest-plugin.js";
|
|
@@ -36,5 +37,6 @@ export default function patchwork(options) {
|
|
|
36
37
|
netlifyPlugin(options),
|
|
37
38
|
importmap(options),
|
|
38
39
|
serviceworker(),
|
|
40
|
+
devPlugin(options),
|
|
39
41
|
].filter((plugin) => plugin != null);
|
|
40
42
|
}
|
|
@@ -10,7 +10,7 @@ const self = fileURLToPath(import.meta.url);
|
|
|
10
10
|
// own chunks. Their heavy imports are marked external and resolved to
|
|
11
11
|
// /packages/... URLs (both workers are created with type:"module", so the
|
|
12
12
|
// browser fetches those as regular network requests).
|
|
13
|
-
const workers = [
|
|
13
|
+
export const workers = [
|
|
14
14
|
{
|
|
15
15
|
specifier: "@inkandswitch/patchwork-bootloader/service-worker",
|
|
16
16
|
fileName: "service-worker.js",
|
|
@@ -26,10 +26,17 @@ const workers = [
|
|
|
26
26
|
];
|
|
27
27
|
export function serviceworker() {
|
|
28
28
|
const entryIds = new Set();
|
|
29
|
+
let serve = false;
|
|
29
30
|
return {
|
|
30
31
|
name: "@patchwork/service-worker",
|
|
31
32
|
enforce: "pre",
|
|
33
|
+
configResolved(config) {
|
|
34
|
+
serve = config.command === "serve";
|
|
35
|
+
},
|
|
32
36
|
async buildStart() {
|
|
37
|
+
// emitFile throws in serve mode.
|
|
38
|
+
if (serve)
|
|
39
|
+
return;
|
|
33
40
|
for (const { specifier, fileName } of workers) {
|
|
34
41
|
const resolved = await this.resolve(specifier, self);
|
|
35
42
|
entryIds.add(resolved.id);
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+https://github.com/inkandswitch/patchwork-system.git",
|
|
6
6
|
"directory": "core/patchwork"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.6.0",
|
|
9
9
|
"author": "Ink & Switch",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"license": "MIT",
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@automerge/automerge": "3.3.2",
|
|
36
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
36
|
+
"@automerge/automerge-repo": "2.6.0-subduction.47",
|
|
37
37
|
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.8b",
|
|
38
|
-
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.
|
|
38
|
+
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47",
|
|
39
39
|
"@automerge/automerge-subduction": "0.16.0",
|
|
40
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
40
|
+
"@automerge/vanillajs": "2.6.0-subduction.47",
|
|
41
41
|
"@types/debug": "^4.1.13",
|
|
42
42
|
"debug": "^4.4.3",
|
|
43
|
+
"esbuild": "^0.23.1",
|
|
43
44
|
"sharp": "^0.35.3",
|
|
44
45
|
"vite-plugin-wasm": "^3.6.0",
|
|
46
|
+
"@inkandswitch/patchwork-filesystem": "^0.2.5",
|
|
47
|
+
"@inkandswitch/patchwork-bootloader": "^0.6.1",
|
|
45
48
|
"@inkandswitch/patchwork-plugins": "^1.2.0",
|
|
46
49
|
"@inkandswitch/patchwork-providers": "^0.5.0",
|
|
47
|
-
"@inkandswitch/patchwork-elements": "^6.0.0"
|
|
48
|
-
"@inkandswitch/patchwork-filesystem": "^0.2.5",
|
|
49
|
-
"@inkandswitch/patchwork-bootloader": "^0.5.4"
|
|
50
|
+
"@inkandswitch/patchwork-elements": "^6.0.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"esbuild": "^0.23.1",
|
|
53
53
|
"rollup": "^4.61.1",
|
|
54
54
|
"vite": "^7.3.5"
|
|
55
55
|
},
|
package/src/client.d.ts
CHANGED
package/src/createAccount.ts
CHANGED
|
@@ -40,7 +40,6 @@ export const createDefaultAccount: AccountCreator<AccountDoc> = async (
|
|
|
40
40
|
]);
|
|
41
41
|
|
|
42
42
|
accountHandle.change((doc) => {
|
|
43
|
-
doc.frameToolId = "threepane";
|
|
44
43
|
doc.rootFolderUrl = rootFolder.url;
|
|
45
44
|
doc.moduleSettingsUrl = moduleSettings.url;
|
|
46
45
|
doc.contactUrl = contact.url;
|
package/src/index.ts
CHANGED
|
@@ -64,7 +64,7 @@ import { createDefaultAccount } from "./createAccount.js";
|
|
|
64
64
|
|
|
65
65
|
const log = debug("patchwork:setup");
|
|
66
66
|
|
|
67
|
-
declare const
|
|
67
|
+
declare const __SITE_TITLE__: string;
|
|
68
68
|
|
|
69
69
|
declare global {
|
|
70
70
|
interface Window {
|
|
@@ -114,9 +114,9 @@ export function setup(options: PatchworkOptions = {}): Promise<Patchwork> {
|
|
|
114
114
|
export default setup;
|
|
115
115
|
|
|
116
116
|
async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
|
|
117
|
-
const
|
|
118
|
-
options.
|
|
119
|
-
(typeof
|
|
117
|
+
const siteTitle =
|
|
118
|
+
options.title ??
|
|
119
|
+
(typeof __SITE_TITLE__ !== "undefined" ? __SITE_TITLE__ : "Patchwork");
|
|
120
120
|
const moduleSources = resolveDefaultModules(options);
|
|
121
121
|
const routing = options.routing ?? "hash";
|
|
122
122
|
|
|
@@ -151,10 +151,7 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
|
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
|
|
154
|
-
({ repo, hive, signerIdentity } = await createRepo(
|
|
155
|
-
siteName,
|
|
156
|
-
workerAdapter
|
|
157
|
-
));
|
|
154
|
+
({ repo, hive, signerIdentity } = await createRepo(workerAdapter));
|
|
158
155
|
|
|
159
156
|
// The worker was recreated with cold state: wire the repo onto the fresh
|
|
160
157
|
// port and drop the adapter stranded on the dead one.
|
|
@@ -246,7 +243,8 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
|
|
|
246
243
|
rootElement,
|
|
247
244
|
repo,
|
|
248
245
|
accountDocHandle,
|
|
249
|
-
|
|
246
|
+
siteTitle,
|
|
247
|
+
frameToolId: options.frameToolId,
|
|
250
248
|
});
|
|
251
249
|
}
|
|
252
250
|
|
package/src/repo.ts
CHANGED
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
18
18
|
import { MemorySigner } from "@automerge/automerge-subduction/slim";
|
|
19
19
|
import setupServiceWorker from "@inkandswitch/patchwork-bootloader";
|
|
20
|
+
import {
|
|
21
|
+
keyhiveStorageName,
|
|
22
|
+
storagePrefix,
|
|
23
|
+
} from "@inkandswitch/patchwork-bootloader/storage";
|
|
20
24
|
import type { SignerIdentity } from "./types.js";
|
|
21
25
|
import debug from "debug";
|
|
22
26
|
|
|
@@ -50,7 +54,6 @@ export function initWasm(): Promise<void> {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export async function createRepo(
|
|
53
|
-
siteName: string,
|
|
54
57
|
workerAdapter: MessageChannelNetworkAdapter
|
|
55
58
|
): Promise<{
|
|
56
59
|
repo: Repo;
|
|
@@ -62,8 +65,8 @@ export async function createRepo(
|
|
|
62
65
|
initKeyhiveWasm();
|
|
63
66
|
const { hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
|
|
64
67
|
createRepo: (repoConfig) => new Repo(repoConfig),
|
|
65
|
-
storage: new IndexedDBWorkerStorageAdapter(
|
|
66
|
-
peerIdSuffix:
|
|
68
|
+
storage: new IndexedDBWorkerStorageAdapter(keyhiveStorageName),
|
|
69
|
+
peerIdSuffix: storagePrefix + Math.random().toString(36).slice(2),
|
|
67
70
|
networkAdapter: workerAdapter,
|
|
68
71
|
automaticArchiveIngestion: true,
|
|
69
72
|
cachingMode: "periodic",
|
|
@@ -91,7 +94,8 @@ export async function createRepo(
|
|
|
91
94
|
return peerId.includes("automerge-worker");
|
|
92
95
|
},
|
|
93
96
|
enableRemoteHeadsGossiping: true,
|
|
94
|
-
peerId:
|
|
97
|
+
peerId:
|
|
98
|
+
`${storagePrefix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
|
|
95
99
|
});
|
|
96
100
|
const signerIdentity = {
|
|
97
101
|
peerId: signer.peerId().toString(),
|
package/src/router.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isValidAutomergeUrl,
|
|
3
3
|
isValidDocumentId,
|
|
4
|
+
parseAutomergeUrl,
|
|
4
5
|
stringifyAutomergeUrl,
|
|
5
6
|
type AutomergeUrl,
|
|
6
7
|
type DocHandle,
|
|
@@ -23,12 +24,12 @@ import {
|
|
|
23
24
|
const BIG_PATCHWORK_HASH_REGEX =
|
|
24
25
|
/^(?<title>[^=&?/#]*)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)/;
|
|
25
26
|
|
|
26
|
-
// The `doc=`
|
|
27
|
+
// The `doc=` and `draft=` values are automerge URLs, kept literal rather than
|
|
27
28
|
// percent-encoded so links stay readable.
|
|
28
|
-
const RAW_HASH_KEYS = new Set(["doc"]);
|
|
29
|
+
const RAW_HASH_KEYS = new Set(["doc", "draft"]);
|
|
29
30
|
// A stable order means re-serializing the same logical params is
|
|
30
31
|
// byte-identical, avoiding spurious `hashchange` round-trips.
|
|
31
|
-
const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame"];
|
|
32
|
+
const HASH_KEY_ORDER = ["doc", "tool", "type", "title", "frame", "draft"];
|
|
32
33
|
|
|
33
34
|
function serializeHashParams(params: URLSearchParams): string {
|
|
34
35
|
const keys = [...HASH_KEY_ORDER, ...params.keys()];
|
|
@@ -64,7 +65,8 @@ export interface RouterParams {
|
|
|
64
65
|
rootElement: HTMLElement;
|
|
65
66
|
repo: Repo;
|
|
66
67
|
accountDocHandle: DocHandle<AccountDoc>;
|
|
67
|
-
|
|
68
|
+
siteTitle: string;
|
|
69
|
+
frameToolId?: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export interface Router {
|
|
@@ -82,7 +84,8 @@ export function createRouter({
|
|
|
82
84
|
rootElement,
|
|
83
85
|
repo,
|
|
84
86
|
accountDocHandle,
|
|
85
|
-
|
|
87
|
+
siteTitle,
|
|
88
|
+
frameToolId,
|
|
86
89
|
}: RouterParams): Router {
|
|
87
90
|
const route = async () => {
|
|
88
91
|
// The first call seeds the root view's tool/doc so it can mount; later
|
|
@@ -91,7 +94,10 @@ export function createRouter({
|
|
|
91
94
|
const params = new URLSearchParams(location.hash.slice(1));
|
|
92
95
|
const frame = params.get("frame");
|
|
93
96
|
const toolId =
|
|
94
|
-
frame ??
|
|
97
|
+
frame ??
|
|
98
|
+
accountDocHandle.doc().frameToolId ??
|
|
99
|
+
frameToolId ??
|
|
100
|
+
registeredFrameToolId();
|
|
95
101
|
if (!toolId) {
|
|
96
102
|
console.error("patchwork: no frame tool registered, nothing to mount");
|
|
97
103
|
return;
|
|
@@ -166,6 +172,16 @@ export function createRouter({
|
|
|
166
172
|
// `doc` is the full automerge URL, so heads live inside it and the separate
|
|
167
173
|
// `heads=` param is gone.
|
|
168
174
|
params.delete("heads");
|
|
175
|
+
// `draft` is doc-scoped (owned by the drafts plugin, opaque here):
|
|
176
|
+
// navigating to a different document invalidates the selection, while
|
|
177
|
+
// same-doc navigation (tool switches, heads changes) keeps it.
|
|
178
|
+
const prevDoc = docParamToUrl(params.get("doc"));
|
|
179
|
+
if (
|
|
180
|
+
prevDoc &&
|
|
181
|
+
parseAutomergeUrl(prevDoc).documentId !== parseAutomergeUrl(url).documentId
|
|
182
|
+
) {
|
|
183
|
+
params.delete("draft");
|
|
184
|
+
}
|
|
169
185
|
params.set("doc", url);
|
|
170
186
|
for (const [key, value] of [
|
|
171
187
|
["tool", toolId],
|
|
@@ -192,7 +208,7 @@ export function createRouter({
|
|
|
192
208
|
const docTitle = (datatype.module as DatatypeImplementation).getTitle(
|
|
193
209
|
doc
|
|
194
210
|
);
|
|
195
|
-
if (docTitle) document.title = `${docTitle} | ${
|
|
211
|
+
if (docTitle) document.title = `${docTitle} | ${siteTitle}`;
|
|
196
212
|
} catch (e) {
|
|
197
213
|
console.error("Failed to update document title", e);
|
|
198
214
|
}
|
package/src/site-kit/html.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { DEFAULT_TITLE, type PatchworkSiteOptions } from "./options.js";
|
|
2
2
|
import { resolveSyncServers, PRELOAD_WASM_ASSETS } from "./sync-servers.js";
|
|
3
3
|
import { ICON_SPECS } from "./icons.js";
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ export function escapeHtml(value: string): string {
|
|
|
16
16
|
|
|
17
17
|
/** Builds the generated index.html as a plain string — no bundler involved. */
|
|
18
18
|
export function buildHtml(options: PatchworkSiteOptions): string {
|
|
19
|
-
const title = options.title ??
|
|
19
|
+
const title = options.title ?? DEFAULT_TITLE;
|
|
20
20
|
const lang = (options.html && options.html.lang) || "en";
|
|
21
21
|
const entry = options.entry ?? "/src/main.ts";
|
|
22
22
|
const syncServers = resolveSyncServers(options);
|
package/src/site-kit/manifest.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { DEFAULT_TITLE, type PatchworkSiteOptions } from "./options.js";
|
|
2
2
|
import { ICON_SPECS } from "./icons.js";
|
|
3
3
|
|
|
4
4
|
/** Builds the generated manifest.webmanifest object — no bundler involved. */
|
|
5
5
|
export function buildManifest(
|
|
6
6
|
options: PatchworkSiteOptions
|
|
7
7
|
): Record<string, unknown> {
|
|
8
|
-
const title = options.title ??
|
|
8
|
+
const title = options.title ?? DEFAULT_TITLE;
|
|
9
9
|
const icons = !options.icons
|
|
10
10
|
? []
|
|
11
11
|
: ICON_SPECS.filter(
|
package/src/site-kit/options.ts
CHANGED
|
@@ -42,10 +42,24 @@ export type PatchworkSyncServersOptions = {
|
|
|
42
42
|
classic?: string | false;
|
|
43
43
|
} & PatchworkPrimarySyncServerOptions;
|
|
44
44
|
|
|
45
|
+
export const DEFAULT_TITLE = "Patchwork";
|
|
46
|
+
|
|
45
47
|
export interface PatchworkSiteOptions {
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Namespace for this site's IndexedDB databases and peer ids
|
|
50
|
+
* (-> __STORAGE_PREFIX__ define). Defaults to `"patchwork"`. Sites sharing
|
|
51
|
+
* an origin MUST use distinct prefixes.
|
|
52
|
+
*
|
|
53
|
+
* The tab and the shared automerge worker are separate bundles that have to
|
|
54
|
+
* open the same databases, so this is settable only here, where both of them
|
|
55
|
+
* receive it. Changing it on an existing site points it at empty storage.
|
|
56
|
+
*/
|
|
57
|
+
storagePrefix?: string;
|
|
58
|
+
/**
|
|
59
|
+
* This site's name: `<title>`, apple-mobile-web-app-title, manifest name,
|
|
60
|
+
* and — via the __SITE_TITLE__ define — the brand word the router appends to
|
|
61
|
+
* the document title as `"<doc> | <title>"`. Defaults to `"Patchwork"`.
|
|
62
|
+
*/
|
|
49
63
|
title?: string;
|
|
50
64
|
/** manifest short_name (defaults to title) */
|
|
51
65
|
shortName?: string;
|
package/src/types.ts
CHANGED
|
@@ -65,13 +65,25 @@ export interface PatchworkOptions {
|
|
|
65
65
|
createAccount?: AccountCreator;
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* for you), and used to namespace this site's storage and peer ids.
|
|
68
|
+
* This site's name, appended to the document title as `"<doc> | <title>"`
|
|
69
|
+
* when a document is open (the separator is provided for you).
|
|
71
70
|
*
|
|
72
|
-
* Defaults to the build-time `
|
|
71
|
+
* Defaults to the build-time `__SITE_TITLE__` define — the vite plugin's
|
|
72
|
+
* `title` option, which also names the html `<title>` and the manifest —
|
|
73
|
+
* then `"Patchwork"`.
|
|
73
74
|
*/
|
|
74
|
-
|
|
75
|
+
title?: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Id of the tool that frames this site — what mounts in the root view when
|
|
79
|
+
* the user hasn't chosen one on their account and the URL doesn't name one
|
|
80
|
+
* via `#frame=`. Must be a tool one of the {@link
|
|
81
|
+
* PatchworkOptions.packageListURL} sources registers.
|
|
82
|
+
*
|
|
83
|
+
* Falls back to the first registered tool tagged `frame-tool`, which with
|
|
84
|
+
* more than one of them depends on module load order.
|
|
85
|
+
*/
|
|
86
|
+
frameToolId?: string;
|
|
75
87
|
|
|
76
88
|
/** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
|
|
77
89
|
rootElementId?: string;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import wasm from "vite-plugin-wasm";
|
|
3
|
+
import { DEFAULT_STORAGE_PREFIX } from "@inkandswitch/patchwork-bootloader/storage";
|
|
4
|
+
import { DEFAULT_TITLE } from "../site-kit/options.js";
|
|
3
5
|
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
4
6
|
import {
|
|
5
7
|
DEFAULT_SYNC_SERVERS,
|
|
@@ -9,7 +11,30 @@ import {
|
|
|
9
11
|
const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
14
|
+
* The build-time constants both the page bundle and the workers are compiled
|
|
15
|
+
* against. Values are already JSON — the shape vite's `define` and esbuild's
|
|
16
|
+
* `define` both take.
|
|
17
|
+
*/
|
|
18
|
+
export function buildDefines(
|
|
19
|
+
options: PatchworkVitePluginOptions = {}
|
|
20
|
+
): Record<string, string> {
|
|
21
|
+
const classicSyncServer =
|
|
22
|
+
options.syncServers && typeof options.syncServers.classic === "string"
|
|
23
|
+
? options.syncServers.classic
|
|
24
|
+
: DEFAULT_SYNC_SERVERS.classic;
|
|
25
|
+
return {
|
|
26
|
+
__SYNC_SERVER__: JSON.stringify(resolvePrimarySyncServer(options)),
|
|
27
|
+
__CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
|
|
28
|
+
__SITE_TITLE__: JSON.stringify(options.title ?? DEFAULT_TITLE),
|
|
29
|
+
__STORAGE_PREFIX__: JSON.stringify(
|
|
30
|
+
options.storagePrefix ?? DEFAULT_STORAGE_PREFIX
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
|
|
37
|
+
* configuration),
|
|
13
38
|
* server/preview CORS defaults, worker format + the wasm plugin, and build
|
|
14
39
|
* defaults (firefox150 target, unminified, sourcemapped) — everything a site
|
|
15
40
|
* used to hand-write in its own vite.config.ts. Each is switched off
|
|
@@ -21,22 +46,17 @@ export function configPlugin(
|
|
|
21
46
|
return {
|
|
22
47
|
name: "@patchwork/config",
|
|
23
48
|
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;
|
|
29
|
-
const define: Record<string, string> = {
|
|
30
|
-
__SYNC_SERVER__: JSON.stringify(primarySyncServer),
|
|
31
|
-
__CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
|
|
32
|
-
};
|
|
33
|
-
if (options.siteName) {
|
|
34
|
-
define.__SITE_NAME__ = JSON.stringify(options.siteName);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
49
|
return {
|
|
38
50
|
envPrefix: ["VITE_", "PATCHWORK_"],
|
|
39
|
-
define,
|
|
51
|
+
define: buildDefines(options),
|
|
52
|
+
optimizeDeps: {
|
|
53
|
+
exclude: ["@automerge/automerge-repo-storage-indexeddb"],
|
|
54
|
+
// Dep pre-bundling runs esbuild directly, outside the plugin
|
|
55
|
+
// pipeline that applies `define`. Without this the page would fall
|
|
56
|
+
// back to the built-in storage prefix while the workers used the
|
|
57
|
+
// configured one, and they have to open the same databases.
|
|
58
|
+
esbuildOptions: { define: buildDefines(options) },
|
|
59
|
+
},
|
|
40
60
|
server:
|
|
41
61
|
options.server === false
|
|
42
62
|
? undefined
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import * as esbuild from "esbuild";
|
|
5
|
+
import { wasmAssets } from "@inkandswitch/patchwork-bootloader/externals";
|
|
6
|
+
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
7
|
+
import { buildDefines } from "./config-plugin.js";
|
|
8
|
+
import { builtins, devDependencyId } from "./importmap-plugin.js";
|
|
9
|
+
import { workers } from "./service-worker-plugin.js";
|
|
10
|
+
|
|
11
|
+
const PATCHWORK_CSS = "/@inkandswitch/patchwork/global.css";
|
|
12
|
+
const BOOTLOADER_CSS = "/@inkandswitch/patchwork-bootloader/global.css";
|
|
13
|
+
|
|
14
|
+
// The generated index.html links the stylesheet by bare specifier, which the
|
|
15
|
+
// build resolves and the dev server does not. Serving the two files under
|
|
16
|
+
// these paths, and pointing the link at the first one, covers the gap.
|
|
17
|
+
const stylesheets: Record<string, string> = {
|
|
18
|
+
[PATCHWORK_CSS]: fileURLToPath(
|
|
19
|
+
import.meta.resolve("@inkandswitch/patchwork/global.css")
|
|
20
|
+
),
|
|
21
|
+
[BOOTLOADER_CSS]: fileURLToPath(
|
|
22
|
+
import.meta.resolve("@inkandswitch/patchwork-bootloader/global.css")
|
|
23
|
+
),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Workers are `type: "module"` scripts the browser fetches directly, so import
|
|
28
|
+
* maps don't apply to them and their heavy imports have to resolve to real
|
|
29
|
+
* URLs. The build rewrites those to the `/packages/...` chunks it emits; here
|
|
30
|
+
* they become the dev server's own optimized-dep URLs.
|
|
31
|
+
*/
|
|
32
|
+
function externalBuiltins(): esbuild.Plugin {
|
|
33
|
+
return {
|
|
34
|
+
name: "patchwork-dev-externals",
|
|
35
|
+
setup(build) {
|
|
36
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
37
|
+
if (!(args.path in builtins)) return null;
|
|
38
|
+
return {
|
|
39
|
+
path: `/@id/${devDependencyId(args.path)}`,
|
|
40
|
+
external: true,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function workerContexts(
|
|
48
|
+
options: PatchworkVitePluginOptions
|
|
49
|
+
): Map<string, Promise<esbuild.BuildContext>> {
|
|
50
|
+
const contexts = new Map<string, Promise<esbuild.BuildContext>>();
|
|
51
|
+
for (const { specifier, fileName } of workers) {
|
|
52
|
+
contexts.set(
|
|
53
|
+
`/${fileName}`,
|
|
54
|
+
esbuild.context({
|
|
55
|
+
entryPoints: [fileURLToPath(import.meta.resolve(specifier))],
|
|
56
|
+
bundle: true,
|
|
57
|
+
write: false,
|
|
58
|
+
format: "esm",
|
|
59
|
+
platform: "browser",
|
|
60
|
+
target: "firefox115",
|
|
61
|
+
sourcemap: "inline",
|
|
62
|
+
define: buildDefines(options),
|
|
63
|
+
plugins: [externalBuiltins()],
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return contexts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function devPlugin(options: PatchworkVitePluginOptions = {}): Plugin {
|
|
71
|
+
let serve = false;
|
|
72
|
+
let contexts: Map<string, Promise<esbuild.BuildContext>> | undefined;
|
|
73
|
+
const wasm = new Map(
|
|
74
|
+
wasmAssets().map(({ fileName, path }) => [`/${fileName}`, path])
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
name: "@patchwork/dev",
|
|
79
|
+
configResolved(config) {
|
|
80
|
+
serve = config.command === "serve";
|
|
81
|
+
},
|
|
82
|
+
transformIndexHtml(html) {
|
|
83
|
+
if (!serve) return html;
|
|
84
|
+
return html.replace(
|
|
85
|
+
`href="@inkandswitch/patchwork/global.css"`,
|
|
86
|
+
`href="${PATCHWORK_CSS}"`
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
async buildEnd() {
|
|
90
|
+
if (!contexts) return;
|
|
91
|
+
for (const context of contexts.values()) (await context).dispose();
|
|
92
|
+
contexts = undefined;
|
|
93
|
+
},
|
|
94
|
+
configureServer(server) {
|
|
95
|
+
contexts = workerContexts(options);
|
|
96
|
+
|
|
97
|
+
server.middlewares.use(async (request, response, next) => {
|
|
98
|
+
const pathname = request.url?.split("?")[0] ?? "";
|
|
99
|
+
|
|
100
|
+
const stylesheet = stylesheets[pathname];
|
|
101
|
+
if (stylesheet) {
|
|
102
|
+
try {
|
|
103
|
+
const css = await readFile(stylesheet, "utf8");
|
|
104
|
+
response.setHeader("Content-Type", "text/css");
|
|
105
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
106
|
+
response.end(
|
|
107
|
+
pathname === PATCHWORK_CSS
|
|
108
|
+
? css.replace(
|
|
109
|
+
`"@inkandswitch/patchwork-bootloader/global.css"`,
|
|
110
|
+
`"${BOOTLOADER_CSS}"`
|
|
111
|
+
)
|
|
112
|
+
: css
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
next();
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const binary = wasm.get(pathname);
|
|
121
|
+
if (binary) {
|
|
122
|
+
try {
|
|
123
|
+
response.setHeader("Content-Type", "application/wasm");
|
|
124
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
125
|
+
response.end(await readFile(binary));
|
|
126
|
+
} catch {
|
|
127
|
+
next();
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const context = contexts?.get(pathname);
|
|
133
|
+
if (!context) return next();
|
|
134
|
+
try {
|
|
135
|
+
// Rebuilt per request rather than watched: a worker is fetched once
|
|
136
|
+
// when the browser starts it, and esbuild's incremental rebuild is
|
|
137
|
+
// cheaper than keeping a watcher per entry.
|
|
138
|
+
const result = await (await context).rebuild();
|
|
139
|
+
response.setHeader("Content-Type", "text/javascript");
|
|
140
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
141
|
+
response.end(result.outputFiles![0]!.text);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// A worker that fails to build is otherwise a silent 500 in a
|
|
144
|
+
// context with no console of its own.
|
|
145
|
+
server.config.logger.error(
|
|
146
|
+
`[patchwork] failed to bundle ${pathname}: ${error}`
|
|
147
|
+
);
|
|
148
|
+
response.statusCode = 500;
|
|
149
|
+
response.end(`console.error(${JSON.stringify(String(error))})`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -65,7 +65,7 @@ function createImportMap(options?: PatchworkVitePluginOptions) {
|
|
|
65
65
|
return { importmap, builtins };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function devDependencyId(id: string): string {
|
|
68
|
+
export function devDependencyId(id: string): string {
|
|
69
69
|
if (id === "@inkandswitch/patchwork") return id;
|
|
70
70
|
if (id === "@inkandswitch/patchwork-bootloader") {
|
|
71
71
|
return `@inkandswitch/patchwork > ${id}`;
|
|
@@ -3,6 +3,7 @@ import type { Plugin, ServerOptions, PreviewOptions, BuildOptions } from "vite";
|
|
|
3
3
|
import { importmap } from "./importmap-plugin.js";
|
|
4
4
|
import { serviceworker } from "./service-worker-plugin.js";
|
|
5
5
|
import { configPlugin, wasm } from "./config-plugin.js";
|
|
6
|
+
import { devPlugin } from "./dev-plugin.js";
|
|
6
7
|
import { iconsPlugin } from "./icons.js";
|
|
7
8
|
import { htmlPlugin } from "./html-plugin.js";
|
|
8
9
|
import { manifestPlugin } from "./manifest-plugin.js";
|
|
@@ -40,6 +41,7 @@ export default function patchwork(options?: PatchworkVitePluginOptions) {
|
|
|
40
41
|
netlifyPlugin(options),
|
|
41
42
|
importmap(options),
|
|
42
43
|
serviceworker(),
|
|
44
|
+
devPlugin(options),
|
|
43
45
|
].filter((plugin): plugin is Plugin => plugin != null);
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -13,7 +13,7 @@ const self = fileURLToPath(import.meta.url);
|
|
|
13
13
|
// own chunks. Their heavy imports are marked external and resolved to
|
|
14
14
|
// /packages/... URLs (both workers are created with type:"module", so the
|
|
15
15
|
// browser fetches those as regular network requests).
|
|
16
|
-
const workers = [
|
|
16
|
+
export const workers = [
|
|
17
17
|
{
|
|
18
18
|
specifier: "@inkandswitch/patchwork-bootloader/service-worker",
|
|
19
19
|
fileName: "service-worker.js",
|
|
@@ -30,11 +30,17 @@ const workers = [
|
|
|
30
30
|
|
|
31
31
|
export function serviceworker(): Plugin {
|
|
32
32
|
const entryIds = new Set<string>();
|
|
33
|
+
let serve = false;
|
|
33
34
|
|
|
34
35
|
return {
|
|
35
36
|
name: "@patchwork/service-worker",
|
|
36
37
|
enforce: "pre",
|
|
38
|
+
configResolved(config) {
|
|
39
|
+
serve = config.command === "serve";
|
|
40
|
+
},
|
|
37
41
|
async buildStart() {
|
|
42
|
+
// emitFile throws in serve mode.
|
|
43
|
+
if (serve) return;
|
|
38
44
|
for (const { specifier, fileName } of workers) {
|
|
39
45
|
const resolved = await this.resolve(specifier, self);
|
|
40
46
|
entryIds.add(resolved!.id);
|