Merge ad96cd065c8ed809aa3e5afad4273020b4c9d9da into 8a05c05596ca9ba0735dafd8e359885de4c2c969
This commit is contained in:
commit
80b9489da1
@ -529,6 +529,7 @@
|
|||||||
"plugin-sdk:check-exports": "node scripts/sync-plugin-sdk-exports.mjs --check",
|
"plugin-sdk:check-exports": "node scripts/sync-plugin-sdk-exports.mjs --check",
|
||||||
"plugin-sdk:sync-exports": "node scripts/sync-plugin-sdk-exports.mjs",
|
"plugin-sdk:sync-exports": "node scripts/sync-plugin-sdk-exports.mjs",
|
||||||
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
|
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
|
||||||
|
"postinstall": "node scripts/patch-esm-exports.cjs",
|
||||||
"prepack": "pnpm build && pnpm ui:build",
|
"prepack": "pnpm build && pnpm ui:build",
|
||||||
"prepare": "command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath git-hooks || exit 0",
|
"prepare": "command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath git-hooks || exit 0",
|
||||||
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
||||||
|
|||||||
116
scripts/patch-esm-exports.cjs
Normal file
116
scripts/patch-esm-exports.cjs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Postinstall patch: add "default" export condition to ESM-only packages.
|
||||||
|
*
|
||||||
|
* jiti (the TS/ESM loader used at runtime) converts imports to CJS require().
|
||||||
|
* Some dependencies ship export maps with only an "import" condition and no
|
||||||
|
* "default" or "require" fallback, which causes ERR_PACKAGE_PATH_NOT_EXPORTED.
|
||||||
|
* This script walks node_modules and adds the missing "default" condition so
|
||||||
|
* both ESM and CJS resolution work.
|
||||||
|
*
|
||||||
|
* Safe to run multiple times (idempotent). Never exits non-zero.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const MAX_DEPTH = 8;
|
||||||
|
const SKIP_DIRS = new Set([".cache", ".store"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutate an exports object in-place, adding a "default" condition to any entry
|
||||||
|
* that has "import" but neither "default" nor "require".
|
||||||
|
*
|
||||||
|
* @param {unknown} exports - The "exports" field from package.json
|
||||||
|
* @returns {boolean} Whether any entry was modified
|
||||||
|
*/
|
||||||
|
function patchExports(exports) {
|
||||||
|
if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let modified = false;
|
||||||
|
for (const key of Object.keys(exports)) {
|
||||||
|
const entry = exports[key];
|
||||||
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ("import" in entry && !("default" in entry) && !("require" in entry)) {
|
||||||
|
entry.default = entry.import;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk a directory tree and patch every package.json whose exports need a
|
||||||
|
* "default" condition.
|
||||||
|
*
|
||||||
|
* @param {string} dir - Root directory to walk (typically node_modules)
|
||||||
|
* @returns {{ patchedCount: number, errors: Array<{ file: string, error: string }> }}
|
||||||
|
*/
|
||||||
|
function patchDir(dir) {
|
||||||
|
let patchedCount = 0;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
function walk(currentDir, depth) {
|
||||||
|
if (depth > MAX_DEPTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const name = entry.name;
|
||||||
|
if (SKIP_DIRS.has(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fullPath = path.join(currentDir, name);
|
||||||
|
|
||||||
|
if (name === "package.json") {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(fullPath, "utf8");
|
||||||
|
const pkg = JSON.parse(content);
|
||||||
|
if (pkg.exports && patchExports(pkg.exports)) {
|
||||||
|
fs.writeFileSync(fullPath, JSON.stringify(pkg, null, 2) + "\n");
|
||||||
|
patchedCount++;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errors.push({ file: fullPath, error: err.message });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDir = entry.isDirectory();
|
||||||
|
if (!isDir && entry.isSymbolicLink()) {
|
||||||
|
try {
|
||||||
|
isDir = fs.statSync(fullPath).isDirectory();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isDir) {
|
||||||
|
walk(fullPath, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(dir, 0);
|
||||||
|
return { patchedCount, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
try {
|
||||||
|
const nodeModules = path.resolve(__dirname, "..", "node_modules");
|
||||||
|
const { patchedCount } = patchDir(nodeModules);
|
||||||
|
console.log(`patch-esm-exports: patched ${patchedCount} package(s)`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("patch-esm-exports: unexpected error —", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { patchExports, patchDir };
|
||||||
189
src/scripts/patch-esm-exports.e2e.test.ts
Normal file
189
src/scripts/patch-esm-exports.e2e.test.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { afterAll, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const projectRoot = path.resolve(__dirname, "../..");
|
||||||
|
const nodeModules = path.join(projectRoot, "node_modules");
|
||||||
|
|
||||||
|
// Use dynamic import for the CJS patch script (createRequire fails in vmForks
|
||||||
|
// because the shebang line is not valid JS in the VM context).
|
||||||
|
const { patchDir } = (await import(
|
||||||
|
/* @vite-ignore */ path.join(projectRoot, "scripts/patch-esm-exports.cjs")
|
||||||
|
)) as {
|
||||||
|
patchDir: (dir: string) => {
|
||||||
|
patchedCount: number;
|
||||||
|
errors: Array<{ file: string; error: string }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectRequire = createRequire(path.join(projectRoot, "__anchor__.js"));
|
||||||
|
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "esm-patch-e2e-"));
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try {
|
||||||
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup failures
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let caseIndex = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fake ESM-only package inside a fresh temp directory.
|
||||||
|
* Uses a unique package name per call to avoid Node.js module-resolution caching.
|
||||||
|
*/
|
||||||
|
function createEsmOnlyPackage(prefix = "esm-only") {
|
||||||
|
const id = caseIndex++;
|
||||||
|
const pkgName = `${prefix}-e2e-${id}-${Date.now()}`;
|
||||||
|
const root = path.join(fixtureRoot, `case-${id}`);
|
||||||
|
const pkgDir = path.join(root, "node_modules", pkgName);
|
||||||
|
const distDir = path.join(pkgDir, "dist");
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(distDir, "index.mjs"), "export default {};");
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "package.json"),
|
||||||
|
JSON.stringify({ name: pkgName, exports: { ".": { import: "./dist/index.mjs" } } }, null, 2) +
|
||||||
|
"\n",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
return { root, pkgDir, pkgName };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERR_PACKAGE_PATH_NOT_EXPORTED is the error code; the message text varies by Node version.
|
||||||
|
const ESM_EXPORT_ERROR = /ERR_PACKAGE_PATH_NOT_EXPORTED|No "exports" main defined/;
|
||||||
|
|
||||||
|
describe("patch-esm-exports e2e", () => {
|
||||||
|
describe("reproduces ERR_PACKAGE_PATH_NOT_EXPORTED without patch", () => {
|
||||||
|
it("CJS require.resolve fails for ESM-only package", () => {
|
||||||
|
const { root, pkgName } = createEsmOnlyPackage();
|
||||||
|
const req = createRequire(path.join(root, "__test__.js"));
|
||||||
|
|
||||||
|
expect(() => req.resolve(pkgName)).toThrowError(ESM_EXPORT_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("CJS require() fails for ESM-only package", () => {
|
||||||
|
const { root, pkgName } = createEsmOnlyPackage();
|
||||||
|
const req = createRequire(path.join(root, "__test__.js"));
|
||||||
|
|
||||||
|
expect(() => req(pkgName)).toThrowError(ESM_EXPORT_ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("patch fixes CJS resolution", () => {
|
||||||
|
it("require.resolve succeeds after patchDir", () => {
|
||||||
|
// Use unique package name and patch BEFORE first resolution attempt
|
||||||
|
// to avoid Node.js caching the export-map failure.
|
||||||
|
const { root, pkgName } = createEsmOnlyPackage("fix-resolve");
|
||||||
|
const result = patchDir(root);
|
||||||
|
expect(result.patchedCount).toBe(1);
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
|
||||||
|
const req = createRequire(path.join(root, "__test__.js"));
|
||||||
|
const resolved = req.resolve(pkgName);
|
||||||
|
expect(resolved).toContain(path.join("dist", "index.mjs"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("patched package.json has correct 'default' condition", () => {
|
||||||
|
const { root, pkgDir } = createEsmOnlyPackage("fix-pkg");
|
||||||
|
patchDir(root);
|
||||||
|
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8")) as {
|
||||||
|
exports: Record<string, Record<string, string>>;
|
||||||
|
};
|
||||||
|
expect(pkg.exports["."]).toHaveProperty("default", "./dist/index.mjs");
|
||||||
|
expect(pkg.exports["."]).toHaveProperty("import", "./dist/index.mjs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles packages with multiple export entries", () => {
|
||||||
|
const id = caseIndex++;
|
||||||
|
const pkgName = `esm-multi-e2e-${id}-${Date.now()}`;
|
||||||
|
const root = path.join(fixtureRoot, `case-${id}`);
|
||||||
|
const pkgDir = path.join(root, "node_modules", pkgName);
|
||||||
|
const distDir = path.join(pkgDir, "dist");
|
||||||
|
fs.mkdirSync(path.join(distDir, "hooks"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(distDir, "index.mjs"), "export default {};");
|
||||||
|
fs.writeFileSync(path.join(distDir, "hooks", "index.mjs"), "export default {};");
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "package.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
name: pkgName,
|
||||||
|
exports: {
|
||||||
|
".": { import: "./dist/index.mjs" },
|
||||||
|
"./hooks": { import: "./dist/hooks/index.mjs" },
|
||||||
|
},
|
||||||
|
}) + "\n",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Patch before first resolution to avoid caching the failure
|
||||||
|
const result = patchDir(root);
|
||||||
|
expect(result.patchedCount).toBe(1);
|
||||||
|
|
||||||
|
const req = createRequire(path.join(root, "__test__.js"));
|
||||||
|
const resolved = req.resolve(pkgName);
|
||||||
|
expect(resolved).toContain(path.join("dist", "index.mjs"));
|
||||||
|
|
||||||
|
const resolvedHooks = req.resolve(`${pkgName}/hooks`);
|
||||||
|
expect(resolvedHooks).toContain(path.join("dist", "hooks", "index.mjs"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("real-world package verification", () => {
|
||||||
|
it("@buape/carbon is resolvable via CJS require.resolve", () => {
|
||||||
|
const resolved = projectRequire.resolve("@buape/carbon");
|
||||||
|
expect(resolved).toBeTruthy();
|
||||||
|
expect(fs.existsSync(resolved)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("osc-progress is resolvable via CJS require.resolve", () => {
|
||||||
|
const resolved = projectRequire.resolve("osc-progress");
|
||||||
|
expect(resolved).toBeTruthy();
|
||||||
|
expect(fs.existsSync(resolved)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("@mariozechner/pi-coding-agent is resolvable via CJS require.resolve", () => {
|
||||||
|
const resolved = projectRequire.resolve("@mariozechner/pi-coding-agent");
|
||||||
|
expect(resolved).toBeTruthy();
|
||||||
|
expect(fs.existsSync(resolved)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("real packages have 'default' condition in exports after postinstall", () => {
|
||||||
|
const packages = ["@buape/carbon", "osc-progress", "@mariozechner/pi-coding-agent"];
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const pkgJsonPath = path.join(nodeModules, ...pkg.split("/"), "package.json");
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")) as {
|
||||||
|
exports?: Record<string, Record<string, string>>;
|
||||||
|
};
|
||||||
|
expect(manifest.exports, `${pkg} should have exports`).toBeDefined();
|
||||||
|
const mainEntry = manifest.exports!["."];
|
||||||
|
expect(mainEntry, `${pkg} should have '.' export`).toBeDefined();
|
||||||
|
expect(mainEntry).toHaveProperty("default");
|
||||||
|
expect(mainEntry).toHaveProperty("import");
|
||||||
|
expect(mainEntry.default).toBe(mainEntry.import);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("jiti can resolve a patched package", async () => {
|
||||||
|
const { createJiti } = await import("jiti");
|
||||||
|
|
||||||
|
// Anchor jiti at the project root so node_modules resolution works.
|
||||||
|
const jiti = createJiti(path.join(projectRoot, "__entry__.ts"), {
|
||||||
|
interopDefault: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify jiti's internal resolution finds the patched package.
|
||||||
|
// We use resolve() rather than evaluation to avoid vmForks VM context
|
||||||
|
// conflicts with jiti's module wrapper.
|
||||||
|
const resolved = jiti.resolve("@buape/carbon");
|
||||||
|
expect(resolved).toBeTruthy();
|
||||||
|
expect(fs.existsSync(resolved)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
342
src/scripts/patch-esm-exports.test.ts
Normal file
342
src/scripts/patch-esm-exports.test.ts
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterAll, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const esmRequire = createRequire(import.meta.url);
|
||||||
|
const { patchExports, patchDir } = esmRequire("../../scripts/patch-esm-exports.cjs") as {
|
||||||
|
patchExports: (exports: unknown) => boolean;
|
||||||
|
patchDir: (dir: string) => {
|
||||||
|
patchedCount: number;
|
||||||
|
errors: Array<{ file: string; error: string }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "esm-patch-test-"));
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try {
|
||||||
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup failures
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let caseIndex = 0;
|
||||||
|
function makeDir() {
|
||||||
|
const dir = path.join(fixtureRoot, `case-${caseIndex++}`);
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePackageJson(dir: string, pkg: unknown) {
|
||||||
|
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPackageJson(dir: string) {
|
||||||
|
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("patchExports", () => {
|
||||||
|
it("adds 'default' condition when only 'import' exists", () => {
|
||||||
|
const exports = {
|
||||||
|
".": { import: "./dist/index.mjs" },
|
||||||
|
};
|
||||||
|
const modified = patchExports(exports);
|
||||||
|
expect(modified).toBe(true);
|
||||||
|
expect(exports["."]).toEqual({
|
||||||
|
import: "./dist/index.mjs",
|
||||||
|
default: "./dist/index.mjs",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not modify entries with existing 'default' condition", () => {
|
||||||
|
const exports = {
|
||||||
|
".": { import: "./dist/index.mjs", default: "./dist/index.cjs" },
|
||||||
|
};
|
||||||
|
const modified = patchExports(exports);
|
||||||
|
expect(modified).toBe(false);
|
||||||
|
expect(exports["."]).toEqual({
|
||||||
|
import: "./dist/index.mjs",
|
||||||
|
default: "./dist/index.cjs",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not modify entries with existing 'require' condition", () => {
|
||||||
|
const exports = {
|
||||||
|
".": { import: "./dist/index.mjs", require: "./dist/index.cjs" },
|
||||||
|
};
|
||||||
|
const modified = patchExports(exports);
|
||||||
|
expect(modified).toBe(false);
|
||||||
|
expect(exports["."]).toEqual({
|
||||||
|
import: "./dist/index.mjs",
|
||||||
|
require: "./dist/index.cjs",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles string shorthand exports without modification", () => {
|
||||||
|
const exports = { ".": "./dist/index.js" };
|
||||||
|
const modified = patchExports(exports);
|
||||||
|
expect(modified).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles non-object export values without modification", () => {
|
||||||
|
const exports = { ".": null, "./foo": 42 };
|
||||||
|
const modified = patchExports(exports);
|
||||||
|
expect(modified).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles array export values without modification", () => {
|
||||||
|
const exports = { ".": ["./dist/a.js", "./dist/b.js"] };
|
||||||
|
const modified = patchExports(exports);
|
||||||
|
expect(modified).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for null", () => {
|
||||||
|
expect(patchExports(null)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for a string", () => {
|
||||||
|
expect(patchExports("./index.js")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for an array", () => {
|
||||||
|
expect(patchExports(["./index.js"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles multiple export entries", () => {
|
||||||
|
const exports = {
|
||||||
|
".": { import: "./dist/index.mjs" },
|
||||||
|
"./hooks": { import: "./dist/hooks.mjs" },
|
||||||
|
"./*": { import: "./dist/*.mjs", require: "./dist/*.cjs" },
|
||||||
|
};
|
||||||
|
const modified = patchExports(exports);
|
||||||
|
expect(modified).toBe(true);
|
||||||
|
expect(exports["."]).toHaveProperty("default", "./dist/index.mjs");
|
||||||
|
expect(exports["./hooks"]).toHaveProperty("default", "./dist/hooks.mjs");
|
||||||
|
expect(exports["./*"]).not.toHaveProperty("default");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves existing fields in the exports entry", () => {
|
||||||
|
const exports = {
|
||||||
|
".": { import: "./dist/index.mjs", types: "./dist/index.d.ts" },
|
||||||
|
};
|
||||||
|
patchExports(exports);
|
||||||
|
expect(exports["."]).toEqual({
|
||||||
|
import: "./dist/index.mjs",
|
||||||
|
types: "./dist/index.d.ts",
|
||||||
|
default: "./dist/index.mjs",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("patchDir", () => {
|
||||||
|
it("patches package.json that needs 'default' condition", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "esm-only-pkg");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, {
|
||||||
|
name: "esm-only-pkg",
|
||||||
|
exports: { ".": { import: "./dist/index.mjs" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(1);
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
const pkg = readPackageJson(pkgDir);
|
||||||
|
expect(pkg.exports["."]).toHaveProperty("default", "./dist/index.mjs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not modify packages with existing 'default' condition", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "dual-pkg");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, {
|
||||||
|
name: "dual-pkg",
|
||||||
|
exports: { ".": { import: "./dist/index.mjs", default: "./dist/index.cjs" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(0);
|
||||||
|
const pkg = readPackageJson(pkgDir);
|
||||||
|
expect(pkg.exports["."]).toEqual({
|
||||||
|
import: "./dist/index.mjs",
|
||||||
|
default: "./dist/index.cjs",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles packages with no exports field", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "no-exports-pkg");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, { name: "no-exports-pkg", main: "./index.js" });
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(0);
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles malformed package.json gracefully", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "bad-pkg");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(pkgDir, "package.json"), "{ not valid json !!!", "utf8");
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(0);
|
||||||
|
expect(result.errors.length).toBeGreaterThan(0);
|
||||||
|
expect(result.errors[0].file).toContain("bad-pkg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is idempotent - running twice produces same result", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "idem-pkg");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, {
|
||||||
|
name: "idem-pkg",
|
||||||
|
exports: { ".": { import: "./dist/index.mjs" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
patchDir(root);
|
||||||
|
const afterFirst = readPackageJson(pkgDir);
|
||||||
|
|
||||||
|
const secondResult = patchDir(root);
|
||||||
|
const afterSecond = readPackageJson(pkgDir);
|
||||||
|
|
||||||
|
expect(secondResult.patchedCount).toBe(0);
|
||||||
|
expect(afterSecond).toEqual(afterFirst);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects max depth limit", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
// Create a deeply nested directory (depth > 8)
|
||||||
|
let nested = root;
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
nested = path.join(nested, `level-${i}`);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(nested, { recursive: true });
|
||||||
|
writePackageJson(nested, {
|
||||||
|
name: "deep-pkg",
|
||||||
|
exports: { ".": { import: "./dist/index.mjs" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips .cache and .store directories", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
|
||||||
|
for (const skipDir of [".cache", ".store"]) {
|
||||||
|
const pkgDir = path.join(root, skipDir, "hidden-pkg");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, {
|
||||||
|
name: `hidden-${skipDir}`,
|
||||||
|
exports: { ".": { import: "./dist/index.mjs" } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles multiple packages in the same tree", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const nm = path.join(root, "node_modules");
|
||||||
|
|
||||||
|
const pkgA = path.join(nm, "pkg-a");
|
||||||
|
fs.mkdirSync(pkgA, { recursive: true });
|
||||||
|
writePackageJson(pkgA, {
|
||||||
|
name: "pkg-a",
|
||||||
|
exports: { ".": { import: "./a.mjs" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const pkgB = path.join(nm, "pkg-b");
|
||||||
|
fs.mkdirSync(pkgB, { recursive: true });
|
||||||
|
writePackageJson(pkgB, {
|
||||||
|
name: "pkg-b",
|
||||||
|
exports: { ".": { import: "./b.mjs", default: "./b.cjs" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const pkgC = path.join(nm, "pkg-c");
|
||||||
|
fs.mkdirSync(pkgC, { recursive: true });
|
||||||
|
writePackageJson(pkgC, {
|
||||||
|
name: "pkg-c",
|
||||||
|
exports: { ".": { import: "./c.mjs" }, "./sub": { import: "./sub.mjs" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(2);
|
||||||
|
expect(readPackageJson(pkgA).exports["."]).toHaveProperty("default", "./a.mjs");
|
||||||
|
expect(readPackageJson(pkgB).exports["."].default).toBe("./b.cjs");
|
||||||
|
expect(readPackageJson(pkgC).exports["."]).toHaveProperty("default", "./c.mjs");
|
||||||
|
expect(readPackageJson(pkgC).exports["./sub"]).toHaveProperty("default", "./sub.mjs");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("affected packages verification", () => {
|
||||||
|
it("correctly identifies @buape/carbon as needing patch", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "@buape", "carbon");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, {
|
||||||
|
name: "@buape/carbon",
|
||||||
|
exports: {
|
||||||
|
".": { import: "./dist/index.js", types: "./dist/index.d.ts" },
|
||||||
|
"./*": { import: "./dist/*.js", types: "./dist/*.d.ts" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(1);
|
||||||
|
const pkg = readPackageJson(pkgDir);
|
||||||
|
expect(pkg.exports["."]).toHaveProperty("default", "./dist/index.js");
|
||||||
|
expect(pkg.exports["./*"]).toHaveProperty("default", "./dist/*.js");
|
||||||
|
expect(pkg.exports["."].types).toBe("./dist/index.d.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly identifies osc-progress as needing patch", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "osc-progress");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, {
|
||||||
|
name: "osc-progress",
|
||||||
|
exports: {
|
||||||
|
".": { import: "./dist/index.js", types: "./dist/index.d.ts" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(1);
|
||||||
|
const pkg = readPackageJson(pkgDir);
|
||||||
|
expect(pkg.exports["."]).toHaveProperty("default", "./dist/index.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly identifies @mariozechner/pi-coding-agent as needing patch", () => {
|
||||||
|
const root = makeDir();
|
||||||
|
const pkgDir = path.join(root, "node_modules", "@mariozechner", "pi-coding-agent");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
writePackageJson(pkgDir, {
|
||||||
|
name: "@mariozechner/pi-coding-agent",
|
||||||
|
exports: {
|
||||||
|
".": { import: "./dist/index.js", types: "./dist/index.d.ts" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = patchDir(root);
|
||||||
|
|
||||||
|
expect(result.patchedCount).toBe(1);
|
||||||
|
const pkg = readPackageJson(pkgDir);
|
||||||
|
expect(pkg.exports["."]).toHaveProperty("default", "./dist/index.js");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user