mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix(plugins): repair package boundary sdk paths
This commit is contained in:
120
scripts/lib/extension-package-boundary.ts
Normal file
120
scripts/lib/extension-package-boundary.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
export const EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG =
|
||||
"extensions/tsconfig.package-boundary.base.json" as const;
|
||||
|
||||
export const EXTENSION_PACKAGE_BOUNDARY_INCLUDE = ["./*.ts", "./src/**/*.ts"] as const;
|
||||
export const EXTENSION_PACKAGE_BOUNDARY_EXCLUDE = [
|
||||
"./**/*.test.ts",
|
||||
"./dist/**",
|
||||
"./node_modules/**",
|
||||
] as const;
|
||||
export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
|
||||
"openclaw/extension-api": ["../src/extensionAPI.ts"],
|
||||
"openclaw/plugin-sdk": ["../packages/plugin-sdk/dist/src/plugin-sdk/index.d.ts"],
|
||||
"openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/src/plugin-sdk/*.d.ts"],
|
||||
"openclaw/plugin-sdk/account-id": ["../src/plugin-sdk/account-id.ts"],
|
||||
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
|
||||
"@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/src/plugin-sdk/*.d.ts"],
|
||||
} as const;
|
||||
|
||||
export type ExtensionPackageBoundaryTsConfigJson = {
|
||||
extends?: unknown;
|
||||
compilerOptions?: {
|
||||
rootDir?: unknown;
|
||||
paths?: unknown;
|
||||
};
|
||||
include?: unknown;
|
||||
exclude?: unknown;
|
||||
};
|
||||
|
||||
export type ExtensionPackageBoundaryPackageJson = {
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
function readJsonFile<T>(filePath: string): T {
|
||||
return JSON.parse(readFileSync(filePath, "utf8")) as T;
|
||||
}
|
||||
|
||||
export function collectBundledExtensionIds(rootDir = resolve(".")): string[] {
|
||||
return readdirSync(join(rootDir, "extensions"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
export function resolveExtensionTsconfigPath(extensionId: string, rootDir = resolve(".")): string {
|
||||
return join(rootDir, "extensions", extensionId, "tsconfig.json");
|
||||
}
|
||||
|
||||
export function resolveExtensionPackageJsonPath(
|
||||
extensionId: string,
|
||||
rootDir = resolve("."),
|
||||
): string {
|
||||
return join(rootDir, "extensions", extensionId, "package.json");
|
||||
}
|
||||
|
||||
export function readExtensionPackageBoundaryTsconfig(
|
||||
extensionId: string,
|
||||
rootDir = resolve("."),
|
||||
): ExtensionPackageBoundaryTsConfigJson {
|
||||
return readJsonFile<ExtensionPackageBoundaryTsConfigJson>(
|
||||
resolveExtensionTsconfigPath(extensionId, rootDir),
|
||||
);
|
||||
}
|
||||
|
||||
export function readExtensionPackageBoundaryPackageJson(
|
||||
extensionId: string,
|
||||
rootDir = resolve("."),
|
||||
): ExtensionPackageBoundaryPackageJson {
|
||||
return readJsonFile<ExtensionPackageBoundaryPackageJson>(
|
||||
resolveExtensionPackageJsonPath(extensionId, rootDir),
|
||||
);
|
||||
}
|
||||
|
||||
export function isOptInExtensionPackageBoundaryTsconfig(
|
||||
tsconfig: ExtensionPackageBoundaryTsConfigJson,
|
||||
): boolean {
|
||||
return tsconfig.extends === "../tsconfig.package-boundary.base.json";
|
||||
}
|
||||
|
||||
export function collectExtensionsWithTsconfig(rootDir = resolve(".")): string[] {
|
||||
return collectBundledExtensionIds(rootDir).filter((extensionId) =>
|
||||
existsSync(resolveExtensionTsconfigPath(extensionId, rootDir)),
|
||||
);
|
||||
}
|
||||
|
||||
export function collectOptInExtensionPackageBoundaries(rootDir = resolve(".")): string[] {
|
||||
return collectExtensionsWithTsconfig(rootDir).filter((extensionId) =>
|
||||
isOptInExtensionPackageBoundaryTsconfig(
|
||||
readExtensionPackageBoundaryTsconfig(extensionId, rootDir),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function renderExtensionPackageBoundaryTsconfig(params?: {
|
||||
paths?: Record<string, string[]>;
|
||||
}): {
|
||||
extends: "../tsconfig.package-boundary.base.json";
|
||||
compilerOptions: { rootDir: "."; paths?: Record<string, string[]> };
|
||||
include: typeof EXTENSION_PACKAGE_BOUNDARY_INCLUDE;
|
||||
exclude: typeof EXTENSION_PACKAGE_BOUNDARY_EXCLUDE;
|
||||
} {
|
||||
return {
|
||||
extends: "../tsconfig.package-boundary.base.json",
|
||||
compilerOptions: {
|
||||
rootDir: ".",
|
||||
...(params?.paths
|
||||
? {
|
||||
paths: {
|
||||
...EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS,
|
||||
...params.paths,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
include: EXTENSION_PACKAGE_BOUNDARY_INCLUDE,
|
||||
exclude: EXTENSION_PACKAGE_BOUNDARY_EXCLUDE,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectExtensionsWithTsconfig,
|
||||
collectOptInExtensionPackageBoundaries,
|
||||
EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS,
|
||||
EXTENSION_PACKAGE_BOUNDARY_EXCLUDE,
|
||||
EXTENSION_PACKAGE_BOUNDARY_INCLUDE,
|
||||
isOptInExtensionPackageBoundaryTsconfig,
|
||||
readExtensionPackageBoundaryPackageJson,
|
||||
readExtensionPackageBoundaryTsconfig,
|
||||
} from "../../../scripts/lib/extension-package-boundary.ts";
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dirname, "../../..");
|
||||
const EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG =
|
||||
@@ -35,14 +45,7 @@ describe("opt-in extension package boundaries", () => {
|
||||
it("keeps path aliases in a dedicated shared config", () => {
|
||||
const pathsConfig = readJsonFile<TsConfigJson>(EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG);
|
||||
expect(pathsConfig.extends).toBe("../tsconfig.json");
|
||||
expect(pathsConfig.compilerOptions?.paths).toEqual({
|
||||
"openclaw/extension-api": ["../src/extensionAPI.ts"],
|
||||
"openclaw/plugin-sdk": ["../packages/plugin-sdk/dist/src/plugin-sdk/index.d.ts"],
|
||||
"openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/src/plugin-sdk/*.d.ts"],
|
||||
"openclaw/plugin-sdk/account-id": ["../src/plugin-sdk/account-id.ts"],
|
||||
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
|
||||
"@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/src/plugin-sdk/*.d.ts"],
|
||||
});
|
||||
expect(pathsConfig.compilerOptions?.paths).toEqual(EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS);
|
||||
|
||||
const baseConfig = readJsonFile<TsConfigJson>(EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG);
|
||||
expect(baseConfig.extends).toBe("./tsconfig.package-boundary.paths.json");
|
||||
@@ -52,29 +55,19 @@ describe("opt-in extension package boundaries", () => {
|
||||
});
|
||||
|
||||
it("keeps every opt-in extension rooted inside its package and on the package sdk", () => {
|
||||
const optInExtensions = readdirSync(resolve(REPO_ROOT, "extensions"), {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.filter((extensionName) => {
|
||||
const tsconfigPath = `extensions/${extensionName}/tsconfig.json`;
|
||||
if (!existsSync(resolve(REPO_ROOT, tsconfigPath))) {
|
||||
return false;
|
||||
}
|
||||
const tsconfig = readJsonFile<TsConfigJson>(tsconfigPath);
|
||||
return tsconfig.extends === "../tsconfig.package-boundary.base.json";
|
||||
});
|
||||
const extensionsWithTsconfig = collectExtensionsWithTsconfig(REPO_ROOT);
|
||||
const optInExtensions = collectOptInExtensionPackageBoundaries(REPO_ROOT);
|
||||
|
||||
expect(optInExtensions).toEqual(["xai"]);
|
||||
expect(extensionsWithTsconfig).toEqual(optInExtensions);
|
||||
|
||||
for (const extensionName of optInExtensions) {
|
||||
const tsconfig = readJsonFile<TsConfigJson>(`extensions/${extensionName}/tsconfig.json`);
|
||||
const tsconfig = readExtensionPackageBoundaryTsconfig(extensionName, REPO_ROOT);
|
||||
expect(isOptInExtensionPackageBoundaryTsconfig(tsconfig)).toBe(true);
|
||||
expect(tsconfig.compilerOptions?.rootDir).toBe(".");
|
||||
expect(tsconfig.include).toEqual(["./*.ts", "./src/**/*.ts"]);
|
||||
expect(tsconfig.exclude).toEqual(["./**/*.test.ts", "./dist/**", "./node_modules/**"]);
|
||||
expect(tsconfig.include).toEqual([...EXTENSION_PACKAGE_BOUNDARY_INCLUDE]);
|
||||
expect(tsconfig.exclude).toEqual([...EXTENSION_PACKAGE_BOUNDARY_EXCLUDE]);
|
||||
|
||||
const packageJson = readJsonFile<PackageJson>(`extensions/${extensionName}/package.json`);
|
||||
const packageJson = readExtensionPackageBoundaryPackageJson(extensionName, REPO_ROOT);
|
||||
expect(packageJson.devDependencies?.["@openclaw/plugin-sdk"]).toBe("workspace:*");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,13 @@ import { rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectOptInExtensionPackageBoundaries } from "../scripts/lib/extension-package-boundary.ts";
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dirname, "..");
|
||||
const XAI_ROOT = resolve(REPO_ROOT, "extensions/xai");
|
||||
const require = createRequire(import.meta.url);
|
||||
const TSC_BIN = require.resolve("typescript/bin/tsc");
|
||||
const PLUGIN_SDK_PACKAGE_TSCONFIG = resolve(REPO_ROOT, "packages/plugin-sdk/tsconfig.json");
|
||||
const OPT_IN_EXTENSION_IDS = collectOptInExtensionPackageBoundaries(REPO_ROOT);
|
||||
|
||||
function runTsc(args: string[]) {
|
||||
return spawnSync(process.execPath, [TSC_BIN, ...args], {
|
||||
@@ -17,48 +18,57 @@ function runTsc(args: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
describe("xai package TypeScript boundary", () => {
|
||||
it("typechecks cleanly through @openclaw/plugin-sdk", () => {
|
||||
describe("opt-in extension package TypeScript boundaries", () => {
|
||||
it("typechecks each opt-in extension cleanly through @openclaw/plugin-sdk", () => {
|
||||
const prepareResult = runTsc(["-p", PLUGIN_SDK_PACKAGE_TSCONFIG]);
|
||||
expect(prepareResult.status, `${prepareResult.stdout}\n${prepareResult.stderr}`).toBe(0);
|
||||
|
||||
const result = runTsc(["-p", resolve(XAI_ROOT, "tsconfig.json"), "--noEmit"]);
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
});
|
||||
|
||||
it("fails when xai imports src/cli through a relative path", () => {
|
||||
const canaryPath = resolve(XAI_ROOT, "__rootdir_boundary_canary__.ts");
|
||||
const tsconfigPath = resolve(XAI_ROOT, "tsconfig.rootdir-canary.json");
|
||||
|
||||
try {
|
||||
writeFileSync(
|
||||
canaryPath,
|
||||
'import * as foo from "../../src/cli/acp-cli.ts";\nvoid foo;\nexport {};\n',
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
tsconfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
extends: "./tsconfig.json",
|
||||
include: ["./__rootdir_boundary_canary__.ts"],
|
||||
exclude: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = runTsc(["-p", tsconfigPath, "--noEmit"]);
|
||||
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(output).toContain("TS6059");
|
||||
expect(output).toContain("src/cli/acp-cli.ts");
|
||||
} finally {
|
||||
rmSync(canaryPath, { force: true });
|
||||
rmSync(tsconfigPath, { force: true });
|
||||
for (const extensionId of OPT_IN_EXTENSION_IDS) {
|
||||
const result = runTsc([
|
||||
"-p",
|
||||
resolve(REPO_ROOT, "extensions", extensionId, "tsconfig.json"),
|
||||
"--noEmit",
|
||||
]);
|
||||
expect(result.status, `${extensionId}\n${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(OPT_IN_EXTENSION_IDS)(
|
||||
"fails when %s imports src/cli through a relative path",
|
||||
(extensionId) => {
|
||||
const extensionRoot = resolve(REPO_ROOT, "extensions", extensionId);
|
||||
const canaryPath = resolve(extensionRoot, "__rootdir_boundary_canary__.ts");
|
||||
const tsconfigPath = resolve(extensionRoot, "tsconfig.rootdir-canary.json");
|
||||
|
||||
try {
|
||||
writeFileSync(
|
||||
canaryPath,
|
||||
'import * as foo from "../../src/cli/acp-cli.ts";\nvoid foo;\nexport {};\n',
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
tsconfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
extends: "./tsconfig.json",
|
||||
include: ["./__rootdir_boundary_canary__.ts"],
|
||||
exclude: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = runTsc(["-p", tsconfigPath, "--noEmit"]);
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(output).toContain("TS6059");
|
||||
expect(output).toContain("src/cli/acp-cli.ts");
|
||||
} finally {
|
||||
rmSync(canaryPath, { force: true });
|
||||
rmSync(tsconfigPath, { force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user