@chee/patchwork-bundles 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +17 -0
- package/dist/esbuild.d.ts +2 -0
- package/dist/esbuild.js +2 -0
- package/dist/farm.d.ts +2 -0
- package/dist/farm.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +54 -0
- package/dist/rollup.d.ts +2 -0
- package/dist/rollup.js +2 -0
- package/dist/rspack.d.ts +2 -0
- package/dist/rspack.js +2 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.js +9 -0
- package/dist/utils.d.ts +29 -0
- package/dist/utils.js +73 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +2 -0
- package/dist/webpack.d.ts +2 -0
- package/dist/webpack.js +2 -0
- package/package.json +55 -0
- package/src/esbuild.ts +2 -0
- package/src/farm.ts +2 -0
- package/src/index.ts +76 -0
- package/src/rollup.ts +2 -0
- package/src/rspack.ts +2 -0
- package/src/types.ts +41 -0
- package/src/utils.ts +96 -0
- package/src/vite.ts +2 -0
- package/src/webpack.ts +2 -0
- package/test/integration.test.ts +243 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebFetch(domain:www.npmjs.com)",
|
|
5
|
+
"WebSearch",
|
|
6
|
+
"Bash(find:*)",
|
|
7
|
+
"WebFetch(domain:registry.npmjs.org)",
|
|
8
|
+
"Bash(pnpm --version:*)",
|
|
9
|
+
"Bash(npx pnpm@next-11:*)",
|
|
10
|
+
"Bash(npm view:*)",
|
|
11
|
+
"Bash(ls:*)",
|
|
12
|
+
"Bash(echo:*)",
|
|
13
|
+
"Bash(pnpm test:*)",
|
|
14
|
+
"Bash(node -e:*)"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/esbuild.js
ADDED
package/dist/farm.d.ts
ADDED
package/dist/farm.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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
ADDED
package/dist/rollup.js
ADDED
package/dist/rspack.d.ts
ADDED
package/dist/rspack.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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
ADDED
package/dist/vite.js
ADDED
package/dist/webpack.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chee/patchwork-bundles",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": "Ink & Switch",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./vite": {
|
|
13
|
+
"import": "./dist/vite.js",
|
|
14
|
+
"types": "./dist/vite.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./esbuild": {
|
|
17
|
+
"import": "./dist/esbuild.js",
|
|
18
|
+
"types": "./dist/esbuild.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./webpack": {
|
|
21
|
+
"import": "./dist/webpack.js",
|
|
22
|
+
"types": "./dist/webpack.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./rollup": {
|
|
25
|
+
"import": "./dist/rollup.js",
|
|
26
|
+
"types": "./dist/rollup.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./rspack": {
|
|
29
|
+
"import": "./dist/rspack.js",
|
|
30
|
+
"types": "./dist/rspack.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./farm": {
|
|
33
|
+
"import": "./dist/farm.js",
|
|
34
|
+
"types": "./dist/farm.d.ts"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@automerge/automerge-repo": "2.6.0-subduction.15",
|
|
39
|
+
"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
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"esbuild": "^0.23.1",
|
|
46
|
+
"vite": "^7.1.9",
|
|
47
|
+
"vitest": "^2.1.8"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc",
|
|
51
|
+
"dev": "tsc -w --preserveWatchOutput",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/esbuild.ts
ADDED
package/src/farm.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createUnplugin } from "unplugin";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
readPackageJson,
|
|
5
|
+
parseBareSpecifier,
|
|
6
|
+
isBareSpecifier,
|
|
7
|
+
resolveDepEntryPoint,
|
|
8
|
+
} from "./utils.js";
|
|
9
|
+
import { getImportableUrlFromAutomergeUrl } from "@inkandswitch/patchwork-filesystem";
|
|
10
|
+
import externals from "@inkandswitch/patchwork-bootloader/externals";
|
|
11
|
+
import type { AutomergeUrl } from "@automerge/automerge-repo";
|
|
12
|
+
import { resolveOptions, type PatchworkBundleOptions } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export const patchworkBundles = createUnplugin(
|
|
15
|
+
(rawOptions?: PatchworkBundleOptions) => {
|
|
16
|
+
const options = resolveOptions(rawOptions);
|
|
17
|
+
let deps: Record<string, string>;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
name: "patchwork-bundles",
|
|
21
|
+
enforce: "pre",
|
|
22
|
+
|
|
23
|
+
buildStart() {
|
|
24
|
+
const pkgPath = resolve(process.cwd(), options.packageJsonPath);
|
|
25
|
+
const pkgJson = readPackageJson(pkgPath);
|
|
26
|
+
deps = pkgJson.dependencies ?? {};
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
resolveId(id: string) {
|
|
30
|
+
if (!isBareSpecifier(id)) return undefined;
|
|
31
|
+
|
|
32
|
+
// Bootloader externals — always external (served via importmap)
|
|
33
|
+
if (externals.includes(id)) {
|
|
34
|
+
return { id, external: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { pkgName, subpath } = parseBareSpecifier(id);
|
|
38
|
+
|
|
39
|
+
if (pkgName !== id && externals.includes(pkgName)) {
|
|
40
|
+
return { id, external: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const version = deps[pkgName];
|
|
44
|
+
if (!version) return undefined;
|
|
45
|
+
|
|
46
|
+
// Automerge deps — rewrite to service worker URLs
|
|
47
|
+
if (version.startsWith("automerge:")) {
|
|
48
|
+
if (options.rewrite.automerge === "patchwork") {
|
|
49
|
+
const entryPoint = resolveDepEntryPoint(pkgName, subpath);
|
|
50
|
+
const url = getImportableUrlFromAutomergeUrl(
|
|
51
|
+
version as AutomergeUrl,
|
|
52
|
+
entryPoint
|
|
53
|
+
);
|
|
54
|
+
return { id: url, external: true };
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// npm fallback — optionally rewrite to esm.sh
|
|
60
|
+
if (options.rewrite.npm === "esm.sh") {
|
|
61
|
+
const cleanVersion = version.replace(/^[\^~>=<\s]+/, "");
|
|
62
|
+
const esmId = subpath === "." ? pkgName : id;
|
|
63
|
+
return {
|
|
64
|
+
id: `https://esm.sh/${esmId}@${cleanVersion}`,
|
|
65
|
+
external: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return undefined;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export default patchworkBundles;
|
|
76
|
+
export type { PatchworkBundleOptions } from "./types.js";
|
package/src/rollup.ts
ADDED
package/src/rspack.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface PatchworkBundleOptions {
|
|
2
|
+
/** Path to the project's package.json. Default: "./package.json" relative to cwd */
|
|
3
|
+
packageJsonPath?: string;
|
|
4
|
+
|
|
5
|
+
rewrite?: {
|
|
6
|
+
/**
|
|
7
|
+
* How to handle dependencies with `automerge:` version specifiers.
|
|
8
|
+
* - `"patchwork"` (default): rewrite bare imports to service-worker URLs
|
|
9
|
+
* (`/{encodeURIComponent("automerge:docid")}/resolved/entry.js`),
|
|
10
|
+
* resolving through the dep's package.json exports with the `"patchwork"` condition.
|
|
11
|
+
* - `"bundle"`: include the dependency code in the bundle normally.
|
|
12
|
+
*/
|
|
13
|
+
automerge?: "patchwork" | "bundle";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How to handle npm dependencies (deps that aren't `automerge:` URLs
|
|
17
|
+
* and aren't in the bootloader externals list).
|
|
18
|
+
* - `"bundle"` (default): bundle them normally.
|
|
19
|
+
* - `"esm.sh"`: rewrite to `https://esm.sh/<pkg>@<version>` external URLs.
|
|
20
|
+
*/
|
|
21
|
+
npm?: "esm.sh" | "bundle";
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResolvedOptions {
|
|
26
|
+
packageJsonPath: string;
|
|
27
|
+
rewrite: {
|
|
28
|
+
automerge: "patchwork" | "bundle";
|
|
29
|
+
npm: "esm.sh" | "bundle";
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveOptions(raw?: PatchworkBundleOptions): ResolvedOptions {
|
|
34
|
+
return {
|
|
35
|
+
packageJsonPath: raw?.packageJsonPath ?? "./package.json",
|
|
36
|
+
rewrite: {
|
|
37
|
+
automerge: raw?.rewrite?.automerge ?? "patchwork",
|
|
38
|
+
npm: raw?.rewrite?.npm ?? "bundle",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { resolve as resolveExports } from "resolve.exports";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read and parse a package.json file.
|
|
7
|
+
*/
|
|
8
|
+
export function readPackageJson(path: string): Record<string, any> {
|
|
9
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a bare import specifier into package name and subpath.
|
|
14
|
+
*
|
|
15
|
+
* - `"abc"` → `{ pkgName: "abc", subpath: "." }`
|
|
16
|
+
* - `"abc/utils"` → `{ pkgName: "abc", subpath: "./utils" }`
|
|
17
|
+
* - `"@scope/pkg"` → `{ pkgName: "@scope/pkg", subpath: "." }`
|
|
18
|
+
* - `"@scope/pkg/utils"` → `{ pkgName: "@scope/pkg", subpath: "./utils" }`
|
|
19
|
+
*/
|
|
20
|
+
export function parseBareSpecifier(id: string): {
|
|
21
|
+
pkgName: string;
|
|
22
|
+
subpath: string;
|
|
23
|
+
} {
|
|
24
|
+
const firstSlash = id.indexOf("/");
|
|
25
|
+
const isScoped = id.startsWith("@");
|
|
26
|
+
|
|
27
|
+
if (isScoped) {
|
|
28
|
+
const secondSlash = id.indexOf("/", firstSlash + 1);
|
|
29
|
+
if (secondSlash === -1) {
|
|
30
|
+
return { pkgName: id, subpath: "." };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
pkgName: id.slice(0, secondSlash),
|
|
34
|
+
subpath: "./" + id.slice(secondSlash + 1),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (firstSlash === -1) {
|
|
39
|
+
return { pkgName: id, subpath: "." };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
pkgName: id.slice(0, firstSlash),
|
|
44
|
+
subpath: "./" + id.slice(firstSlash + 1),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the specifier is a bare import (not relative, absolute,
|
|
50
|
+
* or a protocol URL).
|
|
51
|
+
*/
|
|
52
|
+
export function isBareSpecifier(id: string): boolean {
|
|
53
|
+
return (
|
|
54
|
+
id.length > 0 &&
|
|
55
|
+
!id.startsWith(".") &&
|
|
56
|
+
!id.startsWith("/") &&
|
|
57
|
+
!id.includes(":")
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a dependency's entry point by reading its package.json from
|
|
63
|
+
* `node_modules/` and resolving through its `exports` field.
|
|
64
|
+
*
|
|
65
|
+
* Returns the resolved path relative to the package root (e.g. `"./dist/index.js"`),
|
|
66
|
+
* or the subpath itself if resolution fails.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveDepEntryPoint(
|
|
69
|
+
pkgName: string,
|
|
70
|
+
subpath: string = ".",
|
|
71
|
+
conditions: string[] = ["patchwork", "browser", "import"]
|
|
72
|
+
): string {
|
|
73
|
+
try {
|
|
74
|
+
const depPkgJsonPath = resolvePath(
|
|
75
|
+
process.cwd(),
|
|
76
|
+
"node_modules",
|
|
77
|
+
pkgName,
|
|
78
|
+
"package.json"
|
|
79
|
+
);
|
|
80
|
+
const depPkgJson = readPackageJson(depPkgJsonPath);
|
|
81
|
+
|
|
82
|
+
const resolved = resolveExports(depPkgJson, subpath, { conditions });
|
|
83
|
+
if (resolved && resolved[0]) {
|
|
84
|
+
return resolved[0];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback to "main" for root export
|
|
88
|
+
if (subpath === "." && typeof depPkgJson.main === "string") {
|
|
89
|
+
return depPkgJson.main;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// If we can't read the dep's package.json, just pass through the subpath
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return subpath;
|
|
96
|
+
}
|
package/src/vite.ts
ADDED
package/src/webpack.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { build as viteBuild } from "vite";
|
|
7
|
+
import * as esbuild from "esbuild";
|
|
8
|
+
|
|
9
|
+
const AUTOMERGE_URL = "automerge:kFcrzeDmr5zXE1jShvxUPsoToAN";
|
|
10
|
+
const ENCODED_URL = encodeURIComponent(AUTOMERGE_URL);
|
|
11
|
+
|
|
12
|
+
describe("patchwork-bundles integration", () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
let originalCwd: string;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
originalCwd = process.cwd();
|
|
18
|
+
tmpDir = await fs.mkdtemp(
|
|
19
|
+
path.join(os.tmpdir(), "patchwork-bundles-test-")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Phase 1: set up project with pnpm-plugin-patchwork as a configDependency
|
|
23
|
+
await fs.writeFile(
|
|
24
|
+
path.join(tmpDir, "package.json"),
|
|
25
|
+
JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
name: "test-patchwork-bundles",
|
|
28
|
+
version: "0.0.0",
|
|
29
|
+
type: "module",
|
|
30
|
+
dependencies: {
|
|
31
|
+
"my-sideboard-tool": AUTOMERGE_URL,
|
|
32
|
+
"solid-js": "^1.9.9",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
null,
|
|
36
|
+
2
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// stub workspace packages that the automerge doc depends on via workspace:^
|
|
41
|
+
const bootloaderDir = path.join(tmpDir, "packages/patchwork-bootloader");
|
|
42
|
+
await fs.mkdir(bootloaderDir, { recursive: true });
|
|
43
|
+
await fs.writeFile(
|
|
44
|
+
path.join(bootloaderDir, "package.json"),
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
name: "@inkandswitch/patchwork-bootloader",
|
|
47
|
+
version: "0.0.1",
|
|
48
|
+
type: "module",
|
|
49
|
+
exports: { "./externals": { import: "./externals.js" } },
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
await fs.writeFile(
|
|
53
|
+
path.join(bootloaderDir, "externals.js"),
|
|
54
|
+
`const externals = [\n "solid-js",\n "solid-js/web",\n "solid-js/html",\n "solid-js/store",\n "solid-js/jsx-runtime",\n "solid-js/h"\n];\nexport default externals;\n`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const elementsDir = path.join(tmpDir, "packages/patchwork-elements");
|
|
58
|
+
await fs.mkdir(elementsDir, { recursive: true });
|
|
59
|
+
await fs.writeFile(
|
|
60
|
+
path.join(elementsDir, "package.json"),
|
|
61
|
+
JSON.stringify({
|
|
62
|
+
name: "@inkandswitch/patchwork-elements",
|
|
63
|
+
version: "0.0.1",
|
|
64
|
+
type: "module",
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await fs.writeFile(
|
|
69
|
+
path.join(tmpDir, "pnpm-workspace.yaml"),
|
|
70
|
+
[
|
|
71
|
+
"packages:",
|
|
72
|
+
' - "packages/*"',
|
|
73
|
+
"configDependencies:",
|
|
74
|
+
' pnpm-plugin-patchwork: "0.1.0"',
|
|
75
|
+
"",
|
|
76
|
+
].join("\n")
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// .npmrc — avoid strict peer deps issues
|
|
80
|
+
await fs.writeFile(
|
|
81
|
+
path.join(tmpDir, ".npmrc"),
|
|
82
|
+
"strict-peer-dependencies=false\nauto-install-peers=true\n"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// install — plugin resolves automerge: deps automatically
|
|
86
|
+
// filter out parent pnpm's npm_config_* env vars to avoid contamination
|
|
87
|
+
const cleanEnv = Object.fromEntries(
|
|
88
|
+
Object.entries(process.env).filter(
|
|
89
|
+
([k]) => !k.startsWith("npm_")
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
execSync("npx pnpm@next-11 install --no-frozen-lockfile", {
|
|
93
|
+
cwd: tmpDir,
|
|
94
|
+
stdio: "pipe",
|
|
95
|
+
timeout: 120_000,
|
|
96
|
+
env: {
|
|
97
|
+
...cleanEnv,
|
|
98
|
+
PATCHWORK_SYNC_SERVER: "wss://sync3.automerge.org",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Phase 2: write source file
|
|
103
|
+
const srcDir = path.join(tmpDir, "src");
|
|
104
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
105
|
+
await fs.writeFile(
|
|
106
|
+
path.join(srcDir, "index.ts"),
|
|
107
|
+
[
|
|
108
|
+
`import { createSignal } from "solid-js";`,
|
|
109
|
+
`import * as sideboard from "my-sideboard-tool";`,
|
|
110
|
+
`console.log(createSignal, sideboard);`,
|
|
111
|
+
"",
|
|
112
|
+
].join("\n")
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
process.chdir(tmpDir);
|
|
116
|
+
}, 180_000);
|
|
117
|
+
|
|
118
|
+
afterAll(async () => {
|
|
119
|
+
process.chdir(originalCwd);
|
|
120
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("pnpm plugin installed my-sideboard-tool from automerge", () => {
|
|
124
|
+
it("has a package.json with the sideboard package name", async () => {
|
|
125
|
+
const pkgPath = path.join(
|
|
126
|
+
tmpDir,
|
|
127
|
+
"node_modules/my-sideboard-tool/package.json"
|
|
128
|
+
);
|
|
129
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
130
|
+
expect(pkg.name).toBe("@chee/patchwork-sideboard");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("has an exports field with a patchwork condition", async () => {
|
|
134
|
+
const pkgPath = path.join(
|
|
135
|
+
tmpDir,
|
|
136
|
+
"node_modules/my-sideboard-tool/package.json"
|
|
137
|
+
);
|
|
138
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
139
|
+
expect(pkg.exports).toBeDefined();
|
|
140
|
+
expect(pkg.exports["."]).toBeDefined();
|
|
141
|
+
expect(pkg.exports["."].patchwork).toBeDefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("contains actual dist files from the automerge document", async () => {
|
|
145
|
+
const distDir = path.join(tmpDir, "node_modules/my-sideboard-tool/dist");
|
|
146
|
+
const files = await fs.readdir(distDir);
|
|
147
|
+
expect(files.length).toBeGreaterThan(0);
|
|
148
|
+
// should have js output
|
|
149
|
+
expect(files.some((f) => f.endsWith(".js"))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("vite build", () => {
|
|
154
|
+
let output: string;
|
|
155
|
+
|
|
156
|
+
beforeAll(async () => {
|
|
157
|
+
// Determine what entry point the plugin will resolve to
|
|
158
|
+
const pluginPath = path.resolve(originalCwd, "dist/vite.js");
|
|
159
|
+
const { default: vitePlugin } = await import(pluginPath);
|
|
160
|
+
|
|
161
|
+
const result = await viteBuild({
|
|
162
|
+
root: tmpDir,
|
|
163
|
+
logLevel: "silent",
|
|
164
|
+
build: {
|
|
165
|
+
lib: {
|
|
166
|
+
entry: path.join(tmpDir, "src/index.ts"),
|
|
167
|
+
formats: ["es"],
|
|
168
|
+
fileName: "index",
|
|
169
|
+
},
|
|
170
|
+
outDir: path.join(tmpDir, "dist-vite"),
|
|
171
|
+
write: true,
|
|
172
|
+
minify: false,
|
|
173
|
+
},
|
|
174
|
+
plugins: [vitePlugin()],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// vite 7 outputs .js, older versions output .mjs
|
|
178
|
+
const mjs = path.join(tmpDir, "dist-vite/index.mjs");
|
|
179
|
+
const js = path.join(tmpDir, "dist-vite/index.js");
|
|
180
|
+
output = await fs.readFile(
|
|
181
|
+
await fs.access(mjs).then(() => mjs, () => js),
|
|
182
|
+
"utf-8"
|
|
183
|
+
);
|
|
184
|
+
}, 60_000);
|
|
185
|
+
|
|
186
|
+
it("rewrites automerge dep to service-worker URL", () => {
|
|
187
|
+
expect(output).toContain(ENCODED_URL);
|
|
188
|
+
// Should be an import from /<encoded-url>/...
|
|
189
|
+
expect(output).toMatch(new RegExp(`from ["']/${ENCODED_URL}/`));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("externalizes solid-js", () => {
|
|
193
|
+
expect(output).toMatch(/from ["']solid-js["']/);
|
|
194
|
+
// if solid-js were inlined, the output would be thousands of lines
|
|
195
|
+
const lines = output.split("\n").length;
|
|
196
|
+
expect(lines).toBeLessThan(20);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("does not contain bundled automerge dep code", () => {
|
|
200
|
+
// The automerge dep should be external, not inlined
|
|
201
|
+
expect(output).not.toContain("my-sideboard-tool");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("esbuild build", () => {
|
|
206
|
+
let output: string;
|
|
207
|
+
|
|
208
|
+
beforeAll(async () => {
|
|
209
|
+
const pluginPath = path.resolve(originalCwd, "dist/esbuild.js");
|
|
210
|
+
const { default: esbuildPlugin } = await import(pluginPath);
|
|
211
|
+
|
|
212
|
+
await esbuild.build({
|
|
213
|
+
entryPoints: [path.join(tmpDir, "src/index.ts")],
|
|
214
|
+
bundle: true,
|
|
215
|
+
format: "esm",
|
|
216
|
+
outfile: path.join(tmpDir, "dist-esbuild/index.js"),
|
|
217
|
+
plugins: [esbuildPlugin()],
|
|
218
|
+
logLevel: "silent",
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
output = await fs.readFile(
|
|
222
|
+
path.join(tmpDir, "dist-esbuild/index.js"),
|
|
223
|
+
"utf-8"
|
|
224
|
+
);
|
|
225
|
+
}, 60_000);
|
|
226
|
+
|
|
227
|
+
it("rewrites automerge dep to service-worker URL", () => {
|
|
228
|
+
expect(output).toContain(ENCODED_URL);
|
|
229
|
+
expect(output).toMatch(new RegExp(`from ["']/${ENCODED_URL}/`));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("externalizes solid-js", () => {
|
|
233
|
+
expect(output).toMatch(/from ["']solid-js["']/);
|
|
234
|
+
// if solid-js were inlined, the output would be thousands of lines
|
|
235
|
+
const lines = output.split("\n").length;
|
|
236
|
+
expect(lines).toBeLessThan(20);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("does not contain bundled automerge dep code", () => {
|
|
240
|
+
expect(output).not.toContain("my-sideboard-tool");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
package/tsconfig.json
ADDED