mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-08 18:33:39 +02:00
refactor(cli): normalize route boundaries
This commit is contained in:
@@ -1,14 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"extends": "./tsconfig.package-boundary.paths.json",
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"paths": {
|
||||
"openclaw/extension-api": ["../src/extensionAPI.ts"],
|
||||
"openclaw/plugin-sdk": ["../src/plugin-sdk/index.ts"],
|
||||
"openclaw/plugin-sdk/*": ["../src/plugin-sdk/*.ts"],
|
||||
"openclaw/plugin-sdk/account-id": ["../src/plugin-sdk/account-id.ts"],
|
||||
"@openclaw/*": ["../extensions/*"],
|
||||
"@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"]
|
||||
}
|
||||
"ignoreDeprecations": "6.0"
|
||||
}
|
||||
}
|
||||
|
||||
13
extensions/tsconfig.package-boundary.paths.json
Normal file
13
extensions/tsconfig.package-boundary.paths.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"openclaw/extension-api": ["../src/extensionAPI.ts"],
|
||||
"openclaw/plugin-sdk": ["../src/plugin-sdk/index.ts"],
|
||||
"openclaw/plugin-sdk/*": ["../src/plugin-sdk/*.ts"],
|
||||
"openclaw/plugin-sdk/account-id": ["../src/plugin-sdk/account-id.ts"],
|
||||
"@openclaw/*": ["../extensions/*"],
|
||||
"@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,14 @@ describe("command-path-matches", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("treats structured rules without exact as prefix matches", () => {
|
||||
expect(
|
||||
matchesCommandPathRule(["plugins", "update", "now"], {
|
||||
pattern: ["plugins", "update"],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches any command path from a rule set", () => {
|
||||
expect(
|
||||
matchesAnyCommandPath(
|
||||
|
||||
@@ -5,12 +5,24 @@ export type StructuredCommandPathMatchRule = {
|
||||
|
||||
export type CommandPathMatchRule = readonly string[] | StructuredCommandPathMatchRule;
|
||||
|
||||
type NormalizedCommandPathMatchRule = {
|
||||
pattern: readonly string[];
|
||||
exact: boolean;
|
||||
};
|
||||
|
||||
function isStructuredCommandPathMatchRule(
|
||||
rule: CommandPathMatchRule,
|
||||
): rule is StructuredCommandPathMatchRule {
|
||||
return !Array.isArray(rule);
|
||||
}
|
||||
|
||||
function normalizeCommandPathMatchRule(rule: CommandPathMatchRule): NormalizedCommandPathMatchRule {
|
||||
if (!isStructuredCommandPathMatchRule(rule)) {
|
||||
return { pattern: rule, exact: false };
|
||||
}
|
||||
return { pattern: rule.pattern, exact: rule.exact ?? false };
|
||||
}
|
||||
|
||||
export function matchesCommandPath(
|
||||
commandPath: string[],
|
||||
pattern: readonly string[],
|
||||
@@ -23,10 +35,10 @@ export function matchesCommandPath(
|
||||
}
|
||||
|
||||
export function matchesCommandPathRule(commandPath: string[], rule: CommandPathMatchRule): boolean {
|
||||
if (!isStructuredCommandPathMatchRule(rule)) {
|
||||
return matchesCommandPath(commandPath, rule);
|
||||
}
|
||||
return matchesCommandPath(commandPath, rule.pattern, { exact: rule.exact });
|
||||
const normalizedRule = normalizeCommandPathMatchRule(rule);
|
||||
return matchesCommandPath(commandPath, normalizedRule.pattern, {
|
||||
exact: normalizedRule.exact,
|
||||
});
|
||||
}
|
||||
|
||||
export function matchesAnyCommandPath(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { matchesCommandPath } from "../command-path-matches.js";
|
||||
import { resolveCliCommandPathPolicy } from "../command-path-policy.js";
|
||||
import {
|
||||
routedCommandDefinitions,
|
||||
type RoutedCommandDefinition,
|
||||
type AnyRoutedCommandDefinition,
|
||||
} from "./routed-command-definitions.js";
|
||||
|
||||
export type RouteSpec = {
|
||||
@@ -22,7 +22,7 @@ function createCommandLoadPlugins(commandPath: readonly string[]): (argv: string
|
||||
|
||||
function createParsedRoute(params: {
|
||||
entry: CliCommandCatalogEntry;
|
||||
definition: RoutedCommandDefinition;
|
||||
definition: AnyRoutedCommandDefinition;
|
||||
}): RouteSpec {
|
||||
return {
|
||||
match: (path) =>
|
||||
@@ -51,6 +51,6 @@ export const routedCommands: RouteSpec[] = cliCommandCatalog
|
||||
.map((entry) =>
|
||||
createParsedRoute({
|
||||
entry,
|
||||
definition: routedCommandDefinitions[entry.route.id] as RoutedCommandDefinition,
|
||||
definition: routedCommandDefinitions[entry.route.id],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -11,16 +11,21 @@ import {
|
||||
parseStatusRouteArgs,
|
||||
} from "./route-args.js";
|
||||
|
||||
export type RoutedCommandDefinition<TArgs = unknown> = {
|
||||
parseArgs: (argv: string[]) => TArgs | null;
|
||||
runParsedArgs: (args: TArgs) => Promise<void>;
|
||||
type RouteArgParser<TArgs> = (argv: string[]) => TArgs | null;
|
||||
|
||||
type ParsedRouteArgs<TParse extends RouteArgParser<unknown>> = Exclude<ReturnType<TParse>, null>;
|
||||
|
||||
export type RoutedCommandDefinition<TParse extends RouteArgParser<unknown>> = {
|
||||
parseArgs: TParse;
|
||||
runParsedArgs: (args: ParsedRouteArgs<TParse>) => Promise<void>;
|
||||
};
|
||||
|
||||
function defineRoutedCommand<TParse extends (argv: string[]) => unknown>(definition: {
|
||||
parseArgs: TParse;
|
||||
runParsedArgs: (args: Exclude<ReturnType<TParse>, null>) => Promise<void>;
|
||||
}): RoutedCommandDefinition<Exclude<ReturnType<TParse>, null>> {
|
||||
return definition as RoutedCommandDefinition<Exclude<ReturnType<TParse>, null>>;
|
||||
export type AnyRoutedCommandDefinition = RoutedCommandDefinition<RouteArgParser<unknown>>;
|
||||
|
||||
function defineRoutedCommand<TParse extends RouteArgParser<unknown>>(
|
||||
definition: RoutedCommandDefinition<TParse>,
|
||||
): RoutedCommandDefinition<TParse> {
|
||||
return definition;
|
||||
}
|
||||
|
||||
export const routedCommandDefinitions = {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dirname, "../../..");
|
||||
const EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG =
|
||||
"extensions/tsconfig.package-boundary.paths.json" as const;
|
||||
const EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG =
|
||||
"extensions/tsconfig.package-boundary.base.json" as const;
|
||||
|
||||
type TsConfigJson = {
|
||||
extends?: unknown;
|
||||
@@ -28,10 +32,10 @@ function readJsonFile<T>(relativePath: string): T {
|
||||
}
|
||||
|
||||
describe("opt-in extension package boundaries", () => {
|
||||
it("keeps the opt-in extension base on real package resolution", () => {
|
||||
const tsconfig = readJsonFile<TsConfigJson>("extensions/tsconfig.package-boundary.base.json");
|
||||
expect(tsconfig.extends).toBe("../tsconfig.json");
|
||||
expect(tsconfig.compilerOptions?.paths).toEqual({
|
||||
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": ["../src/plugin-sdk/index.ts"],
|
||||
"openclaw/plugin-sdk/*": ["../src/plugin-sdk/*.ts"],
|
||||
@@ -39,17 +43,40 @@ describe("opt-in extension package boundaries", () => {
|
||||
"@openclaw/*": ["../extensions/*"],
|
||||
"@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"],
|
||||
});
|
||||
|
||||
const baseConfig = readJsonFile<TsConfigJson>(EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG);
|
||||
expect(baseConfig.extends).toBe("./tsconfig.package-boundary.paths.json");
|
||||
expect(baseConfig.compilerOptions).toEqual({
|
||||
ignoreDeprecations: "6.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("roots xai inside its own package and depends on the package sdk", () => {
|
||||
const tsconfig = readJsonFile<TsConfigJson>("extensions/xai/tsconfig.json");
|
||||
expect(tsconfig.extends).toBe("../tsconfig.package-boundary.base.json");
|
||||
expect(tsconfig.compilerOptions?.rootDir).toBe(".");
|
||||
expect(tsconfig.include).toEqual(["./*.ts", "./src/**/*.ts"]);
|
||||
expect(tsconfig.exclude).toEqual(["./**/*.test.ts", "./dist/**", "./node_modules/**"]);
|
||||
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 packageJson = readJsonFile<PackageJson>("extensions/xai/package.json");
|
||||
expect(packageJson.devDependencies?.["@openclaw/plugin-sdk"]).toBe("workspace:*");
|
||||
expect(optInExtensions).toEqual(["xai"]);
|
||||
|
||||
for (const extensionName of optInExtensions) {
|
||||
const tsconfig = readJsonFile<TsConfigJson>(`extensions/${extensionName}/tsconfig.json`);
|
||||
expect(tsconfig.compilerOptions?.rootDir).toBe(".");
|
||||
expect(tsconfig.include).toEqual(["./*.ts", "./src/**/*.ts"]);
|
||||
expect(tsconfig.exclude).toEqual(["./**/*.test.ts", "./dist/**", "./node_modules/**"]);
|
||||
|
||||
const packageJson = readJsonFile<PackageJson>(`extensions/${extensionName}/package.json`);
|
||||
expect(packageJson.devDependencies?.["@openclaw/plugin-sdk"]).toBe("workspace:*");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps plugin-sdk package types generated from the package build, not a hand-maintained types bridge", () => {
|
||||
|
||||
Reference in New Issue
Block a user