fix(plugins): restored legacy commonjs compatibility
- Added DefaultPackageManager discovery compatibility for legacy extensions. - Loaded graph-owned CommonJS modules through synchronous default bridges. Fixes #5658
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed linked legacy pi extensions failing to load when they import `DefaultPackageManager` or linkedom: the coding-agent compatibility shim now enumerates OMP extension paths with plugin metadata, and extension-graph CommonJS modules load through synchronous default-export bridges with linkedom's bundled canvas fallback. ([#5658](https://github.com/can1357/oh-my-pi/issues/5658))
|
||||
|
||||
## [17.0.1] - 2026-07-16
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -44,9 +44,10 @@ import { ReadTool } from "../tools/read";
|
||||
import { formatBytes } from "../tools/render-utils";
|
||||
import { WriteTool } from "../tools/write";
|
||||
import { EventBus } from "../utils/event-bus";
|
||||
import { loadExtensionFromFactory, loadExtensions } from "./extensions";
|
||||
import { discoverExtensionPaths, loadExtensionFromFactory, loadExtensions } from "./extensions";
|
||||
import { ExtensionRuntime } from "./extensions/loader";
|
||||
import type { ExtensionFactory, ToolDefinition } from "./extensions/types";
|
||||
import { getEnabledPlugins, resolvePluginExtensionPaths, type ScopedInstalledPlugin } from "./plugins/loader";
|
||||
import type { Skill } from "./skills";
|
||||
import { loadSkillsFromDir } from "./skills";
|
||||
import { Type } from "./typebox";
|
||||
@@ -655,6 +656,100 @@ export const SettingsManager = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** Scope used by the legacy package manager for discovered resources. */
|
||||
export type SourceScope = "user" | "project" | "temporary";
|
||||
|
||||
/** Discovery metadata exposed alongside a legacy package resource path. */
|
||||
export interface PathMetadata {
|
||||
source: string;
|
||||
scope: SourceScope;
|
||||
origin: "package" | "top-level";
|
||||
baseDir?: string;
|
||||
}
|
||||
|
||||
/** One extension, skill, prompt, or theme resolved by the legacy package manager. */
|
||||
export interface ResolvedResource {
|
||||
path: string;
|
||||
enabled: boolean;
|
||||
metadata: PathMetadata;
|
||||
}
|
||||
|
||||
/** Resource groups returned by {@link DefaultPackageManager.resolve}. */
|
||||
export interface ResolvedPaths {
|
||||
extensions: ResolvedResource[];
|
||||
skills: ResolvedResource[];
|
||||
prompts: ResolvedResource[];
|
||||
themes: ResolvedResource[];
|
||||
}
|
||||
|
||||
/** Action a legacy caller requests when a configured package is unavailable. */
|
||||
export type MissingSourceAction = "install" | "skip" | "error";
|
||||
|
||||
/** Construction inputs accepted by the legacy package manager. */
|
||||
export interface DefaultPackageManagerOptions {
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
settingsManager: Settings | Promise<Settings>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerates the extensions OMP would load through the historical package
|
||||
* manager surface used by legacy extensions.
|
||||
*/
|
||||
export class DefaultPackageManager {
|
||||
#cwd: string;
|
||||
#agentDir: string;
|
||||
#settingsManager: Settings | Promise<Settings>;
|
||||
|
||||
constructor(options: DefaultPackageManagerOptions) {
|
||||
this.#cwd = options.cwd;
|
||||
this.#agentDir = options.agentDir;
|
||||
this.#settingsManager = options.settingsManager;
|
||||
}
|
||||
|
||||
/** Resolve enabled extension paths with their OMP plugin provenance. */
|
||||
async resolve(_onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> {
|
||||
const settings = await this.#settingsManager;
|
||||
const configuredPaths = settings.get("extensions") ?? [];
|
||||
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
||||
const [extensionPaths, plugins] = await Promise.all([
|
||||
discoverExtensionPaths(configuredPaths, this.#cwd, disabledExtensionIds),
|
||||
getEnabledPlugins(this.#cwd),
|
||||
]);
|
||||
const pluginByExtensionPath = new Map<string, ScopedInstalledPlugin>();
|
||||
for (const plugin of plugins) {
|
||||
for (const extensionPath of resolvePluginExtensionPaths(plugin)) {
|
||||
pluginByExtensionPath.set(path.resolve(extensionPath), plugin);
|
||||
}
|
||||
}
|
||||
|
||||
const extensions = extensionPaths.map(extensionPath => {
|
||||
const resolvedPath = path.resolve(extensionPath);
|
||||
const plugin = pluginByExtensionPath.get(resolvedPath);
|
||||
const agentDirRelative = path.relative(path.resolve(this.#agentDir), resolvedPath);
|
||||
const metadata: PathMetadata = plugin
|
||||
? {
|
||||
source: `npm:${plugin.name}`,
|
||||
scope: plugin.scope,
|
||||
origin: "package",
|
||||
baseDir: plugin.path,
|
||||
}
|
||||
: {
|
||||
source: "auto",
|
||||
scope:
|
||||
agentDirRelative === "" ||
|
||||
(!agentDirRelative.startsWith("..") && !path.isAbsolute(agentDirRelative))
|
||||
? "user"
|
||||
: "project",
|
||||
origin: "top-level",
|
||||
};
|
||||
return { path: resolvedPath, enabled: true, metadata };
|
||||
});
|
||||
|
||||
return { extensions, skills: [], prompts: [], themes: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource-loader compatibility layer for legacy pi extensions.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference path="./legacy-pi-virtual-modules.d.ts" />
|
||||
import * as fs from "node:fs";
|
||||
import { isBuiltin } from "node:module";
|
||||
import { createRequire, isBuiltin } from "node:module";
|
||||
import * as path from "node:path";
|
||||
import * as url from "node:url";
|
||||
import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
|
||||
@@ -1111,6 +1111,9 @@ const EXTENSION_GRAPH_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["
|
||||
// reloads install supplemental hooks only for modules added to the graph since
|
||||
// the previous load.
|
||||
const extensionGraphHookModules = new Map<string, Set<string>>();
|
||||
const COMMONJS_MODULES_GLOBAL = "__ompLegacyPiCommonJsModules";
|
||||
const commonJsModuleExports = new Map<string, unknown>();
|
||||
Reflect.set(globalThis, COMMONJS_MODULES_GLOBAL, commonJsModuleExports);
|
||||
|
||||
let legacyPiLoadTag = 0;
|
||||
|
||||
@@ -1144,9 +1147,9 @@ async function realpathOrSelfUncached(p: string): Promise<string> {
|
||||
* Extension-local bare dependency entries are also included so their relative
|
||||
* children receive the reload mtime tag; bare imports inside those dependencies
|
||||
* remain native Bun resolutions to avoid taking over full third-party graphs.
|
||||
* CommonJS modules reached through `require()` stay on Bun's native loader.
|
||||
* The only exception is a module whose bare requires resolve to native addons:
|
||||
* those require a synchronous hook that pins the addon to an absolute path.
|
||||
* CommonJS modules reached through `require()` stay on Bun's native loader
|
||||
* unless they resolve native addons. CommonJS reached through ESM imports stays
|
||||
* graph-owned so the load hook can expose its exports through an ESM default.
|
||||
*/
|
||||
async function collectExtensionModules(entryRealPath: string): Promise<Map<string, string>> {
|
||||
const modules = new Map<string, string>();
|
||||
@@ -1254,20 +1257,51 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
|
||||
}
|
||||
|
||||
/**
|
||||
* Install exact-path load hooks for the current extension graph. ESM/TS source
|
||||
* retains the async rewrite path. Native-addon CJS loaders use a synchronous
|
||||
* hook with source pre-rewritten during graph collection; Bun rejects a CJS
|
||||
* `require()` whose onLoad callback returns a promise.
|
||||
* Load a graph-owned CommonJS module before its hook is installed and expose
|
||||
* the value through a synchronous ESM wrapper. Anchoring `createRequire` at the
|
||||
* owning package preserves its dependency resolution; linkedom's canvas bridge
|
||||
* goes directly to its bundled fallback because OMP does not ship native canvas.
|
||||
*/
|
||||
function installExtensionGraphHook(
|
||||
async function synthesizeCommonJsDefaultModule(modulePath: string): Promise<string> {
|
||||
const packageRoot = await findPackageRoot(modulePath);
|
||||
const packageJsonPath = packageRoot ? path.join(packageRoot, "package.json") : modulePath;
|
||||
let targetPath = modulePath;
|
||||
if (packageRoot) {
|
||||
const manifest = await readPackageManifest(packageRoot);
|
||||
const packageRelativePath = path.relative(packageRoot, modulePath).split(path.sep).join("/");
|
||||
if (manifest?.name === "linkedom" && packageRelativePath === "commonjs/canvas.cjs") {
|
||||
targetPath = path.join(packageRoot, "commonjs", "canvas-shim.cjs");
|
||||
}
|
||||
}
|
||||
|
||||
const requireFromPackage = createRequire(packageJsonPath);
|
||||
const specifier = packageRoot ? `./${path.relative(packageRoot, targetPath).split(path.sep).join("/")}` : targetPath;
|
||||
commonJsModuleExports.set(modulePath, requireFromPackage(specifier));
|
||||
return `export default globalThis[${JSON.stringify(COMMONJS_MODULES_GLOBAL)}].get(${JSON.stringify(modulePath)});\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install exact-path load hooks for the current extension graph. ESM/TS source
|
||||
* retains the async rewrite path. CommonJS wrappers and native-addon loaders
|
||||
* stay synchronous because Bun rejects `require()` targets backed by async
|
||||
* `onLoad` callbacks.
|
||||
*/
|
||||
async function installExtensionGraphHook(
|
||||
entryRealPath: string,
|
||||
modules: Map<string, string>,
|
||||
): { asyncModules: Map<string, string>; syncCommonJsModules: Map<string, string> } {
|
||||
): Promise<{ asyncModules: Map<string, string>; syncSourceModules: Map<string, string> }> {
|
||||
const asyncModules = new Map<string, string>();
|
||||
const syncCommonJsModules = new Map<string, string>();
|
||||
const commonJsModules = new Map<string, string>();
|
||||
const syncSourceModules = new Map<string, string>();
|
||||
for (const [modulePath, source] of modules) {
|
||||
const destination = nativeAddonLoaderModulePaths.has(modulePath) ? syncCommonJsModules : asyncModules;
|
||||
destination.set(modulePath, source);
|
||||
const extension = path.extname(modulePath);
|
||||
if (extension === ".cjs" || extension === ".cts") {
|
||||
commonJsModules.set(modulePath, await synthesizeCommonJsDefaultModule(modulePath));
|
||||
} else if (nativeAddonLoaderModulePaths.has(modulePath)) {
|
||||
syncSourceModules.set(modulePath, source);
|
||||
} else {
|
||||
asyncModules.set(modulePath, source);
|
||||
}
|
||||
}
|
||||
|
||||
if (asyncModules.size > 0) {
|
||||
@@ -1299,17 +1333,39 @@ function installExtensionGraphHook(
|
||||
});
|
||||
}
|
||||
|
||||
if (syncCommonJsModules.size > 0) {
|
||||
const alternation = [...syncCommonJsModules.keys()].map(escapeRegExp).join("|");
|
||||
if (commonJsModules.size > 0) {
|
||||
const alternation = [...commonJsModules.keys()].map(escapeRegExp).join("|");
|
||||
const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
|
||||
const hookId = Bun.hash(`${entryRealPath}\0sync-cjs\0${[...syncCommonJsModules.keys()].join("\0")}`).toString(36);
|
||||
const hookId = Bun.hash(`${entryRealPath}\0commonjs\0${[...commonJsModules.keys()].join("\0")}`).toString(36);
|
||||
Bun.plugin({
|
||||
name: `omp:legacy-pi-ext:${hookId}`,
|
||||
setup(build) {
|
||||
build.onLoad({ filter, namespace: "file" }, args => {
|
||||
const queryIndex = args.path.indexOf("?mtime=");
|
||||
const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
|
||||
const source = syncCommonJsModules.get(sourcePath);
|
||||
const source = commonJsModules.get(sourcePath);
|
||||
if (source === undefined) {
|
||||
throw new Error(`Missing CommonJS compatibility module: ${sourcePath}`);
|
||||
}
|
||||
return { contents: source, loader: "js" };
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (syncSourceModules.size > 0) {
|
||||
const alternation = [...syncSourceModules.keys()].map(escapeRegExp).join("|");
|
||||
const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
|
||||
const hookId = Bun.hash(`${entryRealPath}\0sync-source\0${[...syncSourceModules.keys()].join("\0")}`).toString(
|
||||
36,
|
||||
);
|
||||
Bun.plugin({
|
||||
name: `omp:legacy-pi-ext:${hookId}`,
|
||||
setup(build) {
|
||||
build.onLoad({ filter, namespace: "file" }, args => {
|
||||
const queryIndex = args.path.indexOf("?mtime=");
|
||||
const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
|
||||
const source = syncSourceModules.get(sourcePath);
|
||||
if (source === undefined) {
|
||||
throw new Error(`Missing pre-rewritten CommonJS extension source: ${sourcePath}`);
|
||||
}
|
||||
@@ -1318,7 +1374,7 @@ function installExtensionGraphHook(
|
||||
},
|
||||
});
|
||||
}
|
||||
return { asyncModules, syncCommonJsModules };
|
||||
return { asyncModules, syncSourceModules };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1347,14 +1403,14 @@ async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { asyncModules, syncCommonJsModules } = installExtensionGraphHook(entryRealPath, pendingModules);
|
||||
const { asyncModules, syncSourceModules } = await installExtensionGraphHook(entryRealPath, pendingModules);
|
||||
for (const modulePath of pendingModules.keys()) {
|
||||
hookedModules.add(modulePath);
|
||||
}
|
||||
return {
|
||||
clear() {
|
||||
asyncModules.clear();
|
||||
syncCommonJsModules.clear();
|
||||
syncSourceModules.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import {
|
||||
DefaultPackageManager,
|
||||
DefaultResourceLoader,
|
||||
createAgentSession as legacyCreateAgentSession,
|
||||
} from "@oh-my-pi/pi-coding-agent/extensibility/legacy-pi-coding-agent-shim";
|
||||
@@ -41,6 +42,35 @@ async function mkTempCwd(prefix: string): Promise<string> {
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe("DefaultPackageManager.resolve() (issue #5658)", () => {
|
||||
it("enumerates configured extension paths through OMP discovery", async () => {
|
||||
const tmp = await mkTempCwd("omp-legacy-default-package-manager-");
|
||||
const cwd = path.join(tmp, "project");
|
||||
const agentDir = path.join(tmp, "agent");
|
||||
const extensionPath = path.join(cwd, "configured-extension.ts");
|
||||
await fs.mkdir(cwd, { recursive: true });
|
||||
await fs.writeFile(extensionPath, "export default function () {}\n", "utf8");
|
||||
const settingsManager = Settings.isolated({ extensions: [extensionPath] });
|
||||
const manager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
||||
|
||||
const resolved = await manager.resolve(() => Promise.resolve("skip"));
|
||||
const extension = resolved.extensions.find(resource => resource.path === extensionPath);
|
||||
|
||||
expect(extension).toEqual({
|
||||
path: extensionPath,
|
||||
enabled: true,
|
||||
metadata: {
|
||||
source: "auto",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
},
|
||||
});
|
||||
expect(resolved.skills).toEqual([]);
|
||||
expect(resolved.prompts).toEqual([]);
|
||||
expect(resolved.themes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultResourceLoader.reload() (issue #4567)", () => {
|
||||
it("populates the discovery snapshot honoring no* flags and applying every override", async () => {
|
||||
const tmp = await mkTempCwd("omp-legacy-default-resource-loader-reload-");
|
||||
|
||||
@@ -78,6 +78,36 @@ describe("legacy-pi in-place module loading (issue #1674)", () => {
|
||||
expect(mod.value).toBe("config-ok");
|
||||
});
|
||||
|
||||
it("loads a default import from linkedom's CommonJS canvas fallback", async () => {
|
||||
const dir = await writePackage({
|
||||
"package.json": JSON.stringify({ name: "linkedom-consumer", version: "1.0.0", type: "module" }),
|
||||
"index.js": 'export { canvasValue } from "linkedom";\n',
|
||||
"node_modules/linkedom/package.json": JSON.stringify({
|
||||
name: "linkedom",
|
||||
version: "0.18.12",
|
||||
type: "module",
|
||||
exports: "./index.js",
|
||||
}),
|
||||
"node_modules/linkedom/index.js": [
|
||||
'import Canvas from "./commonjs/canvas.cjs";',
|
||||
"export const canvasValue = Canvas.createCanvas();",
|
||||
].join("\n"),
|
||||
"node_modules/linkedom/commonjs/canvas.cjs": [
|
||||
"try {",
|
||||
' module.exports = require("canvas");',
|
||||
"} catch {",
|
||||
' module.exports = require("./canvas-shim.cjs");',
|
||||
"}",
|
||||
].join("\n"),
|
||||
"node_modules/linkedom/commonjs/canvas-shim.cjs":
|
||||
'module.exports = { createCanvas: () => "linkedom-canvas-shim" };\n',
|
||||
});
|
||||
|
||||
const mod = await loadLegacyPiModule(path.join(dir, "index.js"));
|
||||
|
||||
expect(Reflect.get(Object(mod), "canvasValue")).toBe("linkedom-canvas-shim");
|
||||
});
|
||||
|
||||
it("reloads an edited entry module without polluting fileURLToPath-derived paths", async () => {
|
||||
const entrySource = (version: string): string =>
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user