Stabilize plugin loader and Docker extension smoke (#50058)
* Plugins: stabilize Area 6 loader and Docker smoke * Docker: fail fast on extension npm install errors * Tests: stabilize loader non-native Jiti boundary CI timeout * Tests: stabilize plugin loader Jiti source-runtime coverage * Docker: keep extension deps on lockfile graph * Tests: cover tsx-cache renamed package cwd fallback * Tests: stabilize plugin-sdk export subpath assertions * Plugins: align tsx-cache alias fallback with subpath fallback * Tests: normalize guardrail path checks for Windows * Plugins: restrict plugin-sdk cwd fallback to trusted roots * Tests: exempt outbound-session from extension import guard * Tests: tighten guardrails and cli-entry trust coverage * Tests: guard optional loader fixture exports * Tests: make loader fixture package exports null-safe * Tests: make loader fixture package exports null-safe * Tests: make loader fixture package exports null-safe * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
parent
3abffe0967
commit
a2a9a553e1
45
.github/workflows/install-smoke.yml
vendored
45
.github/workflows/install-smoke.yml
vendored
@ -62,24 +62,57 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
|
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
|
||||||
|
|
||||||
# This smoke only validates that the build-arg path preinstalls selected
|
# This smoke validates that the build-arg path preinstalls selected
|
||||||
# extension deps without breaking image build or basic CLI startup. It
|
# extension deps and that matrix plugin discovery stays healthy in the
|
||||||
# does not exercise runtime loading/registration of diagnostics-otel.
|
# final runtime image.
|
||||||
- name: Build extension Dockerfile smoke image
|
- name: Build extension Dockerfile smoke image
|
||||||
uses: useblacksmith/build-push-action@v2
|
uses: useblacksmith/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
OPENCLAW_EXTENSIONS=diagnostics-otel
|
OPENCLAW_EXTENSIONS=matrix
|
||||||
tags: openclaw-ext-smoke:local
|
tags: openclaw-ext-smoke:local
|
||||||
load: true
|
load: true
|
||||||
push: false
|
push: false
|
||||||
provenance: false
|
provenance: false
|
||||||
|
|
||||||
- name: Smoke test Dockerfile with extension build arg
|
- name: Smoke test Dockerfile with matrix extension build arg
|
||||||
run: |
|
run: |
|
||||||
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc 'which openclaw && openclaw --version'
|
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
|
||||||
|
which openclaw &&
|
||||||
|
openclaw --version &&
|
||||||
|
node -e "
|
||||||
|
const Module = require(\"node:module\");
|
||||||
|
const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\");
|
||||||
|
requireFromMatrix.resolve(\"@vector-im/matrix-bot-sdk/package.json\");
|
||||||
|
requireFromMatrix.resolve(\"@matrix-org/matrix-sdk-crypto-nodejs/package.json\");
|
||||||
|
const { spawnSync } = require(\"node:child_process\");
|
||||||
|
const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" });
|
||||||
|
if (run.status !== 0) {
|
||||||
|
process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\");
|
||||||
|
process.exit(run.status ?? 1);
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(run.stdout);
|
||||||
|
const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\");
|
||||||
|
if (!matrix) {
|
||||||
|
throw new Error(\"matrix plugin missing from bundled plugin list\");
|
||||||
|
}
|
||||||
|
const matrixDiag = (parsed.diagnostics || []).filter(
|
||||||
|
(diag) =>
|
||||||
|
typeof diag.source === \"string\" &&
|
||||||
|
diag.source.includes(\"/extensions/matrix\") &&
|
||||||
|
typeof diag.message === \"string\" &&
|
||||||
|
diag.message.includes(\"extension entry escapes package directory\"),
|
||||||
|
);
|
||||||
|
if (matrixDiag.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
\"unexpected matrix diagnostics: \" +
|
||||||
|
matrixDiag.map((diag) => diag.message).join(\"; \"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
'
|
||||||
|
|
||||||
- name: Build installer smoke image
|
- name: Build installer smoke image
|
||||||
uses: useblacksmith/build-push-action@v2
|
uses: useblacksmith/build-push-action@v2
|
||||||
|
|||||||
@ -134,6 +134,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Gateway/channels: serialize per-account channel startup so overlapping starts do not boot the same provider twice, preventing MS Teams `EADDRINUSE` crash loops during startup and restart. (#49583) Thanks @sudie-codes.
|
- Gateway/channels: serialize per-account channel startup so overlapping starts do not boot the same provider twice, preventing MS Teams `EADDRINUSE` crash loops during startup and restart. (#49583) Thanks @sudie-codes.
|
||||||
- Tests/OpenAI Codex auth: align login expectations with the default `gpt-5.4` model so CI coverage stays consistent with the current OpenAI Codex default. (#44367) Thanks @jrrcdev.
|
- Tests/OpenAI Codex auth: align login expectations with the default `gpt-5.4` model so CI coverage stays consistent with the current OpenAI Codex default. (#44367) Thanks @jrrcdev.
|
||||||
- Discord: enforce strict DM component allowlist auth (#49997) Thanks @joshavant.
|
- Discord: enforce strict DM component allowlist auth (#49997) Thanks @joshavant.
|
||||||
|
- Stabilize plugin loader and Docker extension smoke (#50058) Thanks @joshavant.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
|||||||
@ -146,6 +146,10 @@ COPY --from=runtime-assets --chown=node:node /app/extensions ./extensions
|
|||||||
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
|
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
|
||||||
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
|
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
|
||||||
|
|
||||||
|
# In npm-installed Docker images, prefer the copied source extension tree for
|
||||||
|
# bundled discovery so package metadata that points at source entries stays valid.
|
||||||
|
ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/extensions
|
||||||
|
|
||||||
# Keep pnpm available in the runtime image for container-local workflows.
|
# Keep pnpm available in the runtime image for container-local workflows.
|
||||||
# Use a shared Corepack home so the non-root `node` user does not need a
|
# Use a shared Corepack home so the non-root `node` user does not need a
|
||||||
# first-run network fetch when invoking pnpm.
|
# first-run network fetch when invoking pnpm.
|
||||||
|
|||||||
@ -41,11 +41,17 @@ describe("Dockerfile", () => {
|
|||||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||||
expect(dockerfile).toContain("FROM build AS runtime-assets");
|
expect(dockerfile).toContain("FROM build AS runtime-assets");
|
||||||
expect(dockerfile).toContain("CI=true pnpm prune --prod");
|
expect(dockerfile).toContain("CI=true pnpm prune --prod");
|
||||||
|
expect(dockerfile).not.toContain('npm install --prefix "extensions/$ext" --omit=dev --silent');
|
||||||
expect(dockerfile).toContain(
|
expect(dockerfile).toContain(
|
||||||
"COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules",
|
"COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pins bundled plugin discovery to copied source extensions in runtime images", async () => {
|
||||||
|
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||||
|
expect(dockerfile).toContain("ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/extensions");
|
||||||
|
});
|
||||||
|
|
||||||
it("normalizes plugin and agent paths permissions in image layers", async () => {
|
it("normalizes plugin and agent paths permissions in image layers", async () => {
|
||||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||||
expect(dockerfile).toContain("for dir in /app/extensions /app/.agent /app/.agents");
|
expect(dockerfile).toContain("for dir in /app/extensions /app/.agent /app/.agents");
|
||||||
|
|||||||
@ -170,6 +170,10 @@ function readSource(path: string): string {
|
|||||||
return readFileSync(resolve(ROOT_DIR, "..", path), "utf8");
|
return readFileSync(resolve(ROOT_DIR, "..", path), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePath(path: string): string {
|
||||||
|
return path.replaceAll("\\", "/");
|
||||||
|
}
|
||||||
|
|
||||||
function readSetupBarrelImportBlock(path: string): string {
|
function readSetupBarrelImportBlock(path: string): string {
|
||||||
const lines = readSource(path).split("\n");
|
const lines = readSource(path).split("\n");
|
||||||
const targetLineIndex = lines.findIndex((line) =>
|
const targetLineIndex = lines.findIndex((line) =>
|
||||||
@ -186,10 +190,10 @@ function readSetupBarrelImportBlock(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectExtensionSourceFiles(): string[] {
|
function collectExtensionSourceFiles(): string[] {
|
||||||
const extensionsDir = resolve(ROOT_DIR, "..", "extensions");
|
const extensionsDir = normalizePath(resolve(ROOT_DIR, "..", "extensions"));
|
||||||
const sharedExtensionsDir = resolve(extensionsDir, "shared");
|
const sharedExtensionsDir = normalizePath(resolve(extensionsDir, "shared"));
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
const stack = [extensionsDir];
|
const stack = [resolve(ROOT_DIR, "..", "extensions")];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
const current = stack.pop();
|
const current = stack.pop();
|
||||||
if (!current) {
|
if (!current) {
|
||||||
@ -197,6 +201,7 @@ function collectExtensionSourceFiles(): string[] {
|
|||||||
}
|
}
|
||||||
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||||
const fullPath = resolve(current, entry.name);
|
const fullPath = resolve(current, entry.name);
|
||||||
|
const normalizedFullPath = normalizePath(fullPath);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
||||||
continue;
|
continue;
|
||||||
@ -207,18 +212,18 @@ function collectExtensionSourceFiles(): string[] {
|
|||||||
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
|
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (entry.name.endsWith(".d.ts") || fullPath.includes(sharedExtensionsDir)) {
|
if (entry.name.endsWith(".d.ts") || normalizedFullPath.includes(sharedExtensionsDir)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (fullPath.includes(`${resolve(ROOT_DIR, "..", "extensions")}/shared/`)) {
|
if (normalizedFullPath.includes(`${extensionsDir}/shared/`)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
fullPath.includes(".test.") ||
|
normalizedFullPath.includes(".test.") ||
|
||||||
fullPath.includes(".test-") ||
|
normalizedFullPath.includes(".test-") ||
|
||||||
fullPath.includes(".fixture.") ||
|
normalizedFullPath.includes(".fixture.") ||
|
||||||
fullPath.includes(".snap") ||
|
normalizedFullPath.includes(".snap") ||
|
||||||
fullPath.includes("test-support") ||
|
normalizedFullPath.includes("test-support") ||
|
||||||
entry.name === "api.ts" ||
|
entry.name === "api.ts" ||
|
||||||
entry.name === "runtime-api.ts"
|
entry.name === "runtime-api.ts"
|
||||||
) {
|
) {
|
||||||
@ -232,6 +237,7 @@ function collectExtensionSourceFiles(): string[] {
|
|||||||
|
|
||||||
function collectCoreSourceFiles(): string[] {
|
function collectCoreSourceFiles(): string[] {
|
||||||
const srcDir = resolve(ROOT_DIR, "..", "src");
|
const srcDir = resolve(ROOT_DIR, "..", "src");
|
||||||
|
const normalizedPluginSdkDir = normalizePath(resolve(ROOT_DIR, "plugin-sdk"));
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
const stack = [srcDir];
|
const stack = [srcDir];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
@ -241,6 +247,7 @@ function collectCoreSourceFiles(): string[] {
|
|||||||
}
|
}
|
||||||
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||||
const fullPath = resolve(current, entry.name);
|
const fullPath = resolve(current, entry.name);
|
||||||
|
const normalizedFullPath = normalizePath(fullPath);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
||||||
continue;
|
continue;
|
||||||
@ -255,14 +262,14 @@ function collectCoreSourceFiles(): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
fullPath.includes(".test.") ||
|
normalizedFullPath.includes(".test.") ||
|
||||||
fullPath.includes(".mock-harness.") ||
|
normalizedFullPath.includes(".mock-harness.") ||
|
||||||
fullPath.includes(".spec.") ||
|
normalizedFullPath.includes(".spec.") ||
|
||||||
fullPath.includes(".fixture.") ||
|
normalizedFullPath.includes(".fixture.") ||
|
||||||
fullPath.includes(".snap") ||
|
normalizedFullPath.includes(".snap") ||
|
||||||
// src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated
|
// src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated
|
||||||
// plugin-sdk guardrails instead of the generic "core should not touch extensions" rule.
|
// plugin-sdk guardrails instead of the generic "core should not touch extensions" rule.
|
||||||
fullPath.includes(`${resolve(ROOT_DIR, "plugin-sdk")}/`)
|
normalizedFullPath.includes(`${normalizedPluginSdkDir}/`)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -283,6 +290,7 @@ function collectExtensionFiles(extensionId: string): string[] {
|
|||||||
}
|
}
|
||||||
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||||
const fullPath = resolve(current, entry.name);
|
const fullPath = resolve(current, entry.name);
|
||||||
|
const normalizedFullPath = normalizePath(fullPath);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
||||||
continue;
|
continue;
|
||||||
@ -297,11 +305,11 @@ function collectExtensionFiles(extensionId: string): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
fullPath.includes(".test.") ||
|
normalizedFullPath.includes(".test.") ||
|
||||||
fullPath.includes(".test-") ||
|
normalizedFullPath.includes(".test-") ||
|
||||||
fullPath.includes(".spec.") ||
|
normalizedFullPath.includes(".spec.") ||
|
||||||
fullPath.includes(".fixture.") ||
|
normalizedFullPath.includes(".fixture.") ||
|
||||||
fullPath.includes(".snap") ||
|
normalizedFullPath.includes(".snap") ||
|
||||||
entry.name === "runtime-api.ts"
|
entry.name === "runtime-api.ts"
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
@ -392,6 +400,16 @@ describe("channel import guardrails", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps bundled extension source files off legacy core send-deps src imports", () => {
|
||||||
|
const legacyCoreSendDepsImport = /["'][^"']*src\/infra\/outbound\/send-deps\.[cm]?[jt]s["']/;
|
||||||
|
for (const file of collectExtensionSourceFiles()) {
|
||||||
|
const text = readFileSync(file, "utf8");
|
||||||
|
expect(text, `${file} should not import src/infra/outbound/send-deps.*`).not.toMatch(
|
||||||
|
legacyCoreSendDepsImport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps core production files off extension private src imports", () => {
|
it("keeps core production files off extension private src imports", () => {
|
||||||
for (const file of collectCoreSourceFiles()) {
|
for (const file of collectCoreSourceFiles()) {
|
||||||
const text = readFileSync(file, "utf8");
|
const text = readFileSync(file, "utf8");
|
||||||
|
|||||||
@ -101,6 +101,16 @@ function makeTempDir() {
|
|||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withCwd<T>(cwd: string, run: () => T): T {
|
||||||
|
const previousCwd = process.cwd();
|
||||||
|
process.chdir(cwd);
|
||||||
|
try {
|
||||||
|
return run();
|
||||||
|
} finally {
|
||||||
|
process.chdir(previousCwd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function writePlugin(params: {
|
function writePlugin(params: {
|
||||||
id: string;
|
id: string;
|
||||||
body: string;
|
body: string;
|
||||||
@ -299,17 +309,43 @@ function createPluginSdkAliasFixture(params?: {
|
|||||||
distFile?: string;
|
distFile?: string;
|
||||||
srcBody?: string;
|
srcBody?: string;
|
||||||
distBody?: string;
|
distBody?: string;
|
||||||
|
packageName?: string;
|
||||||
|
packageExports?: Record<string, unknown>;
|
||||||
|
trustedRootIndicators?: boolean;
|
||||||
|
trustedRootIndicatorMode?: "bin+marker" | "cli-entry-only" | "none";
|
||||||
}) {
|
}) {
|
||||||
const root = makeTempDir();
|
const root = makeTempDir();
|
||||||
const srcFile = path.join(root, "src", "plugin-sdk", params?.srcFile ?? "index.ts");
|
const srcFile = path.join(root, "src", "plugin-sdk", params?.srcFile ?? "index.ts");
|
||||||
const distFile = path.join(root, "dist", "plugin-sdk", params?.distFile ?? "index.js");
|
const distFile = path.join(root, "dist", "plugin-sdk", params?.distFile ?? "index.js");
|
||||||
mkdirSafe(path.dirname(srcFile));
|
mkdirSafe(path.dirname(srcFile));
|
||||||
mkdirSafe(path.dirname(distFile));
|
mkdirSafe(path.dirname(distFile));
|
||||||
fs.writeFileSync(
|
const trustedRootIndicatorMode =
|
||||||
path.join(root, "package.json"),
|
params?.trustedRootIndicatorMode ??
|
||||||
JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
|
(params?.trustedRootIndicators === false ? "none" : "bin+marker");
|
||||||
"utf-8",
|
const packageJson: Record<string, unknown> = {
|
||||||
);
|
name: params?.packageName ?? "openclaw",
|
||||||
|
type: "module",
|
||||||
|
};
|
||||||
|
if (trustedRootIndicatorMode === "bin+marker") {
|
||||||
|
packageJson.bin = {
|
||||||
|
openclaw: "openclaw.mjs",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (params?.packageExports || trustedRootIndicatorMode === "cli-entry-only") {
|
||||||
|
const trustedExports: Record<string, unknown> =
|
||||||
|
trustedRootIndicatorMode === "cli-entry-only"
|
||||||
|
? { "./cli-entry": { default: "./dist/cli-entry.js" } }
|
||||||
|
: {};
|
||||||
|
packageJson.exports = {
|
||||||
|
"./plugin-sdk": { default: "./dist/plugin-sdk/index.js" },
|
||||||
|
...trustedExports,
|
||||||
|
...params?.packageExports,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify(packageJson, null, 2), "utf-8");
|
||||||
|
if (trustedRootIndicatorMode === "bin+marker") {
|
||||||
|
fs.writeFileSync(path.join(root, "openclaw.mjs"), "export {};\n", "utf-8");
|
||||||
|
}
|
||||||
fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8");
|
fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8");
|
||||||
fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8");
|
fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8");
|
||||||
return { root, srcFile, distFile };
|
return { root, srcFile, distFile };
|
||||||
@ -3326,10 +3362,126 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("derives plugin-sdk subpaths from package exports", () => {
|
it("derives plugin-sdk subpaths from package exports", () => {
|
||||||
const subpaths = __testing.listPluginSdkExportedSubpaths();
|
const fixture = createPluginSdkAliasFixture({
|
||||||
expect(subpaths).toContain("telegram");
|
packageExports: {
|
||||||
expect(subpaths).not.toContain("compat");
|
"./plugin-sdk/compat": { default: "./dist/plugin-sdk/compat.js" },
|
||||||
expect(subpaths).not.toContain("root-alias");
|
"./plugin-sdk/telegram": { default: "./dist/plugin-sdk/telegram.js" },
|
||||||
|
"./plugin-sdk/nested/value": { default: "./dist/plugin-sdk/nested/value.js" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const subpaths = __testing.listPluginSdkExportedSubpaths({
|
||||||
|
modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"),
|
||||||
|
});
|
||||||
|
expect(subpaths).toEqual(["compat", "telegram"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives plugin-sdk subpaths from nearest package exports even when package name is renamed", () => {
|
||||||
|
const fixture = createPluginSdkAliasFixture({
|
||||||
|
packageName: "moltbot",
|
||||||
|
packageExports: {
|
||||||
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
||||||
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
||||||
|
"./plugin-sdk/compat": { default: "./dist/plugin-sdk/compat.js" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const subpaths = __testing.listPluginSdkExportedSubpaths({
|
||||||
|
modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"),
|
||||||
|
});
|
||||||
|
expect(subpaths).toEqual(["channel-runtime", "compat", "core"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives plugin-sdk subpaths via cwd fallback when module path is a transpiler cache and package is renamed", () => {
|
||||||
|
const fixture = createPluginSdkAliasFixture({
|
||||||
|
packageName: "moltbot",
|
||||||
|
packageExports: {
|
||||||
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
||||||
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const subpaths = withCwd(fixture.root, () =>
|
||||||
|
__testing.listPluginSdkExportedSubpaths({
|
||||||
|
modulePath: "/tmp/tsx-cache/openclaw-loader.js",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(subpaths).toEqual(["channel-runtime", "core"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves plugin-sdk alias files via cwd fallback when module path is a transpiler cache and package is renamed", () => {
|
||||||
|
const fixture = createPluginSdkAliasFixture({
|
||||||
|
srcFile: "channel-runtime.ts",
|
||||||
|
distFile: "channel-runtime.js",
|
||||||
|
packageName: "moltbot",
|
||||||
|
packageExports: {
|
||||||
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const resolved = withCwd(fixture.root, () =>
|
||||||
|
resolvePluginSdkAlias({
|
||||||
|
root: fixture.root,
|
||||||
|
srcFile: "channel-runtime.ts",
|
||||||
|
distFile: "channel-runtime.js",
|
||||||
|
modulePath: "/tmp/tsx-cache/openclaw-loader.js",
|
||||||
|
env: { NODE_ENV: undefined },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(resolved).not.toBeNull();
|
||||||
|
expect(fs.realpathSync(resolved ?? "")).toBe(fs.realpathSync(fixture.srcFile));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not derive plugin-sdk subpaths from cwd fallback when package root is not an OpenClaw root", () => {
|
||||||
|
const fixture = createPluginSdkAliasFixture({
|
||||||
|
packageName: "moltbot",
|
||||||
|
trustedRootIndicators: false,
|
||||||
|
packageExports: {
|
||||||
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
||||||
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const subpaths = withCwd(fixture.root, () =>
|
||||||
|
__testing.listPluginSdkExportedSubpaths({
|
||||||
|
modulePath: "/tmp/tsx-cache/openclaw-loader.js",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(subpaths).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives plugin-sdk subpaths via cwd fallback when trusted root indicator is cli-entry export", () => {
|
||||||
|
const fixture = createPluginSdkAliasFixture({
|
||||||
|
packageName: "moltbot",
|
||||||
|
trustedRootIndicatorMode: "cli-entry-only",
|
||||||
|
packageExports: {
|
||||||
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
||||||
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const subpaths = withCwd(fixture.root, () =>
|
||||||
|
__testing.listPluginSdkExportedSubpaths({
|
||||||
|
modulePath: "/tmp/tsx-cache/openclaw-loader.js",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(subpaths).toEqual(["channel-runtime", "core"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve plugin-sdk alias files from cwd fallback when package root is not an OpenClaw root", () => {
|
||||||
|
const fixture = createPluginSdkAliasFixture({
|
||||||
|
srcFile: "channel-runtime.ts",
|
||||||
|
distFile: "channel-runtime.js",
|
||||||
|
packageName: "moltbot",
|
||||||
|
trustedRootIndicators: false,
|
||||||
|
packageExports: {
|
||||||
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const resolved = withCwd(fixture.root, () =>
|
||||||
|
resolvePluginSdkAlias({
|
||||||
|
root: fixture.root,
|
||||||
|
srcFile: "channel-runtime.ts",
|
||||||
|
distFile: "channel-runtime.js",
|
||||||
|
modulePath: "/tmp/tsx-cache/openclaw-loader.js",
|
||||||
|
env: { NODE_ENV: undefined },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(resolved).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("configures the plugin loader jiti boundary to prefer native dist modules", () => {
|
it("configures the plugin loader jiti boundary to prefer native dist modules", () => {
|
||||||
@ -3361,22 +3513,152 @@ module.exports = {
|
|||||||
"src",
|
"src",
|
||||||
"channel.runtime.ts",
|
"channel.runtime.ts",
|
||||||
);
|
);
|
||||||
const discordVoiceRuntime = path.join(
|
|
||||||
process.cwd(),
|
|
||||||
"extensions",
|
|
||||||
"discord",
|
|
||||||
"src",
|
|
||||||
"voice",
|
|
||||||
"manager.runtime.ts",
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(jiti.import(discordChannelRuntime)).resolves.toMatchObject({
|
await expect(jiti.import(discordChannelRuntime)).resolves.toMatchObject({
|
||||||
discordSetupWizard: expect.any(Object),
|
discordSetupWizard: expect.any(Object),
|
||||||
});
|
});
|
||||||
await expect(jiti.import(discordVoiceRuntime)).resolves.toMatchObject({
|
}, 240_000);
|
||||||
DiscordVoiceManager: expect.any(Function),
|
|
||||||
DiscordVoiceReadyListener: expect.any(Function),
|
it("loads copied imessage runtime sources from git-style paths with plugin-sdk aliases (#49806)", async () => {
|
||||||
|
const copiedExtensionRoot = path.join(makeTempDir(), "extensions", "imessage");
|
||||||
|
const copiedSourceDir = path.join(copiedExtensionRoot, "src");
|
||||||
|
const copiedPluginSdkDir = path.join(copiedExtensionRoot, "plugin-sdk");
|
||||||
|
mkdirSafe(copiedSourceDir);
|
||||||
|
mkdirSafe(copiedPluginSdkDir);
|
||||||
|
const jitiBaseFile = path.join(copiedSourceDir, "__jiti-base__.mjs");
|
||||||
|
fs.writeFileSync(jitiBaseFile, "export {};\n", "utf-8");
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(copiedSourceDir, "channel.runtime.ts"),
|
||||||
|
`import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
|
||||||
|
import { PAIRING_APPROVED_MESSAGE } from "../runtime-api.js";
|
||||||
|
|
||||||
|
export const copiedRuntimeMarker = {
|
||||||
|
resolveOutboundSendDep,
|
||||||
|
PAIRING_APPROVED_MESSAGE,
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(copiedExtensionRoot, "runtime-api.ts"),
|
||||||
|
`export const PAIRING_APPROVED_MESSAGE = "paired";
|
||||||
|
`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
const copiedChannelRuntimeShim = path.join(copiedPluginSdkDir, "channel-runtime.ts");
|
||||||
|
fs.writeFileSync(
|
||||||
|
copiedChannelRuntimeShim,
|
||||||
|
`export function resolveOutboundSendDep() {
|
||||||
|
return "shimmed";
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
const copiedChannelRuntime = path.join(copiedExtensionRoot, "src", "channel.runtime.ts");
|
||||||
|
const jitiBaseUrl = pathToFileURL(jitiBaseFile).href;
|
||||||
|
|
||||||
|
const withoutAlias = createJiti(jitiBaseUrl, {
|
||||||
|
...__testing.buildPluginLoaderJitiOptions({}),
|
||||||
|
tryNative: false,
|
||||||
});
|
});
|
||||||
|
await expect(withoutAlias.import(copiedChannelRuntime)).rejects.toThrow(
|
||||||
|
/plugin-sdk\/channel-runtime/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withAlias = createJiti(jitiBaseUrl, {
|
||||||
|
...__testing.buildPluginLoaderJitiOptions({
|
||||||
|
"openclaw/plugin-sdk/channel-runtime": copiedChannelRuntimeShim,
|
||||||
|
}),
|
||||||
|
tryNative: false,
|
||||||
|
});
|
||||||
|
await expect(withAlias.import(copiedChannelRuntime)).resolves.toMatchObject({
|
||||||
|
copiedRuntimeMarker: {
|
||||||
|
PAIRING_APPROVED_MESSAGE: "paired",
|
||||||
|
resolveOutboundSendDep: expect.any(Function),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads git-style package extension entries through the plugin loader when they import plugin-sdk channel-runtime (#49806)", () => {
|
||||||
|
useNoBundledPlugins();
|
||||||
|
const pluginId = "imessage-loader-regression";
|
||||||
|
const gitExtensionRoot = path.join(
|
||||||
|
makeTempDir(),
|
||||||
|
"git-source-checkout",
|
||||||
|
"extensions",
|
||||||
|
pluginId,
|
||||||
|
);
|
||||||
|
const gitSourceDir = path.join(gitExtensionRoot, "src");
|
||||||
|
mkdirSafe(gitSourceDir);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(gitExtensionRoot, "package.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: `@openclaw/${pluginId}`,
|
||||||
|
version: "0.0.1",
|
||||||
|
type: "module",
|
||||||
|
openclaw: {
|
||||||
|
extensions: ["./src/index.ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(gitExtensionRoot, "openclaw.plugin.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
id: pluginId,
|
||||||
|
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(gitSourceDir, "channel.runtime.ts"),
|
||||||
|
`import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-runtime";
|
||||||
|
|
||||||
|
export function runtimeProbeType() {
|
||||||
|
return typeof resolveOutboundSendDep;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(gitSourceDir, "index.ts"),
|
||||||
|
`import { runtimeProbeType } from "./channel.runtime.ts";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: ${JSON.stringify(pluginId)},
|
||||||
|
register() {
|
||||||
|
if (runtimeProbeType() !== "function") {
|
||||||
|
throw new Error("channel-runtime import did not resolve");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const registry = withEnv({ NODE_ENV: "production", VITEST: undefined }, () =>
|
||||||
|
loadOpenClawPlugins({
|
||||||
|
cache: false,
|
||||||
|
workspaceDir: gitExtensionRoot,
|
||||||
|
config: {
|
||||||
|
plugins: {
|
||||||
|
load: { paths: [gitExtensionRoot] },
|
||||||
|
allow: [pluginId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const record = registry.plugins.find((entry) => entry.id === pluginId);
|
||||||
|
expect(record?.status).toBe("loaded");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads source TypeScript plugins that route through local runtime shims", () => {
|
it("loads source TypeScript plugins that route through local runtime shims", () => {
|
||||||
|
|||||||
@ -12,10 +12,83 @@ export type LoaderModuleResolveParams = {
|
|||||||
moduleUrl?: string;
|
moduleUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PluginSdkPackageJson = {
|
||||||
|
exports?: Record<string, unknown>;
|
||||||
|
bin?: string | Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
function resolveLoaderModulePath(params: LoaderModuleResolveParams = {}): string {
|
function resolveLoaderModulePath(params: LoaderModuleResolveParams = {}): string {
|
||||||
return params.modulePath ?? fileURLToPath(params.moduleUrl ?? import.meta.url);
|
return params.modulePath ?? fileURLToPath(params.moduleUrl ?? import.meta.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readPluginSdkPackageJson(packageRoot: string): PluginSdkPackageJson | null {
|
||||||
|
try {
|
||||||
|
const pkgRaw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8");
|
||||||
|
return JSON.parse(pkgRaw) as PluginSdkPackageJson;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listPluginSdkSubpathsFromPackageJson(pkg: PluginSdkPackageJson): string[] {
|
||||||
|
return Object.keys(pkg.exports ?? {})
|
||||||
|
.filter((key) => key.startsWith("./plugin-sdk/"))
|
||||||
|
.map((key) => key.slice("./plugin-sdk/".length))
|
||||||
|
.filter((subpath) => Boolean(subpath) && !subpath.includes("/"))
|
||||||
|
.toSorted();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTrustedOpenClawRootIndicator(params: {
|
||||||
|
packageRoot: string;
|
||||||
|
packageJson: PluginSdkPackageJson;
|
||||||
|
}): boolean {
|
||||||
|
const packageExports = params.packageJson.exports ?? {};
|
||||||
|
const hasPluginSdkRootExport = Object.prototype.hasOwnProperty.call(
|
||||||
|
packageExports,
|
||||||
|
"./plugin-sdk",
|
||||||
|
);
|
||||||
|
if (!hasPluginSdkRootExport) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasCliEntryExport = Object.prototype.hasOwnProperty.call(packageExports, "./cli-entry");
|
||||||
|
const hasOpenClawBin =
|
||||||
|
(typeof params.packageJson.bin === "string" &&
|
||||||
|
params.packageJson.bin.toLowerCase().includes("openclaw")) ||
|
||||||
|
(typeof params.packageJson.bin === "object" &&
|
||||||
|
params.packageJson.bin !== null &&
|
||||||
|
typeof params.packageJson.bin.openclaw === "string");
|
||||||
|
const hasOpenClawEntrypoint = fs.existsSync(path.join(params.packageRoot, "openclaw.mjs"));
|
||||||
|
return hasCliEntryExport || hasOpenClawBin || hasOpenClawEntrypoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPluginSdkSubpathsFromPackageRoot(packageRoot: string): string[] | null {
|
||||||
|
const pkg = readPluginSdkPackageJson(packageRoot);
|
||||||
|
if (!pkg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!hasTrustedOpenClawRootIndicator({ packageRoot, packageJson: pkg })) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const subpaths = listPluginSdkSubpathsFromPackageJson(pkg);
|
||||||
|
return subpaths.length > 0 ? subpaths : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNearestPluginSdkPackageRoot(startDir: string, maxDepth = 12): string | null {
|
||||||
|
let cursor = path.resolve(startDir);
|
||||||
|
for (let i = 0; i < maxDepth; i += 1) {
|
||||||
|
const subpaths = readPluginSdkSubpathsFromPackageRoot(cursor);
|
||||||
|
if (subpaths) {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
const parent = path.dirname(cursor);
|
||||||
|
if (parent === cursor) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor = parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveLoaderPackageRoot(
|
export function resolveLoaderPackageRoot(
|
||||||
params: LoaderModuleResolveParams & { modulePath: string },
|
params: LoaderModuleResolveParams & { modulePath: string },
|
||||||
): string | null {
|
): string | null {
|
||||||
@ -33,6 +106,28 @@ export function resolveLoaderPackageRoot(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveLoaderPluginSdkPackageRoot(
|
||||||
|
params: LoaderModuleResolveParams & { modulePath: string },
|
||||||
|
): string | null {
|
||||||
|
const cwd = params.cwd ?? path.dirname(params.modulePath);
|
||||||
|
const fromCwd = resolveOpenClawPackageRootSync({ cwd });
|
||||||
|
const fromExplicitHints =
|
||||||
|
params.argv1 || params.moduleUrl
|
||||||
|
? resolveOpenClawPackageRootSync({
|
||||||
|
cwd,
|
||||||
|
...(params.argv1 ? { argv1: params.argv1 } : {}),
|
||||||
|
...(params.moduleUrl ? { moduleUrl: params.moduleUrl } : {}),
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
fromCwd ??
|
||||||
|
fromExplicitHints ??
|
||||||
|
findNearestPluginSdkPackageRoot(path.dirname(params.modulePath)) ??
|
||||||
|
(params.cwd ? findNearestPluginSdkPackageRoot(params.cwd) : null) ??
|
||||||
|
findNearestPluginSdkPackageRoot(process.cwd())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function resolvePluginSdkAliasCandidateOrder(params: {
|
export function resolvePluginSdkAliasCandidateOrder(params: {
|
||||||
modulePath: string;
|
modulePath: string;
|
||||||
isProduction: boolean;
|
isProduction: boolean;
|
||||||
@ -54,7 +149,7 @@ export function listPluginSdkAliasCandidates(params: {
|
|||||||
modulePath: params.modulePath,
|
modulePath: params.modulePath,
|
||||||
isProduction: process.env.NODE_ENV === "production",
|
isProduction: process.env.NODE_ENV === "production",
|
||||||
});
|
});
|
||||||
const packageRoot = resolveLoaderPackageRoot(params);
|
const packageRoot = resolveLoaderPluginSdkPackageRoot(params);
|
||||||
if (packageRoot) {
|
if (packageRoot) {
|
||||||
const candidateMap = {
|
const candidateMap = {
|
||||||
src: path.join(packageRoot, "src", "plugin-sdk", params.srcFile),
|
src: path.join(packageRoot, "src", "plugin-sdk", params.srcFile),
|
||||||
@ -113,9 +208,7 @@ const cachedPluginSdkExportedSubpaths = new Map<string, string[]>();
|
|||||||
|
|
||||||
export function listPluginSdkExportedSubpaths(params: { modulePath?: string } = {}): string[] {
|
export function listPluginSdkExportedSubpaths(params: { modulePath?: string } = {}): string[] {
|
||||||
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
|
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
|
||||||
const packageRoot = resolveOpenClawPackageRootSync({
|
const packageRoot = resolveLoaderPluginSdkPackageRoot({ modulePath });
|
||||||
cwd: path.dirname(modulePath),
|
|
||||||
});
|
|
||||||
if (!packageRoot) {
|
if (!packageRoot) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -123,21 +216,9 @@ export function listPluginSdkExportedSubpaths(params: { modulePath?: string } =
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
try {
|
const subpaths = readPluginSdkSubpathsFromPackageRoot(packageRoot) ?? [];
|
||||||
const pkgRaw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8");
|
cachedPluginSdkExportedSubpaths.set(packageRoot, subpaths);
|
||||||
const pkg = JSON.parse(pkgRaw) as {
|
return subpaths;
|
||||||
exports?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
const subpaths = Object.keys(pkg.exports ?? {})
|
|
||||||
.filter((key) => key.startsWith("./plugin-sdk/"))
|
|
||||||
.map((key) => key.slice("./plugin-sdk/".length))
|
|
||||||
.filter((subpath) => Boolean(subpath) && !subpath.includes("/"))
|
|
||||||
.toSorted();
|
|
||||||
cachedPluginSdkExportedSubpaths.set(packageRoot, subpaths);
|
|
||||||
return subpaths;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePluginSdkScopedAliasMap(
|
export function resolvePluginSdkScopedAliasMap(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user