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:sync-exports": "node scripts/sync-plugin-sdk-exports.mjs",
|
||||
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
|
||||
"postinstall": "node scripts/patch-esm-exports.cjs",
|
||||
"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",
|
||||
"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