193 lines
7.5 KiB
TypeScript
193 lines
7.5 KiB
TypeScript
import * as path from "node:path";
|
|
import { isEnoent } from "@oh-my-pi/pi-utils/fs-error";
|
|
|
|
/** Build-time specifier resolved to bundled legacy Pi module namespaces. */
|
|
export const LEGACY_PI_MODULES_SPECIFIER = "omp-legacy-pi-modules";
|
|
|
|
const VIRTUAL_NAMESPACE = "omp-legacy-pi-modules-build";
|
|
const packageDir = path.resolve(import.meta.dir, "..");
|
|
const repoRoot = path.resolve(packageDir, "..", "..");
|
|
|
|
interface BundledPackage {
|
|
readonly dir: string;
|
|
readonly identifier: string;
|
|
readonly rootShim: string | null;
|
|
}
|
|
|
|
const BUNDLED_PACKAGES: readonly BundledPackage[] = [
|
|
{ dir: "agent", identifier: "PiAgentCore", rootShim: null },
|
|
{ dir: "ai", identifier: "PiAi", rootShim: "legacy-pi-ai-shim.ts" },
|
|
{ dir: "coding-agent", identifier: "PiCodingAgent", rootShim: "legacy-pi-coding-agent-shim.ts" },
|
|
{ dir: "natives", identifier: "PiNatives", rootShim: null },
|
|
{ dir: "tui", identifier: "PiTui", rootShim: null },
|
|
{ dir: "utils", identifier: "PiUtils", rootShim: null },
|
|
];
|
|
|
|
const TYPEBOX_MODULE_KEY = "typebox";
|
|
const TYPEBOX_SHIM = "typebox.ts";
|
|
const SKIPPED_WILDCARD_BASENAMES = new Set(["index"]);
|
|
const MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES = new Set(["worker-entry"]);
|
|
|
|
/** One namespace module the binary must retain for legacy extension imports. */
|
|
export interface BundledPiEntry {
|
|
/** Canonical import key exposed to extensions. */
|
|
readonly key: string;
|
|
/** Unique identifier used by the virtual module's generated import. */
|
|
readonly binding: string;
|
|
/** Package or absolute source specifier compiled into the binary. */
|
|
readonly importSpecifier: string;
|
|
}
|
|
|
|
interface WildcardPattern {
|
|
readonly exportPrefix: string;
|
|
readonly exportSuffix: string;
|
|
readonly sourcePrefix: string;
|
|
readonly sourceSuffix: string;
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function bindingForSubpath(identifier: string, subpath: string): string {
|
|
const segments = subpath
|
|
.split("/")
|
|
.filter(Boolean)
|
|
.map(segment =>
|
|
segment
|
|
.split(/[-_]/)
|
|
.filter(Boolean)
|
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(""),
|
|
);
|
|
return `bundled${identifier}${segments.join("")}`;
|
|
}
|
|
|
|
function isSafeWildcardBasename(basename: string): boolean {
|
|
if (!basename || basename.startsWith(".") || basename.startsWith("_")) return false;
|
|
if (SKIPPED_WILDCARD_BASENAMES.has(basename)) return false;
|
|
if (MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES.has(basename)) return false;
|
|
return !/\.(test|spec|d|generated|bench)$/.test(basename);
|
|
}
|
|
|
|
function parseWildcardPattern(exportKey: string, sourcePattern: string): WildcardPattern | null {
|
|
const exportStar = exportKey.indexOf("*");
|
|
const sourceStar = sourcePattern.indexOf("*");
|
|
if (exportStar === -1 || sourceStar === -1) return null;
|
|
if (exportKey.indexOf("*", exportStar + 1) !== -1) return null;
|
|
if (sourcePattern.indexOf("*", sourceStar + 1) !== -1) return null;
|
|
if (!sourcePattern.startsWith("./")) return null;
|
|
return {
|
|
exportPrefix: exportKey.slice(2, exportStar),
|
|
exportSuffix: exportKey.slice(exportStar + 1),
|
|
sourcePrefix: sourcePattern.slice(2, sourceStar),
|
|
sourceSuffix: sourcePattern.slice(sourceStar + 1),
|
|
};
|
|
}
|
|
|
|
function exportImportTarget(value: unknown): string | null {
|
|
if (typeof value === "string") return value;
|
|
if (isRecord(value) && typeof value.import === "string") return value.import;
|
|
return null;
|
|
}
|
|
|
|
function shimSpecifier(file: string): string {
|
|
return path.join(packageDir, "src", "extensibility", file);
|
|
}
|
|
|
|
/**
|
|
* Derive the bundled legacy Pi module surface from current package exports.
|
|
* Named wildcard exports are expanded from source; root catch-alls stay out to
|
|
* avoid importing CLI entrypoints and other non-extension surfaces.
|
|
*/
|
|
export async function collectBundledPiEntries(): Promise<BundledPiEntry[]> {
|
|
const entries: BundledPiEntry[] = [];
|
|
const seenKeys = new Set<string>();
|
|
const seenBindings = new Set<string>();
|
|
function addEntry(key: string, binding: string, importSpecifier: string): void {
|
|
if (seenKeys.has(key)) return;
|
|
if (seenBindings.has(binding)) {
|
|
throw new Error(`Duplicate bundled Pi binding ${binding} for ${key}`);
|
|
}
|
|
seenKeys.add(key);
|
|
seenBindings.add(binding);
|
|
entries.push({ key, binding, importSpecifier });
|
|
}
|
|
|
|
for (const pkg of BUNDLED_PACKAGES) {
|
|
const packageRoot = path.join(repoRoot, "packages", pkg.dir);
|
|
const manifestPath = path.join(packageRoot, "package.json");
|
|
const manifest: unknown = await Bun.file(manifestPath).json();
|
|
if (!isRecord(manifest) || typeof manifest.name !== "string") {
|
|
throw new Error(`Bundled Pi package manifest has no name: ${manifestPath}`);
|
|
}
|
|
const exportsField = isRecord(manifest.exports) ? manifest.exports : {};
|
|
const rootSpecifier = pkg.rootShim ? shimSpecifier(pkg.rootShim) : manifest.name;
|
|
addEntry(manifest.name, `bundled${pkg.identifier}`, rootSpecifier);
|
|
|
|
for (const exportKey in exportsField) {
|
|
if (!exportKey.startsWith("./") || exportKey === "." || exportKey.includes("*")) continue;
|
|
const subpath = exportKey.slice(2);
|
|
const key = `${manifest.name}/${subpath}`;
|
|
addEntry(key, bindingForSubpath(pkg.identifier, subpath), key);
|
|
}
|
|
|
|
for (const exportKey in exportsField) {
|
|
if (!exportKey.startsWith("./") || exportKey === "." || !exportKey.includes("*")) continue;
|
|
const sourcePattern = exportImportTarget(exportsField[exportKey]);
|
|
if (!sourcePattern) continue;
|
|
const pattern = parseWildcardPattern(exportKey, sourcePattern);
|
|
if (!pattern || !/\.(ts|tsx|mts|cts|js|mjs|cjs|jsx)$/.test(pattern.sourceSuffix)) continue;
|
|
if (pattern.exportPrefix === "" || pattern.exportPrefix === "/") continue;
|
|
|
|
const sourceDir = path.join(packageRoot, pattern.sourcePrefix);
|
|
try {
|
|
const glob = new Bun.Glob(`*${pattern.sourceSuffix}`);
|
|
const matches: string[] = [];
|
|
for await (const match of glob.scan({ cwd: sourceDir, onlyFiles: true })) {
|
|
matches.push(match);
|
|
}
|
|
matches.sort();
|
|
for (const match of matches) {
|
|
if (!match.endsWith(pattern.sourceSuffix)) continue;
|
|
const basename = match.slice(0, match.length - pattern.sourceSuffix.length);
|
|
if (!isSafeWildcardBasename(basename) || basename.includes("/")) continue;
|
|
const subpath = `${pattern.exportPrefix}${basename}${pattern.exportSuffix}`;
|
|
const key = `${manifest.name}/${subpath}`;
|
|
addEntry(key, bindingForSubpath(pkg.identifier, subpath), key);
|
|
}
|
|
} catch (error) {
|
|
if (!isEnoent(error)) throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
addEntry(TYPEBOX_MODULE_KEY, "bundledTypeBoxShim", shimSpecifier(TYPEBOX_SHIM));
|
|
return entries;
|
|
}
|
|
|
|
function renderVirtualModule(entries: readonly BundledPiEntry[]): string {
|
|
const imports = entries.map(entry => `import * as ${entry.binding} from ${JSON.stringify(entry.importSpecifier)};`);
|
|
const modules = entries.map(entry => `\t${JSON.stringify(entry.key)}: ${entry.binding},`);
|
|
return [...imports, "", "export const BUNDLED_PI_MODULES = {", ...modules, "};", ""].join("\n");
|
|
}
|
|
|
|
/**
|
|
* Build plugin that materializes the legacy Pi module graph entirely in
|
|
* memory. Bun still needs static import edges at compile time, but no generated
|
|
* source or key-list file is written to the repository.
|
|
*/
|
|
export async function createLegacyPiVirtualModulePlugin(): Promise<Bun.BunPlugin> {
|
|
const source = renderVirtualModule(await collectBundledPiEntries());
|
|
return {
|
|
name: "omp:legacy-pi-modules",
|
|
setup(build) {
|
|
build.onResolve({ filter: /^omp-legacy-pi-modules$/ }, () => ({
|
|
path: LEGACY_PI_MODULES_SPECIFIER,
|
|
namespace: VIRTUAL_NAMESPACE,
|
|
}));
|
|
build.onLoad({ filter: /.*/, namespace: VIRTUAL_NAMESPACE }, () => ({ contents: source, loader: "ts" }));
|
|
},
|
|
};
|
|
}
|