Merge ad96cd065c8ed809aa3e5afad4273020b4c9d9da into 8a05c05596ca9ba0735dafd8e359885de4c2c969

This commit is contained in:
Alberto Leal 2026-03-21 05:49:45 +00:00 committed by GitHub
commit 80b9489da1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 648 additions and 0 deletions

View File

@ -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",

View 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 };

View 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);
});
});
});

View 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");
});
});