@inkandswitch/patchwork 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/createAccount.js +0 -1
- package/dist/index.js +1 -0
- package/dist/router.d.ts +2 -1
- package/dist/router.js +5 -2
- package/dist/types.d.ts +10 -0
- package/dist/vite/config-plugin.d.ts +6 -0
- package/dist/vite/config-plugin.js +25 -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/createAccount.ts +0 -1
- package/src/index.ts +1 -0
- package/src/router.ts +6 -1
- package/src/types.ts +11 -0
- package/src/vite/config-plugin.ts +31 -17
- 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,50 @@
|
|
|
1
1
|
# @inkandswitch/patchwork
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f37bb8e: Bump `@automerge/automerge-subduction` to 0.16.1.
|
|
8
|
+
- Updated dependencies [f37bb8e]
|
|
9
|
+
- @inkandswitch/patchwork-bootloader@0.6.2
|
|
10
|
+
|
|
11
|
+
## 0.6.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 61a152a: Add a `frameToolId` option to `setup`, and stop seeding a frame tool into new accounts.
|
|
16
|
+
|
|
17
|
+
`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.
|
|
18
|
+
|
|
19
|
+
New accounts now leave `frameToolId` unset, and the router resolves the frame each boot:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
#frame= → the account's frameToolId → setup({frameToolId}) → first tool tagged frame-tool
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
Sites relying on the old seeded default should pass `frameToolId: "threepane"` to `setup`.
|
|
28
|
+
|
|
29
|
+
- 61a152a: Make `vite dev` work without a site hand-rolling the dev server.
|
|
30
|
+
|
|
31
|
+
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:
|
|
32
|
+
|
|
33
|
+
- 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.
|
|
34
|
+
- `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.
|
|
35
|
+
- `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.
|
|
36
|
+
- `@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.
|
|
37
|
+
- 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.
|
|
38
|
+
|
|
39
|
+
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`.
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- 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.
|
|
44
|
+
- Updated dependencies [846cfac]
|
|
45
|
+
- Updated dependencies [61a152a]
|
|
46
|
+
- @inkandswitch/patchwork-bootloader@0.6.1
|
|
47
|
+
|
|
3
48
|
## 0.5.0
|
|
4
49
|
|
|
5
50
|
### Minor Changes
|
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
package/dist/router.d.ts
CHANGED
|
@@ -10,9 +10,10 @@ export interface RouterParams {
|
|
|
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, siteTitle, }: RouterParams): Router;
|
|
19
|
+
export declare function createRouter({ rootElement, repo, accountDocHandle, siteTitle, frameToolId, }: RouterParams): Router;
|
package/dist/router.js
CHANGED
|
@@ -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, siteTitle, }) {
|
|
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;
|
package/dist/types.d.ts
CHANGED
|
@@ -55,6 +55,16 @@ export interface PatchworkOptions {
|
|
|
55
55
|
* then `"Patchwork"`.
|
|
56
56
|
*/
|
|
57
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;
|
|
58
68
|
/** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
|
|
59
69
|
rootElementId?: string;
|
|
60
70
|
/**
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import wasm from "vite-plugin-wasm";
|
|
3
3
|
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
4
|
+
/**
|
|
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>;
|
|
4
10
|
/**
|
|
5
11
|
* Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
|
|
6
12
|
* configuration),
|
|
@@ -3,6 +3,22 @@ import { DEFAULT_STORAGE_PREFIX } from "@inkandswitch/patchwork-bootloader/stora
|
|
|
3
3
|
import { DEFAULT_TITLE } from "../site-kit/options.js";
|
|
4
4
|
import { DEFAULT_SYNC_SERVERS, resolvePrimarySyncServer, } from "../site-kit/sync-servers.js";
|
|
5
5
|
const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
|
|
6
|
+
/**
|
|
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
|
+
}
|
|
6
22
|
/**
|
|
7
23
|
* Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
|
|
8
24
|
* configuration),
|
|
@@ -15,21 +31,17 @@ export function configPlugin(options = {}) {
|
|
|
15
31
|
return {
|
|
16
32
|
name: "@patchwork/config",
|
|
17
33
|
config() {
|
|
18
|
-
const primarySyncServer = resolvePrimarySyncServer(options);
|
|
19
|
-
const classicSyncServer = options.syncServers && typeof options.syncServers.classic === "string"
|
|
20
|
-
? options.syncServers.classic
|
|
21
|
-
: DEFAULT_SYNC_SERVERS.classic;
|
|
22
|
-
// __STORAGE_PREFIX__ is defined unconditionally: the tab and the shared
|
|
23
|
-
// automerge worker resolve it separately, and only a define reaches both.
|
|
24
|
-
const define = {
|
|
25
|
-
__SYNC_SERVER__: JSON.stringify(primarySyncServer),
|
|
26
|
-
__CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
|
|
27
|
-
__SITE_TITLE__: JSON.stringify(options.title ?? DEFAULT_TITLE),
|
|
28
|
-
__STORAGE_PREFIX__: JSON.stringify(options.storagePrefix ?? DEFAULT_STORAGE_PREFIX),
|
|
29
|
-
};
|
|
30
34
|
return {
|
|
31
35
|
envPrefix: ["VITE_", "PATCHWORK_"],
|
|
32
|
-
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
|
+
},
|
|
33
45
|
server: options.server === false
|
|
34
46
|
? undefined
|
|
35
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.1",
|
|
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.
|
|
39
|
-
"@automerge/automerge-subduction": "0.16.
|
|
40
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
38
|
+
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47",
|
|
39
|
+
"@automerge/automerge-subduction": "0.16.1",
|
|
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",
|
|
45
|
-
"@inkandswitch/patchwork-
|
|
46
|
+
"@inkandswitch/patchwork-bootloader": "^0.6.2",
|
|
46
47
|
"@inkandswitch/patchwork-plugins": "^1.2.0",
|
|
47
|
-
"@inkandswitch/patchwork-bootloader": "^0.6.0",
|
|
48
48
|
"@inkandswitch/patchwork-filesystem": "^0.2.5",
|
|
49
|
+
"@inkandswitch/patchwork-elements": "^6.0.0",
|
|
49
50
|
"@inkandswitch/patchwork-providers": "^0.5.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/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
package/src/router.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface RouterParams {
|
|
|
66
66
|
repo: Repo;
|
|
67
67
|
accountDocHandle: DocHandle<AccountDoc>;
|
|
68
68
|
siteTitle: string;
|
|
69
|
+
frameToolId?: string;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export interface Router {
|
|
@@ -84,6 +85,7 @@ export function createRouter({
|
|
|
84
85
|
repo,
|
|
85
86
|
accountDocHandle,
|
|
86
87
|
siteTitle,
|
|
88
|
+
frameToolId,
|
|
87
89
|
}: RouterParams): Router {
|
|
88
90
|
const route = async () => {
|
|
89
91
|
// The first call seeds the root view's tool/doc so it can mount; later
|
|
@@ -92,7 +94,10 @@ export function createRouter({
|
|
|
92
94
|
const params = new URLSearchParams(location.hash.slice(1));
|
|
93
95
|
const frame = params.get("frame");
|
|
94
96
|
const toolId =
|
|
95
|
-
frame ??
|
|
97
|
+
frame ??
|
|
98
|
+
accountDocHandle.doc().frameToolId ??
|
|
99
|
+
frameToolId ??
|
|
100
|
+
registeredFrameToolId();
|
|
96
101
|
if (!toolId) {
|
|
97
102
|
console.error("patchwork: no frame tool registered, nothing to mount");
|
|
98
103
|
return;
|
package/src/types.ts
CHANGED
|
@@ -74,6 +74,17 @@ export interface PatchworkOptions {
|
|
|
74
74
|
*/
|
|
75
75
|
title?: string;
|
|
76
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;
|
|
87
|
+
|
|
77
88
|
/** DOM id of the `<patchwork-view>` hosting the root tool. Defaults to "root". */
|
|
78
89
|
rootElementId?: string;
|
|
79
90
|
|
|
@@ -10,6 +10,28 @@ import {
|
|
|
10
10
|
|
|
11
11
|
const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
|
|
12
12
|
|
|
13
|
+
/**
|
|
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
|
+
|
|
13
35
|
/**
|
|
14
36
|
* Owns envPrefix, define (__SITE_TITLE__/__STORAGE_PREFIX__/sync-server
|
|
15
37
|
* configuration),
|
|
@@ -24,25 +46,17 @@ export function configPlugin(
|
|
|
24
46
|
return {
|
|
25
47
|
name: "@patchwork/config",
|
|
26
48
|
config() {
|
|
27
|
-
const primarySyncServer = resolvePrimarySyncServer(options);
|
|
28
|
-
const classicSyncServer =
|
|
29
|
-
options.syncServers && typeof options.syncServers.classic === "string"
|
|
30
|
-
? options.syncServers.classic
|
|
31
|
-
: DEFAULT_SYNC_SERVERS.classic;
|
|
32
|
-
// __STORAGE_PREFIX__ is defined unconditionally: the tab and the shared
|
|
33
|
-
// automerge worker resolve it separately, and only a define reaches both.
|
|
34
|
-
const define: Record<string, string> = {
|
|
35
|
-
__SYNC_SERVER__: JSON.stringify(primarySyncServer),
|
|
36
|
-
__CLASSIC_SYNC_SERVER__: JSON.stringify(classicSyncServer),
|
|
37
|
-
__SITE_TITLE__: JSON.stringify(options.title ?? DEFAULT_TITLE),
|
|
38
|
-
__STORAGE_PREFIX__: JSON.stringify(
|
|
39
|
-
options.storagePrefix ?? DEFAULT_STORAGE_PREFIX
|
|
40
|
-
),
|
|
41
|
-
};
|
|
42
|
-
|
|
43
49
|
return {
|
|
44
50
|
envPrefix: ["VITE_", "PATCHWORK_"],
|
|
45
|
-
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
|
+
},
|
|
46
60
|
server:
|
|
47
61
|
options.server === false
|
|
48
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);
|