mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(plugins): support linked source checkouts on Windows
This commit is contained in:
@@ -18,6 +18,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Installer: let the local-prefix CLI installer use Alpine's `apk` Node.js, npm, and Git packages on musl Linux instead of downloading glibc Node tarballs that fail `node:sqlite`.
|
||||
- Scripts: use `git grep` to prefilter tracked conflict-marker scans so changed checks avoid reading every repository file on clean runs.
|
||||
- Plugins: allow linked local plugin paths to probe TypeScript source entries without requiring compiled package output, restoring source-checkout plugin development on native Windows.
|
||||
- CLI: route source-checkout build output to stderr before launching OpenClaw commands so stale local builds do not corrupt `--json` stdout.
|
||||
- Installer: install Node.js through `apk` on Alpine Linux instead of falling through to the NodeSource package-manager path.
|
||||
- Agents/perf: cache manifest-backed CLI provider descriptors and fallback provider resolution so model fallback retries avoid repeated bundled provider runtime scans while still invalidating across plugin reloads.
|
||||
- Installer: detect musl Linux shells such as Alpine as Linux instead of rejecting them before npm install.
|
||||
|
||||
+31
-7
@@ -647,6 +647,28 @@ const createRunNodeOutputTee = (deps) => {
|
||||
if (!outputLogPath) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const existing = deps.fs.statSync(outputLogPath);
|
||||
if (existing.isDirectory()) {
|
||||
return {
|
||||
outputLogPath,
|
||||
write() {},
|
||||
async close() {
|
||||
throw new Error(`output log path is a directory: ${outputLogPath}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code && error.code !== "ENOENT") {
|
||||
return {
|
||||
outputLogPath,
|
||||
write() {},
|
||||
async close() {
|
||||
throw error;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
deps.fs.mkdirSync(path.dirname(outputLogPath), { recursive: true });
|
||||
const stream = deps.fs.createWriteStream(outputLogPath, {
|
||||
flags: "a",
|
||||
@@ -936,8 +958,9 @@ const runOpenClaw = async (deps) => {
|
||||
return res.exitCode ?? 1;
|
||||
};
|
||||
|
||||
const pipeSpawnedOutput = (childProcess, deps) => {
|
||||
if (!shouldPipeSpawnedOutput(deps)) {
|
||||
const pipeSpawnedOutput = (childProcess, deps, options = {}) => {
|
||||
const stdoutTarget = options.stdoutTarget ?? "stdout";
|
||||
if (!shouldPipeSpawnedOutput(deps) && stdoutTarget !== "stderr") {
|
||||
return;
|
||||
}
|
||||
const stderrFilter =
|
||||
@@ -945,7 +968,8 @@ const pipeSpawnedOutput = (childProcess, deps) => {
|
||||
? createSyncIoTraceStderrFilter(deps)
|
||||
: null;
|
||||
childProcess.stdout?.on("data", (chunk) => {
|
||||
writeRunnerStream(deps, deps.stdout, chunk);
|
||||
const target = stdoutTarget === "stderr" ? deps.stderr : deps.stdout;
|
||||
writeRunnerStream(deps, target, chunk);
|
||||
deps.outputTee?.write(chunk);
|
||||
});
|
||||
childProcess.stderr?.on("data", (chunk) => {
|
||||
@@ -1389,9 +1413,9 @@ export async function runNodeMain(params = {}) {
|
||||
const assetBuild = deps.spawn(buildCmd, bundledPluginAssetBuildArgs, {
|
||||
cwd: deps.cwd,
|
||||
env: deps.env,
|
||||
stdio: shouldPipeSpawnedOutput(deps) ? ["inherit", "pipe", "pipe"] : "inherit",
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
pipeSpawnedOutput(assetBuild, deps);
|
||||
pipeSpawnedOutput(assetBuild, deps, { stdoutTarget: "stderr" });
|
||||
const assetBuildRes = await waitForSpawnedProcess(assetBuild, deps);
|
||||
const assetBuildInterruptedExitCode = getInterruptedSpawnExitCode(assetBuildRes);
|
||||
if (assetBuildInterruptedExitCode !== null) {
|
||||
@@ -1407,9 +1431,9 @@ export async function runNodeMain(params = {}) {
|
||||
...deps.env,
|
||||
[RUN_NODE_SKIP_DTS_BUILD_ENV]: deps.env[RUN_NODE_SKIP_DTS_BUILD_ENV] ?? "1",
|
||||
},
|
||||
stdio: shouldPipeSpawnedOutput(deps) ? ["inherit", "pipe", "pipe"] : "inherit",
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
pipeSpawnedOutput(build, deps);
|
||||
pipeSpawnedOutput(build, deps, { stdoutTarget: "stderr" });
|
||||
|
||||
const buildRes = await waitForSpawnedProcess(build, deps);
|
||||
const interruptedExitCode = getInterruptedSpawnExitCode(buildRes);
|
||||
|
||||
@@ -302,6 +302,7 @@ type MockWithCalls = {
|
||||
};
|
||||
|
||||
type PluginInstallCall = {
|
||||
allowSourceTypeScriptEntries?: boolean;
|
||||
archivePath?: string;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
dryRun?: boolean;
|
||||
@@ -1321,6 +1322,7 @@ describe("plugins cli install", () => {
|
||||
|
||||
expect(pathInstallCall().path).toBe(tmpRoot);
|
||||
expect(pathInstallCall().dryRun).toBe(true);
|
||||
expect(pathInstallCall().allowSourceTypeScriptEntries).toBe(true);
|
||||
expect(pathInstallCall().dangerouslyForceUnsafeInstall).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1536,6 +1538,7 @@ describe("plugins cli install", () => {
|
||||
|
||||
expect(pathInstallCall().path).toBe(localPluginDir);
|
||||
expect(pathInstallCall().dryRun).toBe(true);
|
||||
expect(pathInstallCall().allowSourceTypeScriptEntries).toBe(true);
|
||||
expect(pathInstallCall().dangerouslyForceUnsafeInstall).toBe(true);
|
||||
expect(typeof pathInstallCall().logger?.info).toBe("function");
|
||||
expect(typeof pathInstallCall().logger?.warn).toBe("function");
|
||||
|
||||
@@ -693,6 +693,7 @@ export async function runPluginInstallCommand(params: {
|
||||
mode: installMode,
|
||||
path: resolved,
|
||||
dryRun: true,
|
||||
allowSourceTypeScriptEntries: true,
|
||||
extensionsDir,
|
||||
logger: createPluginInstallLogger(runtime),
|
||||
});
|
||||
|
||||
@@ -559,6 +559,68 @@ describe("run-node script", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("routes local build stdout to stderr before JSON command output", async () => {
|
||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||
await writeRuntimePostBuildScaffold(tmp);
|
||||
const outputPath = path.join(tmp, ".artifacts", "run-node", "output.log");
|
||||
const spawn = (_cmd: string, args: string[]) => {
|
||||
if (args[0] === "scripts/bundled-plugin-assets.mjs") {
|
||||
return createPipedExitedProcess({
|
||||
stdout: "asset stdout\n",
|
||||
stderr: "asset stderr\n",
|
||||
});
|
||||
}
|
||||
if (args[0] === "scripts/tsdown-build.mjs") {
|
||||
return createPipedExitedProcess({
|
||||
stdout: "build stdout\n",
|
||||
stderr: "build stderr\n",
|
||||
});
|
||||
}
|
||||
return createPipedExitedProcess({ stdout: '{"plugins":[]}\n' });
|
||||
};
|
||||
const stdoutChunks: string[] = [];
|
||||
const stderrChunks: string[] = [];
|
||||
const stdout = {
|
||||
write: (chunk: string | Buffer) => {
|
||||
stdoutChunks.push(String(chunk));
|
||||
return true;
|
||||
},
|
||||
} as unknown as NodeJS.WriteStream;
|
||||
const stderr = {
|
||||
write: (chunk: string | Buffer) => {
|
||||
stderrChunks.push(String(chunk));
|
||||
return true;
|
||||
},
|
||||
} as unknown as NodeJS.WriteStream;
|
||||
|
||||
const exitCode = await runNodeMain({
|
||||
cwd: tmp,
|
||||
args: ["plugins", "list", "--json"],
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_FORCE_BUILD: "1",
|
||||
OPENCLAW_RUNNER_LOG: "0",
|
||||
OPENCLAW_RUN_NODE_OUTPUT_LOG: outputPath,
|
||||
},
|
||||
spawn,
|
||||
stdout,
|
||||
stderr,
|
||||
execPath: process.execPath,
|
||||
platform: process.platform,
|
||||
} as Parameters<typeof runNodeMain>[0] & {
|
||||
stdout: NodeJS.WriteStream;
|
||||
stderr: NodeJS.WriteStream;
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdoutChunks.join("")).toBe('{"plugins":[]}\n');
|
||||
expect(stderrChunks.join("")).toContain("asset stdout\n");
|
||||
expect(stderrChunks.join("")).toContain("asset stderr\n");
|
||||
expect(stderrChunks.join("")).toContain("build stdout\n");
|
||||
expect(stderrChunks.join("")).toContain("build stderr\n");
|
||||
});
|
||||
});
|
||||
|
||||
it("routes sync I/O trace stderr blocks to the output log without flooding stderr", async () => {
|
||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||
await setupTrackedProject(tmp);
|
||||
|
||||
@@ -879,6 +879,45 @@ describe("discoverOpenClawPlugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("allows linked local install records to point at TypeScript source entries", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "extensions", "linked-source-pack");
|
||||
mkdirSafe(path.join(pluginDir, "src"));
|
||||
|
||||
writePluginPackageManifest({
|
||||
packageDir: pluginDir,
|
||||
packageName: "@openclaw/linked-source-pack",
|
||||
extensions: ["./src/index.ts"],
|
||||
setupEntry: "./src/setup-entry.ts",
|
||||
});
|
||||
writePluginManifest({ pluginDir, id: "linked-source-pack" });
|
||||
writePluginEntry(path.join(pluginDir, "src", "index.ts"));
|
||||
writePluginEntry(path.join(pluginDir, "src", "setup-entry.ts"));
|
||||
|
||||
const installRecords = {
|
||||
"linked-source-pack": {
|
||||
source: "path",
|
||||
installPath: pluginDir,
|
||||
sourcePath: pluginDir,
|
||||
},
|
||||
} satisfies Record<string, PluginInstallRecord>;
|
||||
const result = await discoverWithStateDir(stateDir, { installRecords });
|
||||
|
||||
expectCandidateSource(
|
||||
result.candidates,
|
||||
"linked-source-pack",
|
||||
fs.realpathSync(path.join(pluginDir, "src", "index.ts")),
|
||||
);
|
||||
expectCandidateFields(requireCandidateById(result.candidates, "linked-source-pack"), {
|
||||
setupSource: fs.realpathSync(path.join(pluginDir, "src", "setup-entry.ts")),
|
||||
});
|
||||
expectNoDiagnostic({
|
||||
diagnostics: result.diagnostics,
|
||||
pluginId: "linked-source-pack",
|
||||
messageIncludes: "requires compiled runtime output",
|
||||
});
|
||||
});
|
||||
|
||||
it("still requires compiled runtime output for tracked installed package plugins", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "extensions", "source-only-pack");
|
||||
|
||||
@@ -389,11 +389,40 @@ function mergeDiscoveryResult(
|
||||
}
|
||||
}
|
||||
|
||||
type InstalledPluginRecordPath = {
|
||||
path: string;
|
||||
requireBuiltRuntimeEntry: boolean;
|
||||
};
|
||||
|
||||
function isLinkedLocalPluginRecord(params: {
|
||||
record: PluginInstallRecord;
|
||||
env: NodeJS.ProcessEnv;
|
||||
realpathCache: Map<string, string>;
|
||||
}): boolean {
|
||||
if (params.record.source !== "path") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof params.record.sourcePath !== "string" ||
|
||||
!params.record.sourcePath.trim() ||
|
||||
typeof params.record.installPath !== "string" ||
|
||||
!params.record.installPath.trim()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return resolvesToSameDirectory(
|
||||
resolveUserPath(params.record.sourcePath, params.env),
|
||||
resolveUserPath(params.record.installPath, params.env),
|
||||
params.realpathCache,
|
||||
);
|
||||
}
|
||||
|
||||
function collectInstalledPluginRecordPaths(
|
||||
installRecords: Record<string, PluginInstallRecord> | undefined,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): string[] {
|
||||
const paths: string[] = [];
|
||||
realpathCache: Map<string, string>,
|
||||
): InstalledPluginRecordPath[] {
|
||||
const paths: InstalledPluginRecordPath[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const record of Object.values(installRecords ?? {})) {
|
||||
const rawPath =
|
||||
@@ -410,7 +439,10 @@ function collectInstalledPluginRecordPaths(
|
||||
continue;
|
||||
}
|
||||
seen.add(resolved);
|
||||
paths.push(resolved);
|
||||
paths.push({
|
||||
path: resolved,
|
||||
requireBuiltRuntimeEntry: !isLinkedLocalPluginRecord({ record, env, realpathCache }),
|
||||
});
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
@@ -1380,19 +1412,26 @@ export function discoverOpenClawPlugins(params: {
|
||||
skipDirectories: readChildDirectoryNames(roots.stock),
|
||||
});
|
||||
}
|
||||
const installedPaths = collectInstalledPluginRecordPaths(params.installRecords, env);
|
||||
const installedPluginDirKeys = collectManagedPluginDirKeys(installedPaths, realpathCache);
|
||||
const installedPaths = collectInstalledPluginRecordPaths(
|
||||
params.installRecords,
|
||||
env,
|
||||
realpathCache,
|
||||
);
|
||||
const installedPluginDirKeys = collectManagedPluginDirKeys(
|
||||
installedPaths.map((installedPath) => installedPath.path),
|
||||
realpathCache,
|
||||
);
|
||||
const managedPluginDirs = collectManagedPluginDirKeys(
|
||||
collectManagedPluginRecordPaths(params.installRecords, env),
|
||||
realpathCache,
|
||||
);
|
||||
for (const installedPath of installedPaths) {
|
||||
discoverFromPath({
|
||||
rawPath: installedPath,
|
||||
rawPath: installedPath.path,
|
||||
origin: "global",
|
||||
ownershipUid: params.ownershipUid,
|
||||
workspaceDir,
|
||||
requireBuiltRuntimeEntry: true,
|
||||
requireBuiltRuntimeEntry: installedPath.requireBuiltRuntimeEntry,
|
||||
managedPluginDirs,
|
||||
env,
|
||||
candidates: result.candidates,
|
||||
|
||||
+45
-12
@@ -293,6 +293,10 @@ function expectWarningExcludes(warnings: readonly string[], fragment: string) {
|
||||
expect(warnings.join("\n")).not.toContain(fragment);
|
||||
}
|
||||
|
||||
function expectMessageIncludesPath(message: string, fragment: string) {
|
||||
expect(message.replaceAll("\\", "/")).toContain(fragment);
|
||||
}
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`expected ${label} to be an object`);
|
||||
@@ -1187,6 +1191,33 @@ describe("installPluginFromArchive", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("allows linked source probes when TypeScript extension entries have no compiled runtime output", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
fs.mkdirSync(path.join(pluginDir, "src"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "source-link-runtime-plugin",
|
||||
version: "1.0.0",
|
||||
openclaw: { extensions: ["./src/index.ts"] },
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(path.join(pluginDir, "src", "index.ts"), "export {};\n");
|
||||
|
||||
const result = await installPluginFromDir({
|
||||
dirPath: pluginDir,
|
||||
extensionsDir,
|
||||
dryRun: true,
|
||||
allowSourceTypeScriptEntries: true,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
expect(result.pluginId).toBe("source-link-runtime-plugin");
|
||||
expect(result.targetDir).toBe(resolvePluginInstallDir(result.pluginId, extensionsDir));
|
||||
});
|
||||
|
||||
it("rejects package installs when runtimeExtensions length does not match extensions", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
fs.mkdirSync(path.join(pluginDir, "dist"), { recursive: true });
|
||||
@@ -1450,7 +1481,7 @@ describe("installPluginFromArchive", () => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain("dist/payload.js");
|
||||
expectMessageIncludesPath(result.error, "dist/payload.js");
|
||||
}
|
||||
expectWarningIncludes(warnings, "dangerous code pattern");
|
||||
});
|
||||
@@ -1601,7 +1632,7 @@ describe("installPluginFromArchive", () => {
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('blocked dependencies "plain-crypto-js" in dependencies');
|
||||
expect(result.error).toContain("declared in axios (vendor/axios/package.json)");
|
||||
expectMessageIncludesPath(result.error, "declared in axios (vendor/axios/package.json)");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1628,7 +1659,7 @@ describe("installPluginFromArchive", () => {
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('blocked dependency directory "plain-crypto-js"');
|
||||
expect(result.error).toContain("vendor/node_modules/plain-crypto-js");
|
||||
expectMessageIncludesPath(result.error, "vendor/node_modules/plain-crypto-js");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1655,7 +1686,7 @@ describe("installPluginFromArchive", () => {
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"');
|
||||
expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js.Js");
|
||||
expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js.Js");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1682,7 +1713,7 @@ describe("installPluginFromArchive", () => {
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"');
|
||||
expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js");
|
||||
expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2085,7 +2116,8 @@ describe("installPluginFromArchive", () => {
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('blocked dependencies "plain-crypto-js" as package name');
|
||||
expect(result.error).toContain(
|
||||
expectMessageIncludesPath(
|
||||
result.error,
|
||||
"vendor/pkg-127/node_modules/nested-safe/node_modules/plain-crypto-js/package.json",
|
||||
);
|
||||
}
|
||||
@@ -2410,7 +2442,7 @@ describe("installPluginFromArchive", () => {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('Bundle "blocked-dependency-bundle" installation blocked');
|
||||
expect(result.error).toContain('blocked dependencies "plain-crypto-js" in dependencies');
|
||||
expect(result.error).toContain("declared in axios (vendor/axios/package.json)");
|
||||
expectMessageIncludesPath(result.error, "declared in axios (vendor/axios/package.json)");
|
||||
}
|
||||
expect(
|
||||
warnings.some((warning) =>
|
||||
@@ -2442,7 +2474,8 @@ describe("installPluginFromArchive", () => {
|
||||
'Bundle "blocked-vendored-package-name-bundle" installation blocked',
|
||||
);
|
||||
expect(result.error).toContain('"plain-crypto-js" as package name');
|
||||
expect(result.error).toContain(
|
||||
expectMessageIncludesPath(
|
||||
result.error,
|
||||
"declared in plain-crypto-js (vendor/plain-crypto-js/package.json)",
|
||||
);
|
||||
}
|
||||
@@ -2464,7 +2497,7 @@ describe("installPluginFromArchive", () => {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('Bundle "blocked-package-dir-bundle" installation blocked');
|
||||
expect(result.error).toContain('blocked dependency directory "plain-crypto-js"');
|
||||
expect(result.error).toContain("vendor/node_modules/plain-crypto-js");
|
||||
expectMessageIncludesPath(result.error, "vendor/node_modules/plain-crypto-js");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2484,7 +2517,7 @@ describe("installPluginFromArchive", () => {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('Bundle "blocked-package-file-bundle" installation blocked');
|
||||
expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"');
|
||||
expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js.Js");
|
||||
expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js.Js");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2506,7 +2539,7 @@ describe("installPluginFromArchive", () => {
|
||||
'Bundle "blocked-package-extensionless-file-bundle" installation blocked',
|
||||
);
|
||||
expect(result.error).toContain('blocked dependency file alias "Plain-Crypto-Js"');
|
||||
expect(result.error).toContain("vendor/Node_Modules/Plain-Crypto-Js");
|
||||
expectMessageIncludesPath(result.error, "vendor/Node_Modules/Plain-Crypto-Js");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3111,7 +3144,7 @@ describe("installPluginFromDir", () => {
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(result.error).toContain('blocked dependencies "plain-crypto-js" as package name');
|
||||
expect(result.error).toContain("node_modules/plain-crypto-js/package.json");
|
||||
expectMessageIncludesPath(result.error, "node_modules/plain-crypto-js/package.json");
|
||||
}
|
||||
expect(vi.mocked(runCommandWithTimeout)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -945,6 +945,7 @@ type PackageInstallCommonParams = InstallSafetyOverrides & {
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
requirePluginManifest?: boolean;
|
||||
allowSourceTypeScriptEntries?: boolean;
|
||||
installPolicyRequest?: PluginInstallPolicyRequest;
|
||||
};
|
||||
|
||||
@@ -973,6 +974,7 @@ function pickPackageInstallCommonParams(
|
||||
dryRun: params.dryRun,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
requirePluginManifest: params.requirePluginManifest,
|
||||
allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries,
|
||||
installPolicyRequest: params.installPolicyRequest,
|
||||
};
|
||||
}
|
||||
@@ -1313,6 +1315,7 @@ async function validatePackagePluginInstallSource(params: {
|
||||
packageDir: string;
|
||||
expectedPluginId?: string;
|
||||
requirePluginManifest?: boolean;
|
||||
allowSourceTypeScriptEntries?: boolean;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
installPolicyRequest?: PluginInstallPolicyRequest;
|
||||
@@ -1422,6 +1425,7 @@ async function validatePackagePluginInstallSource(params: {
|
||||
packageDir: params.packageDir,
|
||||
extensions,
|
||||
manifest,
|
||||
allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries,
|
||||
});
|
||||
if (!extensionValidation.ok) {
|
||||
return {
|
||||
@@ -1531,6 +1535,7 @@ export async function installPluginFromInstalledPackageDir(
|
||||
packageDir: params.packageDir,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
requirePluginManifest: params.requirePluginManifest,
|
||||
allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
installPolicyRequest: params.installPolicyRequest,
|
||||
@@ -1598,6 +1603,7 @@ async function installPluginFromPackageDir(
|
||||
packageDir: params.packageDir,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
requirePluginManifest: params.requirePluginManifest,
|
||||
allowSourceTypeScriptEntries: params.allowSourceTypeScriptEntries,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
installPolicyRequest: params.installPolicyRequest,
|
||||
|
||||
@@ -142,6 +142,7 @@ export async function validatePackageExtensionEntriesForInstall(params: {
|
||||
packageDir: string;
|
||||
extensions: string[];
|
||||
manifest: PackageManifest;
|
||||
allowSourceTypeScriptEntries?: boolean;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
const runtimeResolution = resolvePackageRuntimeExtensionEntries({
|
||||
manifest: params.manifest,
|
||||
@@ -198,6 +199,14 @@ export async function validatePackageExtensionEntriesForInstall(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
sourceEntry.exists &&
|
||||
isTypeScriptPackageEntry(entry) &&
|
||||
params.allowSourceTypeScriptEntries
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sourceEntry.exists && isTypeScriptPackageEntry(entry)) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -282,6 +291,14 @@ export async function validatePackageExtensionEntriesForInstall(params: {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (
|
||||
sourceEntry.exists &&
|
||||
isTypeScriptPackageEntry(setupEntry) &&
|
||||
params.allowSourceTypeScriptEntries
|
||||
) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (sourceEntry.exists && isTypeScriptPackageEntry(setupEntry)) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user