@chee/patchwork-bundles 0.0.1 → 0.1.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/.claude/settings.local.json +11 -1
- package/README.md +40 -0
- package/package.json +7 -5
- package/pnpm-workspace.yaml +41 -0
- package/src/index.ts +58 -1
- package/src/types.ts +15 -4
- package/src/utils.ts +72 -2
- package/test/integration.test.ts +76 -14
- package/tsconfig.json +31 -4
- package/vitest.config.d.ts +3 -0
- package/vitest.config.d.ts.map +1 -0
- package/vitest.config.js +10 -0
- package/vitest.config.js.map +1 -0
- package/dist/esbuild.d.ts +0 -2
- package/dist/esbuild.js +0 -2
- package/dist/farm.d.ts +0 -2
- package/dist/farm.js +0 -2
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -54
- package/dist/rollup.d.ts +0 -2
- package/dist/rollup.js +0 -2
- package/dist/rspack.d.ts +0 -2
- package/dist/rspack.js +0 -2
- package/dist/types.d.ts +0 -29
- package/dist/types.js +0 -9
- package/dist/utils.d.ts +0 -29
- package/dist/utils.js +0 -73
- package/dist/vite.d.ts +0 -2
- package/dist/vite.js +0 -2
- package/dist/webpack.d.ts +0 -2
- package/dist/webpack.js +0 -2
|
@@ -11,7 +11,17 @@
|
|
|
11
11
|
"Bash(ls:*)",
|
|
12
12
|
"Bash(echo:*)",
|
|
13
13
|
"Bash(pnpm test:*)",
|
|
14
|
-
"Bash(node -e:*)"
|
|
14
|
+
"Bash(node -e:*)",
|
|
15
|
+
"Bash(pnpm build *)",
|
|
16
|
+
"Bash(PATCHWORK_SYNC_SERVER=wss://subduction.sync.inkandswitch.com npx pnpm@11 install --no-frozen-lockfile)",
|
|
17
|
+
"Bash(rm -rf node_modules pnpm-lock.yaml)",
|
|
18
|
+
"Bash(time PATCHWORK_SUB=false npx pnpm@11 install --no-frozen-lockfile)",
|
|
19
|
+
"Bash(sed -i '' 's/kFcrzeDmr5zXE1jShvxUPsoToAN/6iXwddwF9cwrjmM5yqp2xUENxUY/' package.json)",
|
|
20
|
+
"Bash(cat)",
|
|
21
|
+
"Read(//private/tmp/**)",
|
|
22
|
+
"Read(//tmp/**)",
|
|
23
|
+
"Bash(PATCHWORK_SYNC_SERVER=wss://subduction.sync.inkandswitch.com npx pnpm@11 install --no-frozen-lockfile --config.strictDepBuilds=false)",
|
|
24
|
+
"Bash(PATCHWORK_SYNC_SERVER=wss://subduction.sync.inkandswitch.com npx pnpm@11 install --no-frozen-lockfile --config.dangerouslyAllowAllBuilds=true)"
|
|
15
25
|
]
|
|
16
26
|
}
|
|
17
27
|
}
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @chee/patchwork-bundles
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
npm install @chee/patchwork-bundles
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// vite.config.ts
|
|
9
|
+
import patchworkBundles from "@chee/patchwork-bundles/vite"
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
plugins: [patchworkBundles()],
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// esbuild
|
|
18
|
+
import patchworkBundles from "@chee/patchwork-bundles/esbuild"
|
|
19
|
+
|
|
20
|
+
await esbuild.build({
|
|
21
|
+
plugins: [patchworkBundles()],
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import patchworkBundles from "@chee/patchwork-bundles/rollup"
|
|
27
|
+
import patchworkBundles from "@chee/patchwork-bundles/webpack"
|
|
28
|
+
import patchworkBundles from "@chee/patchwork-bundles/rspack"
|
|
29
|
+
import patchworkBundles from "@chee/patchwork-bundles/farm"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
patchworkBundles({
|
|
34
|
+
packageJsonPath: "./package.json",
|
|
35
|
+
rewrite: {
|
|
36
|
+
automerge: "patchwork", // | "bundle"
|
|
37
|
+
npm: "bundle", // | "esm.sh"
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chee/patchwork-bundles",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Ink & Switch",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,14 +35,16 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
38
|
+
"@automerge/automerge-repo": "2.6.0-subduction.40",
|
|
39
|
+
"@inkandswitch/patchwork-bootloader": "*",
|
|
40
|
+
"@inkandswitch/patchwork-filesystem": "*",
|
|
41
|
+
"es-module-lexer": "^2.3.0",
|
|
39
42
|
"resolve.exports": "^2.0.3",
|
|
40
|
-
"unplugin": "^2.3.0"
|
|
41
|
-
"@inkandswitch/patchwork-bootloader": "^0.0.5",
|
|
42
|
-
"@inkandswitch/patchwork-filesystem": "^0.0.4"
|
|
43
|
+
"unplugin": "^2.3.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"esbuild": "^0.23.1",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
46
48
|
"vite": "^7.1.9",
|
|
47
49
|
"vitest": "^2.1.8"
|
|
48
50
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
overrides:
|
|
2
|
+
"@automerge/automerge": "catalog:"
|
|
3
|
+
"@automerge/automerge-repo": "catalog:"
|
|
4
|
+
"@automerge/automerge-repo-keyhive": "catalog:"
|
|
5
|
+
"@automerge/automerge-repo-network-messagechannel": "catalog:"
|
|
6
|
+
"@automerge/automerge-repo-network-websocket": "catalog:"
|
|
7
|
+
"@automerge/automerge-repo-react-hooks": "catalog:"
|
|
8
|
+
"@automerge/automerge-repo-solid-primitives": "catalog:"
|
|
9
|
+
"@automerge/automerge-repo-storage-indexeddb": "catalog:"
|
|
10
|
+
"@automerge/automerge-subduction": "catalog:"
|
|
11
|
+
"@automerge/react": "catalog:"
|
|
12
|
+
"@automerge/vanillajs": "catalog:"
|
|
13
|
+
"@keyhive/keyhive": "catalog:"
|
|
14
|
+
|
|
15
|
+
minimumReleaseAgeExclude:
|
|
16
|
+
- '@automerge/*'
|
|
17
|
+
- '@inkandswitch/*'
|
|
18
|
+
- '@chee/*'
|
|
19
|
+
|
|
20
|
+
catalog:
|
|
21
|
+
"@automerge/automerge": 3.3.0-fragments.2
|
|
22
|
+
"@automerge/automerge-repo": 2.6.0-subduction.40
|
|
23
|
+
"@automerge/automerge-repo-keyhive": 0.3.0-alpha.sub.8b
|
|
24
|
+
"@automerge/automerge-repo-network-messagechannel": 2.6.0-subduction.40
|
|
25
|
+
"@automerge/automerge-repo-network-websocket": 2.6.0-subduction.40
|
|
26
|
+
"@automerge/automerge-repo-react-hooks": 2.6.0-subduction.40
|
|
27
|
+
"@automerge/automerge-repo-solid-primitives": 2.6.0-subduction.40
|
|
28
|
+
"@automerge/automerge-repo-storage-indexeddb": 2.6.0-subduction.40
|
|
29
|
+
"@automerge/automerge-repo-storage-nodefs": 2.6.0-subduction.40
|
|
30
|
+
"@automerge/automerge-subduction": 0.16.0
|
|
31
|
+
"@automerge/react": 2.6.0-subduction.40
|
|
32
|
+
"@automerge/vanillajs": 2.6.0-subduction.40
|
|
33
|
+
"@keyhive/keyhive": 0.1.0-alpha.5
|
|
34
|
+
"@types/react": 18.3.1
|
|
35
|
+
"@types/react-dom": 18.3.1
|
|
36
|
+
react: 18.3.1
|
|
37
|
+
react-dom: 18.3.1
|
|
38
|
+
|
|
39
|
+
allowBuilds:
|
|
40
|
+
cbor-extract: true
|
|
41
|
+
esbuild: true
|
package/src/index.ts
CHANGED
|
@@ -5,12 +5,22 @@ import {
|
|
|
5
5
|
parseBareSpecifier,
|
|
6
6
|
isBareSpecifier,
|
|
7
7
|
resolveDepEntryPoint,
|
|
8
|
+
collectModuleExports,
|
|
9
|
+
ensureLexer,
|
|
8
10
|
} from "./utils.js";
|
|
9
11
|
import { getImportableUrlFromAutomergeUrl } from "@inkandswitch/patchwork-filesystem";
|
|
10
12
|
import externals from "@inkandswitch/patchwork-bootloader/externals";
|
|
11
13
|
import type { AutomergeUrl } from "@automerge/automerge-repo";
|
|
12
14
|
import { resolveOptions, type PatchworkBundleOptions } from "./types.js";
|
|
13
15
|
|
|
16
|
+
// Prefix marking a virtual module we generate for
|
|
17
|
+
// `rewrite.automerge: "patchwork:cross-origin"`. The rest of the id is the
|
|
18
|
+
// original bare specifier. The leading NUL keeps other tooling from treating it
|
|
19
|
+
// as a real file path.
|
|
20
|
+
const CROSS_ORIGIN_PREFIX = "\0patchwork-bundle-cross-origin:";
|
|
21
|
+
|
|
22
|
+
const VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
23
|
+
|
|
14
24
|
export const patchworkBundles = createUnplugin(
|
|
15
25
|
(rawOptions?: PatchworkBundleOptions) => {
|
|
16
26
|
const options = resolveOptions(rawOptions);
|
|
@@ -20,13 +30,19 @@ export const patchworkBundles = createUnplugin(
|
|
|
20
30
|
name: "patchwork-bundles",
|
|
21
31
|
enforce: "pre",
|
|
22
32
|
|
|
23
|
-
buildStart() {
|
|
33
|
+
async buildStart() {
|
|
24
34
|
const pkgPath = resolve(process.cwd(), options.packageJsonPath);
|
|
25
35
|
const pkgJson = readPackageJson(pkgPath);
|
|
26
36
|
deps = pkgJson.dependencies ?? {};
|
|
37
|
+
if (options.rewrite.automerge === "patchwork:cross-origin") {
|
|
38
|
+
await ensureLexer();
|
|
39
|
+
}
|
|
27
40
|
},
|
|
28
41
|
|
|
29
42
|
resolveId(id: string) {
|
|
43
|
+
// Virtual modules we emitted below are handled in load().
|
|
44
|
+
if (id.startsWith(CROSS_ORIGIN_PREFIX)) return id;
|
|
45
|
+
|
|
30
46
|
if (!isBareSpecifier(id)) return undefined;
|
|
31
47
|
|
|
32
48
|
// Bootloader externals — always external (served via importmap)
|
|
@@ -53,6 +69,13 @@ export const patchworkBundles = createUnplugin(
|
|
|
53
69
|
);
|
|
54
70
|
return { id: url, external: true };
|
|
55
71
|
}
|
|
72
|
+
// Cross-origin mode: defer URL construction to the browser so the
|
|
73
|
+
// service-worker path resolves against the *running document's*
|
|
74
|
+
// origin, not the (possibly cross-origin) module's. Emit a virtual
|
|
75
|
+
// re-export module; the code is generated in load().
|
|
76
|
+
if (options.rewrite.automerge === "patchwork:cross-origin") {
|
|
77
|
+
return CROSS_ORIGIN_PREFIX + id;
|
|
78
|
+
}
|
|
56
79
|
return undefined;
|
|
57
80
|
}
|
|
58
81
|
|
|
@@ -68,6 +91,40 @@ export const patchworkBundles = createUnplugin(
|
|
|
68
91
|
|
|
69
92
|
return undefined;
|
|
70
93
|
},
|
|
94
|
+
|
|
95
|
+
load(id: string) {
|
|
96
|
+
if (!id.startsWith(CROSS_ORIGIN_PREFIX)) return undefined;
|
|
97
|
+
|
|
98
|
+
const spec = id.slice(CROSS_ORIGIN_PREFIX.length);
|
|
99
|
+
const { pkgName, subpath } = parseBareSpecifier(spec);
|
|
100
|
+
const version = deps[pkgName] as AutomergeUrl;
|
|
101
|
+
const entryPoint = resolveDepEntryPoint(pkgName, subpath);
|
|
102
|
+
|
|
103
|
+
// Enumerate the dep's named exports so the virtual module can re-export
|
|
104
|
+
// the same shape. Read the installed copy in node_modules — assumed to
|
|
105
|
+
// match the automerge doc the service worker serves.
|
|
106
|
+
const entryFile = resolve(
|
|
107
|
+
process.cwd(),
|
|
108
|
+
"node_modules",
|
|
109
|
+
pkgName,
|
|
110
|
+
entryPoint
|
|
111
|
+
);
|
|
112
|
+
const exportNames = collectModuleExports(entryFile);
|
|
113
|
+
const hasDefault = exportNames.delete("default");
|
|
114
|
+
const named = [...exportNames].filter((n) => VALID_IDENT.test(n));
|
|
115
|
+
|
|
116
|
+
const lines = [
|
|
117
|
+
`import { getImportableUrlFromAutomergeUrl as __pwResolve } from "@inkandswitch/patchwork-filesystem";`,
|
|
118
|
+
`const __pwUrl = __pwResolve(${JSON.stringify(version)}, ${JSON.stringify(entryPoint)});`,
|
|
119
|
+
`const __pwMod = await import(/* @vite-ignore */ __pwUrl);`,
|
|
120
|
+
...named.map(
|
|
121
|
+
(n) => `export const ${n} = __pwMod[${JSON.stringify(n)}];`
|
|
122
|
+
),
|
|
123
|
+
];
|
|
124
|
+
if (hasDefault) lines.push(`export default __pwMod.default;`);
|
|
125
|
+
|
|
126
|
+
return { code: lines.join("\n"), moduleSideEffects: true };
|
|
127
|
+
},
|
|
71
128
|
};
|
|
72
129
|
}
|
|
73
130
|
);
|
package/src/types.ts
CHANGED
|
@@ -5,12 +5,23 @@ export interface PatchworkBundleOptions {
|
|
|
5
5
|
rewrite?: {
|
|
6
6
|
/**
|
|
7
7
|
* How to handle dependencies with `automerge:` version specifiers.
|
|
8
|
-
* - `"patchwork"` (default): rewrite bare imports to
|
|
9
|
-
* (`/{encodeURIComponent("automerge:docid")}/resolved/entry.js`),
|
|
8
|
+
* - `"patchwork"` (default): rewrite bare imports to *root-relative*
|
|
9
|
+
* service-worker URLs (`/{encodeURIComponent("automerge:docid")}/resolved/entry.js`),
|
|
10
10
|
* resolving through the dep's package.json exports with the `"patchwork"` condition.
|
|
11
|
+
* The leading `/` resolves against the *importing module's* origin, so this
|
|
12
|
+
* only works when the tool is served same-origin as the patchwork service
|
|
13
|
+
* worker. A tool served from a different origin (e.g. a separate static
|
|
14
|
+
* host) will 404 on these imports.
|
|
15
|
+
* - `"patchwork:cross-origin"`: rewrite bare imports to a virtual module
|
|
16
|
+
* that resolves the URL *at runtime* via `getImportableUrlFromAutomergeUrl`
|
|
17
|
+
* and dynamically imports it, re-exporting the dep's named exports
|
|
18
|
+
* (top-level await). This resolves against the running document's origin
|
|
19
|
+
* instead of the module's, so it works even when the tool is loaded
|
|
20
|
+
* cross-origin from where the service worker lives. Requires a build
|
|
21
|
+
* target that supports top-level await (e.g. `es2022`/`esnext`).
|
|
11
22
|
* - `"bundle"`: include the dependency code in the bundle normally.
|
|
12
23
|
*/
|
|
13
|
-
automerge?: "patchwork" | "bundle";
|
|
24
|
+
automerge?: "patchwork" | "patchwork:cross-origin" | "bundle";
|
|
14
25
|
|
|
15
26
|
/**
|
|
16
27
|
* How to handle npm dependencies (deps that aren't `automerge:` URLs
|
|
@@ -25,7 +36,7 @@ export interface PatchworkBundleOptions {
|
|
|
25
36
|
export interface ResolvedOptions {
|
|
26
37
|
packageJsonPath: string;
|
|
27
38
|
rewrite: {
|
|
28
|
-
automerge: "patchwork" | "bundle";
|
|
39
|
+
automerge: "patchwork" | "patchwork:cross-origin" | "bundle";
|
|
29
40
|
npm: "esm.sh" | "bundle";
|
|
30
41
|
};
|
|
31
42
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolve as resolvePath } from "node:path";
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath, dirname } from "node:path";
|
|
3
3
|
import { resolve as resolveExports } from "resolve.exports";
|
|
4
|
+
import { init as initLexer, parse as parseModule } from "es-module-lexer";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Read and parse a package.json file.
|
|
@@ -94,3 +95,72 @@ export function resolveDepEntryPoint(
|
|
|
94
95
|
|
|
95
96
|
return subpath;
|
|
96
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Enumerate the *named* exports of an ES module file, following relative
|
|
101
|
+
* `export * from "./x"` re-exports one dependency at a time. Used to generate a
|
|
102
|
+
* runtime-resolved virtual module that re-exports the same names as the real
|
|
103
|
+
* module. `"default"` is included in the returned set if the module has a
|
|
104
|
+
* default export.
|
|
105
|
+
*
|
|
106
|
+
* es-module-lexer must be initialised first (see {@link ensureLexer}).
|
|
107
|
+
*/
|
|
108
|
+
export function collectModuleExports(
|
|
109
|
+
entryPath: string,
|
|
110
|
+
seen: Set<string> = new Set()
|
|
111
|
+
): Set<string> {
|
|
112
|
+
const names = new Set<string>();
|
|
113
|
+
if (seen.has(entryPath) || !existsSync(entryPath)) return names;
|
|
114
|
+
seen.add(entryPath);
|
|
115
|
+
|
|
116
|
+
let source: string;
|
|
117
|
+
try {
|
|
118
|
+
source = readFileSync(entryPath, "utf-8");
|
|
119
|
+
} catch {
|
|
120
|
+
return names;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const [imports, exports] = parseModule(source);
|
|
124
|
+
for (const e of exports) {
|
|
125
|
+
if (e.n) names.add(e.n);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// `export * from "./rel"` contributes names that aren't listed inline. Those
|
|
129
|
+
// re-exports show up in `imports`; detect them by the statement text and
|
|
130
|
+
// recurse into relative targets.
|
|
131
|
+
for (const imp of imports) {
|
|
132
|
+
const spec = imp.n;
|
|
133
|
+
if (!spec || !spec.startsWith(".")) continue;
|
|
134
|
+
const stmt = source.slice(imp.ss, imp.se);
|
|
135
|
+
if (!/^export\s*\*/.test(stmt)) continue;
|
|
136
|
+
const target = resolveRelativeModule(dirname(entryPath), spec);
|
|
137
|
+
if (!target) continue;
|
|
138
|
+
for (const n of collectModuleExports(target, seen)) {
|
|
139
|
+
if (n !== "default") names.add(n); // `export *` never re-exports default
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return names;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve a relative module specifier to an existing file, trying the literal
|
|
148
|
+
* path and common JS extensions / index files.
|
|
149
|
+
*/
|
|
150
|
+
function resolveRelativeModule(fromDir: string, spec: string): string | null {
|
|
151
|
+
const base = resolvePath(fromDir, spec);
|
|
152
|
+
const candidates = [
|
|
153
|
+
base,
|
|
154
|
+
`${base}.js`,
|
|
155
|
+
`${base}.mjs`,
|
|
156
|
+
resolvePath(base, "index.js"),
|
|
157
|
+
resolvePath(base, "index.mjs"),
|
|
158
|
+
];
|
|
159
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let lexerReady: Promise<void> | undefined;
|
|
163
|
+
/** Initialise es-module-lexer's WASM once; safe to await repeatedly. */
|
|
164
|
+
export function ensureLexer(): Promise<void> {
|
|
165
|
+
return (lexerReady ??= initLexer);
|
|
166
|
+
}
|
package/test/integration.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import { build as viteBuild } from "vite";
|
|
7
7
|
import * as esbuild from "esbuild";
|
|
8
8
|
|
|
9
|
-
const AUTOMERGE_URL = "automerge:
|
|
9
|
+
const AUTOMERGE_URL = "automerge:6iXwddwF9cwrjmM5yqp2xUENxUY";
|
|
10
10
|
const ENCODED_URL = encodeURIComponent(AUTOMERGE_URL);
|
|
11
11
|
|
|
12
12
|
describe("patchwork-bundles integration", () => {
|
|
@@ -71,7 +71,7 @@ describe("patchwork-bundles integration", () => {
|
|
|
71
71
|
"packages:",
|
|
72
72
|
' - "packages/*"',
|
|
73
73
|
"configDependencies:",
|
|
74
|
-
' pnpm-plugin-patchwork: "0.
|
|
74
|
+
' pnpm-plugin-patchwork: "0.4.0"',
|
|
75
75
|
"",
|
|
76
76
|
].join("\n")
|
|
77
77
|
);
|
|
@@ -89,15 +89,21 @@ describe("patchwork-bundles integration", () => {
|
|
|
89
89
|
([k]) => !k.startsWith("npm_")
|
|
90
90
|
)
|
|
91
91
|
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
// strictDepBuilds=false: the automerge stack pulls in cbor-extract, whose
|
|
93
|
+
// native build script is ignored. Without this, pnpm 11 exits non-zero
|
|
94
|
+
// (ERR_PNPM_IGNORED_BUILDS) even though the install otherwise succeeds.
|
|
95
|
+
execSync(
|
|
96
|
+
"npx pnpm@11 install --no-frozen-lockfile --config.strictDepBuilds=false",
|
|
97
|
+
{
|
|
98
|
+
cwd: tmpDir,
|
|
99
|
+
stdio: "pipe",
|
|
100
|
+
timeout: 120_000,
|
|
101
|
+
env: {
|
|
102
|
+
...cleanEnv,
|
|
103
|
+
PATCHWORK_SYNC_SERVER: "wss://subduction.sync.inkandswitch.com",
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
);
|
|
101
107
|
|
|
102
108
|
// Phase 2: write source file
|
|
103
109
|
const srcDir = path.join(tmpDir, "src");
|
|
@@ -127,10 +133,10 @@ describe("patchwork-bundles integration", () => {
|
|
|
127
133
|
"node_modules/my-sideboard-tool/package.json"
|
|
128
134
|
);
|
|
129
135
|
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
130
|
-
expect(pkg.name).toBe("@
|
|
136
|
+
expect(pkg.name).toBe("@patchwork/chat");
|
|
131
137
|
});
|
|
132
138
|
|
|
133
|
-
it("has an exports field with a
|
|
139
|
+
it("has an exports field with a resolvable root entry", async () => {
|
|
134
140
|
const pkgPath = path.join(
|
|
135
141
|
tmpDir,
|
|
136
142
|
"node_modules/my-sideboard-tool/package.json"
|
|
@@ -138,7 +144,6 @@ describe("patchwork-bundles integration", () => {
|
|
|
138
144
|
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
139
145
|
expect(pkg.exports).toBeDefined();
|
|
140
146
|
expect(pkg.exports["."]).toBeDefined();
|
|
141
|
-
expect(pkg.exports["."].patchwork).toBeDefined();
|
|
142
147
|
});
|
|
143
148
|
|
|
144
149
|
it("contains actual dist files from the automerge document", async () => {
|
|
@@ -240,4 +245,61 @@ describe("patchwork-bundles integration", () => {
|
|
|
240
245
|
expect(output).not.toContain("my-sideboard-tool");
|
|
241
246
|
});
|
|
242
247
|
});
|
|
248
|
+
|
|
249
|
+
describe("vite build — patchwork:cross-origin mode", () => {
|
|
250
|
+
let output: string;
|
|
251
|
+
|
|
252
|
+
beforeAll(async () => {
|
|
253
|
+
const pluginPath = path.resolve(originalCwd, "dist/vite.js");
|
|
254
|
+
const { default: vitePlugin } = await import(pluginPath);
|
|
255
|
+
|
|
256
|
+
await viteBuild({
|
|
257
|
+
root: tmpDir,
|
|
258
|
+
logLevel: "silent",
|
|
259
|
+
build: {
|
|
260
|
+
lib: {
|
|
261
|
+
entry: path.join(tmpDir, "src/index.ts"),
|
|
262
|
+
formats: ["es"],
|
|
263
|
+
fileName: "index",
|
|
264
|
+
},
|
|
265
|
+
outDir: path.join(tmpDir, "dist-vite-xorigin"),
|
|
266
|
+
write: true,
|
|
267
|
+
minify: false,
|
|
268
|
+
// top-level await in the generated virtual module
|
|
269
|
+
target: "esnext",
|
|
270
|
+
},
|
|
271
|
+
plugins: [vitePlugin({ rewrite: { automerge: "patchwork:cross-origin" } })],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const mjs = path.join(tmpDir, "dist-vite-xorigin/index.mjs");
|
|
275
|
+
const js = path.join(tmpDir, "dist-vite-xorigin/index.js");
|
|
276
|
+
output = await fs.readFile(
|
|
277
|
+
await fs.access(mjs).then(() => mjs, () => js),
|
|
278
|
+
"utf-8"
|
|
279
|
+
);
|
|
280
|
+
}, 60_000);
|
|
281
|
+
|
|
282
|
+
it("resolves the service-worker URL at runtime, not at build time", () => {
|
|
283
|
+
// The automerge URL is passed to the resolver as a literal string arg…
|
|
284
|
+
expect(output).toContain(AUTOMERGE_URL);
|
|
285
|
+
expect(output).toContain("getImportableUrlFromAutomergeUrl");
|
|
286
|
+
expect(output).toMatch(/await import\(/);
|
|
287
|
+
// …and is NOT baked as a root-relative `from "/<encoded>/…"` import.
|
|
288
|
+
expect(output).not.toMatch(new RegExp(`from ["']/${ENCODED_URL}/`));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("imports the runtime resolver from patchwork-filesystem (external)", () => {
|
|
292
|
+
expect(output).toMatch(
|
|
293
|
+
/from ["']@inkandswitch\/patchwork-filesystem["']/
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("still externalizes solid-js", () => {
|
|
298
|
+
expect(output).toMatch(/from ["']solid-js["']/);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("does not contain bundled automerge dep code", () => {
|
|
302
|
+
expect(output).not.toContain("my-sideboard-tool");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
243
305
|
});
|
package/tsconfig.json
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
2
3
|
"compilerOptions": {
|
|
3
|
-
|
|
4
|
+
// File Layout
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
|
|
8
|
+
// Environment Settings
|
|
9
|
+
// See also https://aka.ms/tsconfig/module
|
|
4
10
|
"module": "esnext",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
11
|
+
"target": "esnext",
|
|
12
|
+
"types": [],
|
|
13
|
+
|
|
14
|
+
// Other Outputs
|
|
15
|
+
"sourceMap": true,
|
|
7
16
|
"declaration": true,
|
|
17
|
+
"declarationMap": true,
|
|
18
|
+
|
|
19
|
+
// Stricter Typechecking Options
|
|
20
|
+
"noUncheckedIndexedAccess": true,
|
|
21
|
+
"exactOptionalPropertyTypes": true,
|
|
22
|
+
|
|
23
|
+
// Style Options
|
|
24
|
+
// "noImplicitReturns": true,
|
|
25
|
+
// "noImplicitOverride": true,
|
|
26
|
+
// "noUnusedLocals": true,
|
|
27
|
+
// "noUnusedParameters": true,
|
|
28
|
+
// "noFallthroughCasesInSwitch": true,
|
|
29
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
30
|
+
|
|
31
|
+
// Recommended Options
|
|
8
32
|
"strict": true,
|
|
33
|
+
"verbatimModuleSyntax": true,
|
|
34
|
+
"isolatedModules": true,
|
|
35
|
+
"noUncheckedSideEffectImports": true,
|
|
36
|
+
"moduleDetection": "force",
|
|
9
37
|
"skipLibCheck": true,
|
|
10
|
-
"isolatedModules": true
|
|
11
38
|
},
|
|
12
39
|
"include": ["src"]
|
|
13
40
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["vitest.config.ts"],"names":[],"mappings":";AAEA,wBAOG"}
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["vitest.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC;IAC1B,IAAI,EAAE;QACJ,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,MAAM;QACnB,OAAO,EAAE,CAAC,0BAA0B,CAAC;QACrC,WAAW,EAAE,OAAO;KACrB;CACF,CAAC,CAAC"}
|
package/dist/esbuild.d.ts
DELETED
package/dist/esbuild.js
DELETED
package/dist/farm.d.ts
DELETED
package/dist/farm.js
DELETED
package/dist/index.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { type PatchworkBundleOptions } from "./types.js";
|
|
2
|
-
export declare const patchworkBundles: import("unplugin").UnpluginInstance<PatchworkBundleOptions | undefined, boolean>;
|
|
3
|
-
export default patchworkBundles;
|
|
4
|
-
export type { PatchworkBundleOptions } from "./types.js";
|
package/dist/index.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { createUnplugin } from "unplugin";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { readPackageJson, parseBareSpecifier, isBareSpecifier, resolveDepEntryPoint, } from "./utils.js";
|
|
4
|
-
import { getImportableUrlFromAutomergeUrl } from "@inkandswitch/patchwork-filesystem";
|
|
5
|
-
import externals from "@inkandswitch/patchwork-bootloader/externals";
|
|
6
|
-
import { resolveOptions } from "./types.js";
|
|
7
|
-
export const patchworkBundles = createUnplugin((rawOptions) => {
|
|
8
|
-
const options = resolveOptions(rawOptions);
|
|
9
|
-
let deps;
|
|
10
|
-
return {
|
|
11
|
-
name: "patchwork-bundles",
|
|
12
|
-
enforce: "pre",
|
|
13
|
-
buildStart() {
|
|
14
|
-
const pkgPath = resolve(process.cwd(), options.packageJsonPath);
|
|
15
|
-
const pkgJson = readPackageJson(pkgPath);
|
|
16
|
-
deps = pkgJson.dependencies ?? {};
|
|
17
|
-
},
|
|
18
|
-
resolveId(id) {
|
|
19
|
-
if (!isBareSpecifier(id))
|
|
20
|
-
return undefined;
|
|
21
|
-
// Bootloader externals — always external (served via importmap)
|
|
22
|
-
if (externals.includes(id)) {
|
|
23
|
-
return { id, external: true };
|
|
24
|
-
}
|
|
25
|
-
const { pkgName, subpath } = parseBareSpecifier(id);
|
|
26
|
-
if (pkgName !== id && externals.includes(pkgName)) {
|
|
27
|
-
return { id, external: true };
|
|
28
|
-
}
|
|
29
|
-
const version = deps[pkgName];
|
|
30
|
-
if (!version)
|
|
31
|
-
return undefined;
|
|
32
|
-
// Automerge deps — rewrite to service worker URLs
|
|
33
|
-
if (version.startsWith("automerge:")) {
|
|
34
|
-
if (options.rewrite.automerge === "patchwork") {
|
|
35
|
-
const entryPoint = resolveDepEntryPoint(pkgName, subpath);
|
|
36
|
-
const url = getImportableUrlFromAutomergeUrl(version, entryPoint);
|
|
37
|
-
return { id: url, external: true };
|
|
38
|
-
}
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
// npm fallback — optionally rewrite to esm.sh
|
|
42
|
-
if (options.rewrite.npm === "esm.sh") {
|
|
43
|
-
const cleanVersion = version.replace(/^[\^~>=<\s]+/, "");
|
|
44
|
-
const esmId = subpath === "." ? pkgName : id;
|
|
45
|
-
return {
|
|
46
|
-
id: `https://esm.sh/${esmId}@${cleanVersion}`,
|
|
47
|
-
external: true,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return undefined;
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
export default patchworkBundles;
|
package/dist/rollup.d.ts
DELETED
package/dist/rollup.js
DELETED
package/dist/rspack.d.ts
DELETED
package/dist/rspack.js
DELETED
package/dist/types.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export interface PatchworkBundleOptions {
|
|
2
|
-
/** Path to the project's package.json. Default: "./package.json" relative to cwd */
|
|
3
|
-
packageJsonPath?: string;
|
|
4
|
-
rewrite?: {
|
|
5
|
-
/**
|
|
6
|
-
* How to handle dependencies with `automerge:` version specifiers.
|
|
7
|
-
* - `"patchwork"` (default): rewrite bare imports to service-worker URLs
|
|
8
|
-
* (`/{encodeURIComponent("automerge:docid")}/resolved/entry.js`),
|
|
9
|
-
* resolving through the dep's package.json exports with the `"patchwork"` condition.
|
|
10
|
-
* - `"bundle"`: include the dependency code in the bundle normally.
|
|
11
|
-
*/
|
|
12
|
-
automerge?: "patchwork" | "bundle";
|
|
13
|
-
/**
|
|
14
|
-
* How to handle npm dependencies (deps that aren't `automerge:` URLs
|
|
15
|
-
* and aren't in the bootloader externals list).
|
|
16
|
-
* - `"bundle"` (default): bundle them normally.
|
|
17
|
-
* - `"esm.sh"`: rewrite to `https://esm.sh/<pkg>@<version>` external URLs.
|
|
18
|
-
*/
|
|
19
|
-
npm?: "esm.sh" | "bundle";
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
export interface ResolvedOptions {
|
|
23
|
-
packageJsonPath: string;
|
|
24
|
-
rewrite: {
|
|
25
|
-
automerge: "patchwork" | "bundle";
|
|
26
|
-
npm: "esm.sh" | "bundle";
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
export declare function resolveOptions(raw?: PatchworkBundleOptions): ResolvedOptions;
|
package/dist/types.js
DELETED
package/dist/utils.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read and parse a package.json file.
|
|
3
|
-
*/
|
|
4
|
-
export declare function readPackageJson(path: string): Record<string, any>;
|
|
5
|
-
/**
|
|
6
|
-
* Parse a bare import specifier into package name and subpath.
|
|
7
|
-
*
|
|
8
|
-
* - `"abc"` → `{ pkgName: "abc", subpath: "." }`
|
|
9
|
-
* - `"abc/utils"` → `{ pkgName: "abc", subpath: "./utils" }`
|
|
10
|
-
* - `"@scope/pkg"` → `{ pkgName: "@scope/pkg", subpath: "." }`
|
|
11
|
-
* - `"@scope/pkg/utils"` → `{ pkgName: "@scope/pkg", subpath: "./utils" }`
|
|
12
|
-
*/
|
|
13
|
-
export declare function parseBareSpecifier(id: string): {
|
|
14
|
-
pkgName: string;
|
|
15
|
-
subpath: string;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Returns true if the specifier is a bare import (not relative, absolute,
|
|
19
|
-
* or a protocol URL).
|
|
20
|
-
*/
|
|
21
|
-
export declare function isBareSpecifier(id: string): boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Resolve a dependency's entry point by reading its package.json from
|
|
24
|
-
* `node_modules/` and resolving through its `exports` field.
|
|
25
|
-
*
|
|
26
|
-
* Returns the resolved path relative to the package root (e.g. `"./dist/index.js"`),
|
|
27
|
-
* or the subpath itself if resolution fails.
|
|
28
|
-
*/
|
|
29
|
-
export declare function resolveDepEntryPoint(pkgName: string, subpath?: string, conditions?: string[]): string;
|
package/dist/utils.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolve as resolvePath } from "node:path";
|
|
3
|
-
import { resolve as resolveExports } from "resolve.exports";
|
|
4
|
-
/**
|
|
5
|
-
* Read and parse a package.json file.
|
|
6
|
-
*/
|
|
7
|
-
export function readPackageJson(path) {
|
|
8
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Parse a bare import specifier into package name and subpath.
|
|
12
|
-
*
|
|
13
|
-
* - `"abc"` → `{ pkgName: "abc", subpath: "." }`
|
|
14
|
-
* - `"abc/utils"` → `{ pkgName: "abc", subpath: "./utils" }`
|
|
15
|
-
* - `"@scope/pkg"` → `{ pkgName: "@scope/pkg", subpath: "." }`
|
|
16
|
-
* - `"@scope/pkg/utils"` → `{ pkgName: "@scope/pkg", subpath: "./utils" }`
|
|
17
|
-
*/
|
|
18
|
-
export function parseBareSpecifier(id) {
|
|
19
|
-
const firstSlash = id.indexOf("/");
|
|
20
|
-
const isScoped = id.startsWith("@");
|
|
21
|
-
if (isScoped) {
|
|
22
|
-
const secondSlash = id.indexOf("/", firstSlash + 1);
|
|
23
|
-
if (secondSlash === -1) {
|
|
24
|
-
return { pkgName: id, subpath: "." };
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
pkgName: id.slice(0, secondSlash),
|
|
28
|
-
subpath: "./" + id.slice(secondSlash + 1),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
if (firstSlash === -1) {
|
|
32
|
-
return { pkgName: id, subpath: "." };
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
pkgName: id.slice(0, firstSlash),
|
|
36
|
-
subpath: "./" + id.slice(firstSlash + 1),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Returns true if the specifier is a bare import (not relative, absolute,
|
|
41
|
-
* or a protocol URL).
|
|
42
|
-
*/
|
|
43
|
-
export function isBareSpecifier(id) {
|
|
44
|
-
return (id.length > 0 &&
|
|
45
|
-
!id.startsWith(".") &&
|
|
46
|
-
!id.startsWith("/") &&
|
|
47
|
-
!id.includes(":"));
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Resolve a dependency's entry point by reading its package.json from
|
|
51
|
-
* `node_modules/` and resolving through its `exports` field.
|
|
52
|
-
*
|
|
53
|
-
* Returns the resolved path relative to the package root (e.g. `"./dist/index.js"`),
|
|
54
|
-
* or the subpath itself if resolution fails.
|
|
55
|
-
*/
|
|
56
|
-
export function resolveDepEntryPoint(pkgName, subpath = ".", conditions = ["patchwork", "browser", "import"]) {
|
|
57
|
-
try {
|
|
58
|
-
const depPkgJsonPath = resolvePath(process.cwd(), "node_modules", pkgName, "package.json");
|
|
59
|
-
const depPkgJson = readPackageJson(depPkgJsonPath);
|
|
60
|
-
const resolved = resolveExports(depPkgJson, subpath, { conditions });
|
|
61
|
-
if (resolved && resolved[0]) {
|
|
62
|
-
return resolved[0];
|
|
63
|
-
}
|
|
64
|
-
// Fallback to "main" for root export
|
|
65
|
-
if (subpath === "." && typeof depPkgJson.main === "string") {
|
|
66
|
-
return depPkgJson.main;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// If we can't read the dep's package.json, just pass through the subpath
|
|
71
|
-
}
|
|
72
|
-
return subpath;
|
|
73
|
-
}
|
package/dist/vite.d.ts
DELETED
package/dist/vite.js
DELETED
package/dist/webpack.d.ts
DELETED
package/dist/webpack.js
DELETED