mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Move safe-chain package to packages/safe-chain
This commit is contained in:
parent
fc9a9ca129
commit
7673d32912
68 changed files with 85 additions and 52 deletions
|
|
@ -0,0 +1,13 @@
|
|||
export function matchesCommand(args, ...commandArgs) {
|
||||
if (args.length < commandArgs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < commandArgs.length; i++) {
|
||||
if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { createNpmPackageManager } from "./npm/createPackageManager.js";
|
||||
import { createNpxPackageManager } from "./npx/createPackageManager.js";
|
||||
import {
|
||||
createPnpmPackageManager,
|
||||
createPnpxPackageManager,
|
||||
} from "./pnpm/createPackageManager.js";
|
||||
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
||||
|
||||
const state = {
|
||||
packageManagerName: null,
|
||||
};
|
||||
|
||||
export function initializePackageManager(packageManagerName, version) {
|
||||
if (packageManagerName === "npm") {
|
||||
state.packageManagerName = createNpmPackageManager(version);
|
||||
} else if (packageManagerName === "npx") {
|
||||
state.packageManagerName = createNpxPackageManager();
|
||||
} else if (packageManagerName === "yarn") {
|
||||
state.packageManagerName = createYarnPackageManager();
|
||||
} else if (packageManagerName === "pnpm") {
|
||||
state.packageManagerName = createPnpmPackageManager();
|
||||
} else if (packageManagerName === "pnpx") {
|
||||
state.packageManagerName = createPnpxPackageManager();
|
||||
} else {
|
||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||
}
|
||||
|
||||
return state.packageManagerName;
|
||||
}
|
||||
|
||||
export function getPackageManager() {
|
||||
if (!state.packageManagerName) {
|
||||
throw new Error("Package manager not initialized.");
|
||||
}
|
||||
return state.packageManagerName;
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||
import { dryRunScanner } from "./dependencyScanner/dryRunScanner.js";
|
||||
import { nullScanner } from "./dependencyScanner/nullScanner.js";
|
||||
import { runNpm } from "./runNpmCommand.js";
|
||||
import {
|
||||
getNpmCommandForArgs,
|
||||
npmInstallCommand,
|
||||
npmCiCommand,
|
||||
npmInstallTestCommand,
|
||||
npmInstallCiTestCommand,
|
||||
npmUpdateCommand,
|
||||
npmAuditCommand,
|
||||
npmExecCommand,
|
||||
} from "./utils/npmCommands.js";
|
||||
|
||||
export function createNpmPackageManager(version) {
|
||||
const supportedScanners =
|
||||
getMajorVersion(version) >= 22
|
||||
? npm22AndAboveSupportedScanners
|
||||
: npm21AndBelowSupportedScanners;
|
||||
|
||||
function isSupportedCommand(args) {
|
||||
const scanner = findDependencyScannerForCommand(supportedScanners, args);
|
||||
return scanner.shouldScan(args);
|
||||
}
|
||||
|
||||
function getDependencyUpdatesForCommand(args) {
|
||||
const scanner = findDependencyScannerForCommand(supportedScanners, args);
|
||||
return scanner.scan(args);
|
||||
}
|
||||
|
||||
return {
|
||||
getWarningMessage: () => warnForLimitedSupport(version),
|
||||
runCommand: runNpm,
|
||||
isSupportedCommand,
|
||||
getDependencyUpdatesForCommand,
|
||||
};
|
||||
}
|
||||
|
||||
const npm22AndAboveSupportedScanners = {
|
||||
[npmInstallCommand]: dryRunScanner(),
|
||||
[npmUpdateCommand]: dryRunScanner(),
|
||||
[npmCiCommand]: dryRunScanner(),
|
||||
[npmAuditCommand]: dryRunScanner({
|
||||
skipScanWhen: (args) => !args.includes("fix"),
|
||||
}),
|
||||
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
||||
|
||||
// Running dry-run on install-test and install-ci-test will install & run tests.
|
||||
// We only want to know if there are changes in the dependencies.
|
||||
// So we run change the dry-run command to only check the install.
|
||||
[npmInstallTestCommand]: dryRunScanner({ dryRunCommand: npmInstallCommand }),
|
||||
[npmInstallCiTestCommand]: dryRunScanner({ dryRunCommand: npmCiCommand }),
|
||||
};
|
||||
|
||||
const npm21AndBelowSupportedScanners = {
|
||||
[npmInstallCommand]: commandArgumentScanner(),
|
||||
[npmUpdateCommand]: commandArgumentScanner(),
|
||||
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
||||
};
|
||||
|
||||
function warnForLimitedSupport(version) {
|
||||
if (getMajorVersion(version) >= 22) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `Aikido-npm will only scan the arguments of the install command for Node.js version prior to version 22.
|
||||
Please update your Node.js version to 22 or higher for full coverage. Current version: v${version}`;
|
||||
}
|
||||
|
||||
function getMajorVersion(version) {
|
||||
return parseInt(version.split(".")[0]);
|
||||
}
|
||||
|
||||
function findDependencyScannerForCommand(scanners, args) {
|
||||
const command = getNpmCommandForArgs(args);
|
||||
if (!command) {
|
||||
return nullScanner();
|
||||
}
|
||||
|
||||
const scanner = scanners[command];
|
||||
return scanner ? scanner : nullScanner();
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
|
||||
import { hasDryRunArg } from "../utils/npmCommands.js";
|
||||
|
||||
export function commandArgumentScanner(opts) {
|
||||
const ignoreDryRun = opts?.ignoreDryRun ?? false;
|
||||
|
||||
return {
|
||||
scan: (args) => scanDependencies(args),
|
||||
shouldScan: (args) => shouldScanDependencies(args, ignoreDryRun),
|
||||
};
|
||||
}
|
||||
function scanDependencies(args) {
|
||||
return checkChangesFromArgs(args);
|
||||
}
|
||||
|
||||
function shouldScanDependencies(args, ignoreDryRun) {
|
||||
return ignoreDryRun || !hasDryRunArg(args);
|
||||
}
|
||||
|
||||
export async function checkChangesFromArgs(args) {
|
||||
const changes = [];
|
||||
const packageUpdates = parsePackagesFromInstallArgs(args);
|
||||
|
||||
for (const packageUpdate of packageUpdates) {
|
||||
var exactVersion = await resolvePackageVersion(
|
||||
packageUpdate.name,
|
||||
packageUpdate.version
|
||||
);
|
||||
if (exactVersion) {
|
||||
packageUpdate.version = exactVersion;
|
||||
}
|
||||
|
||||
changes.push({ ...packageUpdate, type: "add" });
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { ui } from "../../../environment/userInteraction.js";
|
||||
import { parseDryRunOutput } from "../parsing/parseNpmInstallDryRunOutput.js";
|
||||
import { dryRunNpmCommandAndOutput } from "../runNpmCommand.js";
|
||||
import { hasDryRunArg } from "../utils/npmCommands.js";
|
||||
|
||||
export function dryRunScanner(scannerOptions) {
|
||||
return {
|
||||
scan: (args) => scanDependencies(scannerOptions, args),
|
||||
shouldScan: (args) => shouldScanDependencies(scannerOptions, args),
|
||||
};
|
||||
}
|
||||
function scanDependencies(scannerOptions, args) {
|
||||
let dryRunArgs = args;
|
||||
|
||||
if (scannerOptions?.dryRunCommand) {
|
||||
// Replace the first argument with the dryRunCommand (eg: "install" instead of "install-test")
|
||||
dryRunArgs = [scannerOptions.dryRunCommand, ...args.slice(1)];
|
||||
}
|
||||
|
||||
return checkChangesWithDryRun(dryRunArgs);
|
||||
}
|
||||
|
||||
function shouldScanDependencies(scannerOptions, args) {
|
||||
if (hasDryRunArg(args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scannerOptions?.skipScanWhen && scannerOptions.skipScanWhen(args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkChangesWithDryRun(args) {
|
||||
const dryRunOutput = dryRunNpmCommandAndOutput(args);
|
||||
|
||||
// Dry-run can return a non-zero status code in some cases
|
||||
// e.g., when running "npm audit fix --dry-run", it returns exit code 1
|
||||
// when there are vulnurabilities that can be fixed.
|
||||
if (dryRunOutput.status !== 0 && !dryRunOutput.output) {
|
||||
ui.writeError("Detecting changes failed.");
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsedOutput = parseDryRunOutput(dryRunOutput.output);
|
||||
|
||||
// reverse the array to have the top-level packages first
|
||||
return parsedOutput.reverse();
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export function nullScanner() {
|
||||
return {
|
||||
scan: () => [],
|
||||
shouldScan: () => false,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
export function parseDryRunOutput(output) {
|
||||
const lines = output.split(/\r?\n/);
|
||||
const packageChanges = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("add ")) {
|
||||
packageChanges.push(parseAdd(line));
|
||||
} else if (line.startsWith("remove ")) {
|
||||
packageChanges.push(parseRemove(line));
|
||||
} else if (line.startsWith("change ")) {
|
||||
packageChanges.push(parseChange(line));
|
||||
}
|
||||
}
|
||||
|
||||
return packageChanges;
|
||||
}
|
||||
|
||||
function parseAdd(line) {
|
||||
const splitLine = getLineParts(line);
|
||||
const packageName = splitLine[1];
|
||||
const packageVersion = splitLine[splitLine.length - 1];
|
||||
return addedPackage(packageName, packageVersion);
|
||||
}
|
||||
|
||||
function addedPackage(name, version) {
|
||||
return { type: "add", name, version };
|
||||
}
|
||||
|
||||
function parseRemove(line) {
|
||||
const splitLine = getLineParts(line);
|
||||
const packageName = splitLine[1];
|
||||
const packageVersion = splitLine[splitLine.length - 1];
|
||||
return removedPackage(packageName, packageVersion);
|
||||
}
|
||||
|
||||
function removedPackage(name, version) {
|
||||
return { type: "remove", name, version };
|
||||
}
|
||||
|
||||
function parseChange(line) {
|
||||
const splitLine = getLineParts(line);
|
||||
const packageName = splitLine[1];
|
||||
const packageVersion = splitLine[splitLine.length - 1];
|
||||
const oldVersion = splitLine[2];
|
||||
return changedPackage(packageName, packageVersion, oldVersion);
|
||||
}
|
||||
|
||||
function getLineParts(line) {
|
||||
return line
|
||||
.split(" ")
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part !== "");
|
||||
}
|
||||
|
||||
function changedPackage(name, version, oldVersion) {
|
||||
return { type: "change", name, version, oldVersion };
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parseDryRunOutput } from "./parseNpmInstallDryRunOutput.js";
|
||||
|
||||
describe("parseNpmInstallDryRunOutput", () => {
|
||||
it("should parse added packages", () => {
|
||||
const output = `
|
||||
add @jest/transform 29.7.0
|
||||
add @jest/test-result 29.7.0
|
||||
add @jest/reporters 29.7.0
|
||||
add @jest/console 29.7.0
|
||||
add jest-cli 29.7.0
|
||||
add import-local 3.2.0
|
||||
add @jest/types 29.6.3
|
||||
add @jest/core 29.7.0
|
||||
add jest 29.7.0
|
||||
|
||||
added 267 packages in 831ms
|
||||
|
||||
32 packages are looking for funding
|
||||
run \`npm fund\` for details`;
|
||||
|
||||
const expected = [
|
||||
{ name: "@jest/transform", version: "29.7.0", type: "add" },
|
||||
{ name: "@jest/test-result", version: "29.7.0", type: "add" },
|
||||
{ name: "@jest/reporters", version: "29.7.0", type: "add" },
|
||||
{ name: "@jest/console", version: "29.7.0", type: "add" },
|
||||
{ name: "jest-cli", version: "29.7.0", type: "add" },
|
||||
{ name: "import-local", version: "3.2.0", type: "add" },
|
||||
{ name: "@jest/types", version: "29.6.3", type: "add" },
|
||||
{ name: "@jest/core", version: "29.7.0", type: "add" },
|
||||
{ name: "jest", version: "29.7.0", type: "add" },
|
||||
];
|
||||
|
||||
const result = parseDryRunOutput(output);
|
||||
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it("should parse removed packages", () => {
|
||||
const output = `
|
||||
remove react 19.1.0
|
||||
|
||||
removed 1 package in 115ms`;
|
||||
|
||||
const expected = [{ name: "react", version: "19.1.0", type: "remove" }];
|
||||
|
||||
const result = parseDryRunOutput(output);
|
||||
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it("should parse changed packages", () => {
|
||||
const output = `
|
||||
change react 19.0.0 => 19.1.0
|
||||
|
||||
changed 1 package in 204ms`;
|
||||
|
||||
const expected = [
|
||||
{
|
||||
name: "react",
|
||||
version: "19.1.0",
|
||||
oldVersion: "19.0.0",
|
||||
type: "change",
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseDryRunOutput(output);
|
||||
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it("should parse mixed package changes", () => {
|
||||
const output = `
|
||||
add @jest/transform 29.7.0
|
||||
add @jest/test-result 29.7.0
|
||||
add @jest/reporters 29.7.0
|
||||
add @jest/console 29.7.0
|
||||
add jest-cli 29.7.0
|
||||
add import-local 3.2.0
|
||||
add @jest/types 29.6.3
|
||||
add @jest/core 29.7.0
|
||||
add jest 29.7.0
|
||||
remove react 19.1.0
|
||||
change lodash 4.17.0 => 4.18.0
|
||||
|
||||
removed 1 package in 115ms`;
|
||||
|
||||
const expected = [
|
||||
{ name: "@jest/transform", version: "29.7.0", type: "add" },
|
||||
{ name: "@jest/test-result", version: "29.7.0", type: "add" },
|
||||
{ name: "@jest/reporters", version: "29.7.0", type: "add" },
|
||||
{ name: "@jest/console", version: "29.7.0", type: "add" },
|
||||
{ name: "jest-cli", version: "29.7.0", type: "add" },
|
||||
{ name: "import-local", version: "3.2.0", type: "add" },
|
||||
{ name: "@jest/types", version: "29.6.3", type: "add" },
|
||||
{ name: "@jest/core", version: "29.7.0", type: "add" },
|
||||
{ name: "jest", version: "29.7.0", type: "add" },
|
||||
{ name: "react", version: "19.1.0", type: "remove" },
|
||||
{
|
||||
name: "lodash",
|
||||
version: "4.18.0",
|
||||
oldVersion: "4.17.0",
|
||||
type: "change",
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseDryRunOutput(output);
|
||||
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it("should work with npm v22.0.0", () => {
|
||||
const output = `
|
||||
add @jest/types 29.6.3
|
||||
add @jest/core 29.7.0
|
||||
add jest 29.7.0
|
||||
|
||||
added 257 packages in 791ms
|
||||
|
||||
44 packages are looking for funding
|
||||
run \`npm fund\` for details`;
|
||||
|
||||
const expected = [
|
||||
{ name: "@jest/types", version: "29.6.3", type: "add" },
|
||||
{ name: "@jest/core", version: "29.7.0", type: "add" },
|
||||
{ name: "jest", version: "29.7.0", type: "add" },
|
||||
];
|
||||
|
||||
const result = parseDryRunOutput(output);
|
||||
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
export function parsePackagesFromInstallArgs(args) {
|
||||
const changes = [];
|
||||
let defaultTag = "latest";
|
||||
|
||||
// Skip first argument (install command)
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const npmOption = getNpmOption(arg);
|
||||
|
||||
if (npmOption) {
|
||||
// If the option has a parameter, skip the next argument as well
|
||||
i += npmOption.numberOfParameters;
|
||||
|
||||
// it a tag is specified, set the default tag
|
||||
if (npmOption.name === "--tag") {
|
||||
defaultTag = args[i];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDetails = parsePackagename(arg);
|
||||
if (packageDetails) {
|
||||
changes.push(packageDetails);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
if (!change.version) {
|
||||
change.version = defaultTag;
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function getNpmOption(arg) {
|
||||
if (isNpmOptionWithParameter(arg)) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Arguments starting with "-" or "--" are considered npm options
|
||||
if (arg.startsWith("-")) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isNpmOptionWithParameter(arg) {
|
||||
const optionsWithParameters = [
|
||||
"--access",
|
||||
"--auth-type",
|
||||
"--cache",
|
||||
"--fetch-retries",
|
||||
"--fetch-retry-mintimeout",
|
||||
"--fetch-retry-maxtimeout",
|
||||
"--fetch-retry-factor",
|
||||
"--fetch-timeout",
|
||||
"--https-proxy",
|
||||
"--include",
|
||||
"--location",
|
||||
"--lockfile-version",
|
||||
"--loglevel",
|
||||
"--omit",
|
||||
"--proxy",
|
||||
"--registry",
|
||||
"--replace-registry-host",
|
||||
"--tag",
|
||||
"--user-config",
|
||||
"--workspace",
|
||||
];
|
||||
|
||||
return optionsWithParameters.includes(arg);
|
||||
}
|
||||
|
||||
function parsePackagename(arg) {
|
||||
arg = removeAlias(arg);
|
||||
const lastAtIndex = arg.lastIndexOf("@");
|
||||
|
||||
let name, version;
|
||||
// The index of the last "@" should be greater than 0
|
||||
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
||||
if (lastAtIndex > 0) {
|
||||
name = arg.slice(0, lastAtIndex);
|
||||
version = arg.slice(lastAtIndex + 1);
|
||||
} else {
|
||||
name = arg;
|
||||
version = null;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
function removeAlias(arg) {
|
||||
const aliasIndex = arg.indexOf("@npm:");
|
||||
if (aliasIndex !== -1) {
|
||||
return arg.slice(aliasIndex + 5);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parsePackagesFromInstallArgs } from "./parsePackagesFromInstallArgs.js";
|
||||
|
||||
describe("parsePackagesFromInstallArgs", () => {
|
||||
it("should return an empty array for no changes", () => {
|
||||
const args = ["install"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("should return an array of changes for one package", () => {
|
||||
const args = ["install", "@jest/transform@29.7.0"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@jest/transform", version: "29.7.0" }]);
|
||||
});
|
||||
|
||||
it("should return the package in the format @vercel/otel", () => {
|
||||
const args = ["install", "@vercel/otel"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return an array of changes for multiple packages", () => {
|
||||
const args = ["install", "express@4.17.1", "lodash@4.17.21"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "express", version: "4.17.1" },
|
||||
{ name: "lodash", version: "4.17.21" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore options and return an array of changes", () => {
|
||||
const args = [
|
||||
"install",
|
||||
"--save-dev",
|
||||
"express@4.17.1",
|
||||
"--save-exact",
|
||||
"lodash@4.17.21",
|
||||
];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "express", version: "4.17.1" },
|
||||
{ name: "lodash", version: "4.17.21" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore options with parameters and return an array of changes", () => {
|
||||
const args = [
|
||||
"install",
|
||||
"--save-dev",
|
||||
"express@4.17.1",
|
||||
"--loglevel",
|
||||
"error",
|
||||
"lodash@4.17.21",
|
||||
];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "express", version: "4.17.1" },
|
||||
{ name: "lodash", version: "4.17.21" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not ignore the next argument if it is passed directly with the option", () => {
|
||||
const args = [
|
||||
"install",
|
||||
"--save-dev",
|
||||
"express@4.17.1",
|
||||
"--loglevel=error",
|
||||
"lodash@4.17.21",
|
||||
];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "express", version: "4.17.1" },
|
||||
{ name: "lodash", version: "4.17.21" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set the default tag for packages", () => {
|
||||
const args = ["install", "express", "lodash@4.17.21"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "express", version: "latest" },
|
||||
{ name: "lodash", version: "4.17.21" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set the default tag for packages with a specific tag", () => {
|
||||
const args = ["install", "express", "lodash@4.17.21", "--tag", "beta"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "express", version: "beta" },
|
||||
{ name: "lodash", version: "4.17.21" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore alias", () => {
|
||||
const args = ["install", "express@npm:express@4.17.1"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "express", version: "4.17.1" }]);
|
||||
});
|
||||
|
||||
it("should parse version even for aliased packages", () => {
|
||||
const args = ["install", "express@npm:express@4.17.1"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "express", version: "4.17.1" }]);
|
||||
});
|
||||
|
||||
it("should parse scoped packages", () => {
|
||||
const args = ["install", "@scope/package@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]);
|
||||
});
|
||||
|
||||
it("should parse packages with version ranges", () => {
|
||||
const args = ["install", "express@^4.17.1"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "express", version: "^4.17.1" }]);
|
||||
});
|
||||
|
||||
it("should parse package folders", () => {
|
||||
const args = ["install", "./local-package"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should parse tarballs", () => {
|
||||
const args = ["install", "file:./local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "file:./local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse tarball URLs", () => {
|
||||
const args = ["install", "https://example.com/local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "https://example.com/local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse git URLs", () => {
|
||||
const args = ["install", "git://github.com/npm/cli.git"];
|
||||
|
||||
const result = parsePackagesFromInstallArgs(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "git://github.com/npm/cli.git", version: "latest" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
33
packages/safe-chain/src/packagemanager/npm/runNpmCommand.js
Normal file
33
packages/safe-chain/src/packagemanager/npm/runNpmCommand.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { execSync } from "child_process";
|
||||
import { ui } from "../../environment/userInteraction.js";
|
||||
|
||||
export function runNpm(args) {
|
||||
try {
|
||||
const npmCommand = `npm ${args.join(" ")}`;
|
||||
execSync(npmCommand, { stdio: "inherit" });
|
||||
} catch (error) {
|
||||
if (error.status) {
|
||||
return { status: error.status };
|
||||
} else {
|
||||
ui.writeError("Error executing command:", error.message);
|
||||
return { status: 1 };
|
||||
}
|
||||
}
|
||||
return { status: 0 };
|
||||
}
|
||||
|
||||
export function dryRunNpmCommandAndOutput(args) {
|
||||
try {
|
||||
const npmCommand = `npm ${args.join(" ")} --dry-run`;
|
||||
const output = execSync(npmCommand, { stdio: "pipe" });
|
||||
return { status: 0, output: output.toString() };
|
||||
} catch (error) {
|
||||
if (error.status) {
|
||||
const output = error.stdout ? error.stdout.toString() : "";
|
||||
return { status: error.status, output };
|
||||
} else {
|
||||
ui.writeError("Error executing command:", error.message);
|
||||
return { status: 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
171
packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js
Normal file
171
packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js
|
||||
|
||||
import abbrev from "abbrev";
|
||||
|
||||
const commands = [
|
||||
"access",
|
||||
"adduser",
|
||||
"audit",
|
||||
"bugs",
|
||||
"cache",
|
||||
"ci",
|
||||
"completion",
|
||||
"config",
|
||||
"dedupe",
|
||||
"deprecate",
|
||||
"diff",
|
||||
"dist-tag",
|
||||
"docs",
|
||||
"doctor",
|
||||
"edit",
|
||||
"exec",
|
||||
"explain",
|
||||
"explore",
|
||||
"find-dupes",
|
||||
"fund",
|
||||
"get",
|
||||
"help",
|
||||
"help-search",
|
||||
"init",
|
||||
"install",
|
||||
"install-ci-test",
|
||||
"install-test",
|
||||
"link",
|
||||
"ll",
|
||||
"login",
|
||||
"logout",
|
||||
"ls",
|
||||
"org",
|
||||
"outdated",
|
||||
"owner",
|
||||
"pack",
|
||||
"ping",
|
||||
"pkg",
|
||||
"prefix",
|
||||
"profile",
|
||||
"prune",
|
||||
"publish",
|
||||
"query",
|
||||
"rebuild",
|
||||
"repo",
|
||||
"restart",
|
||||
"root",
|
||||
"run",
|
||||
"sbom",
|
||||
"search",
|
||||
"set",
|
||||
"shrinkwrap",
|
||||
"star",
|
||||
"stars",
|
||||
"start",
|
||||
"stop",
|
||||
"team",
|
||||
"test",
|
||||
"token",
|
||||
"undeprecate",
|
||||
"uninstall",
|
||||
"unpublish",
|
||||
"unstar",
|
||||
"update",
|
||||
"version",
|
||||
"view",
|
||||
"whoami",
|
||||
];
|
||||
|
||||
// These must resolve to an entry in commands
|
||||
const aliases = {
|
||||
// aliases
|
||||
author: "owner",
|
||||
home: "docs",
|
||||
issues: "bugs",
|
||||
info: "view",
|
||||
show: "view",
|
||||
find: "search",
|
||||
add: "install",
|
||||
unlink: "uninstall",
|
||||
remove: "uninstall",
|
||||
rm: "uninstall",
|
||||
r: "uninstall",
|
||||
|
||||
// short names for common things
|
||||
un: "uninstall",
|
||||
rb: "rebuild",
|
||||
list: "ls",
|
||||
ln: "link",
|
||||
create: "init",
|
||||
i: "install",
|
||||
it: "install-test",
|
||||
cit: "install-ci-test",
|
||||
up: "update",
|
||||
c: "config",
|
||||
s: "search",
|
||||
se: "search",
|
||||
tst: "test",
|
||||
t: "test",
|
||||
ddp: "dedupe",
|
||||
v: "view",
|
||||
"run-script": "run",
|
||||
"clean-install": "ci",
|
||||
"clean-install-test": "install-ci-test",
|
||||
x: "exec",
|
||||
why: "explain",
|
||||
la: "ll",
|
||||
verison: "version",
|
||||
ic: "ci",
|
||||
|
||||
// typos
|
||||
innit: "init",
|
||||
// manually abbrev so that install-test doesn't make insta stop working
|
||||
in: "install",
|
||||
ins: "install",
|
||||
inst: "install",
|
||||
insta: "install",
|
||||
instal: "install",
|
||||
isnt: "install",
|
||||
isnta: "install",
|
||||
isntal: "install",
|
||||
isntall: "install",
|
||||
"install-clean": "ci",
|
||||
"isntall-clean": "ci",
|
||||
hlep: "help",
|
||||
"dist-tags": "dist-tag",
|
||||
upgrade: "update",
|
||||
udpate: "update",
|
||||
rum: "run",
|
||||
sit: "install-ci-test",
|
||||
urn: "run",
|
||||
ogr: "org",
|
||||
"add-user": "adduser",
|
||||
};
|
||||
|
||||
export function deref(c) {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate camelCase to snake-case (i.e. installTest to install-test)
|
||||
if (c.match(/[A-Z]/)) {
|
||||
c = c.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
|
||||
}
|
||||
|
||||
// if they asked for something exactly we are done
|
||||
if (commands.includes(c)) {
|
||||
return c;
|
||||
}
|
||||
|
||||
// if they asked for a direct alias
|
||||
if (aliases[c]) {
|
||||
return aliases[c];
|
||||
}
|
||||
|
||||
const abbrevs = abbrev(commands.concat(Object.keys(aliases)));
|
||||
|
||||
// first deref the abbrev, if there is one
|
||||
// then resolve any aliases
|
||||
// so `npm install-cl` will resolve to `install-clean` then to `ci`
|
||||
let a = abbrevs[c];
|
||||
while (aliases[a]) {
|
||||
a = aliases[a];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { deref } from "./cmd-list.js";
|
||||
|
||||
export function getNpmCommandForArgs(args) {
|
||||
if (args.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const argCommand = deref(args[0]);
|
||||
if (!argCommand) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return argCommand;
|
||||
}
|
||||
|
||||
export function hasDryRunArg(args) {
|
||||
return args.some((arg) => arg === "--dry-run");
|
||||
}
|
||||
|
||||
export const npmInstallCommand = "install";
|
||||
export const npmCiCommand = "ci";
|
||||
export const npmInstallTestCommand = "install-test";
|
||||
export const npmInstallCiTestCommand = "install-ci-test";
|
||||
export const npmUpdateCommand = "update";
|
||||
export const npmAuditCommand = "audit";
|
||||
export const npmExecCommand = "exec";
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||
import { runNpx } from "./runNpxCommand.js";
|
||||
|
||||
export function createNpxPackageManager() {
|
||||
const scanner = commandArgumentScanner();
|
||||
|
||||
return {
|
||||
getWarningMessage: () => null,
|
||||
runCommand: runNpx,
|
||||
isSupportedCommand: (args) => scanner.shouldScan(args),
|
||||
getDependencyUpdatesForCommand: (args) => scanner.scan(args),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
||||
|
||||
export function commandArgumentScanner() {
|
||||
return {
|
||||
scan: (args) => scanDependencies(args),
|
||||
shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run
|
||||
};
|
||||
}
|
||||
function scanDependencies(args) {
|
||||
return checkChangesFromArgs(args);
|
||||
}
|
||||
|
||||
export async function checkChangesFromArgs(args) {
|
||||
const changes = [];
|
||||
const packageUpdates = parsePackagesFromArguments(args);
|
||||
|
||||
for (const packageUpdate of packageUpdates) {
|
||||
var exactVersion = await resolvePackageVersion(
|
||||
packageUpdate.name,
|
||||
packageUpdate.version
|
||||
);
|
||||
if (exactVersion) {
|
||||
packageUpdate.version = exactVersion;
|
||||
}
|
||||
|
||||
changes.push({ ...packageUpdate, type: "add" });
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
export function parsePackagesFromArguments(args) {
|
||||
let defaultTag = "latest";
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const option = getOption(arg);
|
||||
|
||||
if (option) {
|
||||
// If the option has a parameter, skip the next argument as well
|
||||
i += option.numberOfParameters;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDetails = parsePackagename(arg, defaultTag);
|
||||
if (packageDetails) {
|
||||
return [packageDetails];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getOption(arg) {
|
||||
if (isOptionWithParameter(arg)) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Arguments starting with "-" or "--" are considered options
|
||||
// except for "--package=" which contains the package name
|
||||
if (arg.startsWith("-") && !arg.startsWith("--package=")) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isOptionWithParameter(arg) {
|
||||
const optionsWithParameters = [
|
||||
"--access",
|
||||
"--auth-type",
|
||||
"--cache",
|
||||
"--fetch-retries",
|
||||
"--fetch-retry-mintimeout",
|
||||
"--fetch-retry-maxtimeout",
|
||||
"--fetch-retry-factor",
|
||||
"--fetch-timeout",
|
||||
"--https-proxy",
|
||||
"--include",
|
||||
"--location",
|
||||
"--lockfile-version",
|
||||
"--loglevel",
|
||||
"--omit",
|
||||
"--proxy",
|
||||
"--registry",
|
||||
"--replace-registry-host",
|
||||
"--tag",
|
||||
"--user-config",
|
||||
"--workspace",
|
||||
];
|
||||
|
||||
return optionsWithParameters.includes(arg);
|
||||
}
|
||||
|
||||
function parsePackagename(arg, defaultTag) {
|
||||
// format can be --package=name@version
|
||||
// in that case, we need to remove the --package= part
|
||||
if (arg.startsWith("--package=")) {
|
||||
arg = arg.slice(10);
|
||||
}
|
||||
|
||||
arg = removeAlias(arg);
|
||||
|
||||
// Split at the last "@" to separate the package name and version
|
||||
const lastAtIndex = arg.lastIndexOf("@");
|
||||
|
||||
let name, version;
|
||||
// The index of the last "@" should be greater than 0
|
||||
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
||||
if (lastAtIndex > 0) {
|
||||
name = arg.slice(0, lastAtIndex);
|
||||
version = arg.slice(lastAtIndex + 1);
|
||||
} else {
|
||||
name = arg;
|
||||
version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
function removeAlias(arg) {
|
||||
// removes the alias.
|
||||
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
||||
const aliasIndex = arg.indexOf("@npm:");
|
||||
if (aliasIndex !== -1) {
|
||||
return arg.slice(aliasIndex + 5);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js";
|
||||
|
||||
describe("parsePackagesFromArguments", () => {
|
||||
it("should return an empty array for no changes", () => {
|
||||
const args = [];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("should return an array of changes for one package", () => {
|
||||
const args = ["http-server@14.1.1"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "14.1.1" }]);
|
||||
});
|
||||
|
||||
it("should return the package in the format @vercel/otel", () => {
|
||||
const args = ["@vercel/otel"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return the package with latest tag if absent", () => {
|
||||
const args = ["http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should ignore double --", () => {
|
||||
const args = ["--", "http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should only return the first package", () => {
|
||||
const args = ["http-server", "jest"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return package with -p option", () => {
|
||||
const args = ["-p", "http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return package with --package option", () => {
|
||||
const args = ["--package", "http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return package with --package=x option", () => {
|
||||
const args = ["--package=http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return package with --package=x@version option", () => {
|
||||
const args = ["--package=http-server@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "1.0.0" }]);
|
||||
});
|
||||
|
||||
it("should ignore options with parameters and return an array of changes", () => {
|
||||
const args = ["--loglevel", "error", "http-server@14.1.1"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "14.1.1" }]);
|
||||
});
|
||||
|
||||
it("should parse version even for aliased packages", () => {
|
||||
const args = ["server@npm:http-server@14.1.1"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "14.1.1" }]);
|
||||
});
|
||||
|
||||
it("should parse scoped packages", () => {
|
||||
const args = ["@scope/package@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]);
|
||||
});
|
||||
|
||||
it("should parse packages with version ranges", () => {
|
||||
const args = ["http-server@^14.1.1"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "http-server", version: "^14.1.1" }]);
|
||||
});
|
||||
|
||||
it("should parse package folders", () => {
|
||||
const args = ["./local-package"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should parse tarballs", () => {
|
||||
const args = ["file:./local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "file:./local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse tarball URLs", () => {
|
||||
const args = ["https://example.com/local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "https://example.com/local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse git URLs", () => {
|
||||
const args = ["git://github.com/http-party/http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "git://github.com/http-party/http-server", version: "latest" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
17
packages/safe-chain/src/packagemanager/npx/runNpxCommand.js
Normal file
17
packages/safe-chain/src/packagemanager/npx/runNpxCommand.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { execSync } from "child_process";
|
||||
import { ui } from "../../environment/userInteraction.js";
|
||||
|
||||
export function runNpx(args) {
|
||||
try {
|
||||
const npxCommand = `npx ${args.join(" ")}`;
|
||||
execSync(npxCommand, { stdio: "inherit" });
|
||||
} catch (error) {
|
||||
if (error.status) {
|
||||
return { status: error.status };
|
||||
} else {
|
||||
ui.writeError("Error executing command:", error.message);
|
||||
return { status: 1 };
|
||||
}
|
||||
}
|
||||
return { status: 0 };
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { matchesCommand } from "../_shared/matchesCommand.js";
|
||||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||
import { runPnpmCommand } from "./runPnpmCommand.js";
|
||||
|
||||
const scanner = commandArgumentScanner();
|
||||
|
||||
export function createPnpmPackageManager() {
|
||||
return {
|
||||
getWarningMessage: () => null,
|
||||
runCommand: (args) => runPnpmCommand(args, "pnpm"),
|
||||
isSupportedCommand: (args) =>
|
||||
matchesCommand(args, "add") ||
|
||||
matchesCommand(args, "update") ||
|
||||
matchesCommand(args, "upgrade") ||
|
||||
matchesCommand(args, "up") ||
|
||||
// dlx does not always come in the first position
|
||||
// eg: pnpm --package=yo --package=generator-webapp dlx yo webapp
|
||||
// documentation: https://pnpm.io/cli/dlx#--package-name
|
||||
args.includes("dlx"),
|
||||
getDependencyUpdatesForCommand: (args) =>
|
||||
getDependencyUpdatesForCommand(args, false),
|
||||
};
|
||||
}
|
||||
|
||||
export function createPnpxPackageManager() {
|
||||
return {
|
||||
getWarningMessage: () => null,
|
||||
runCommand: (args) => runPnpmCommand(args, "pnpx"),
|
||||
isSupportedCommand: () => true,
|
||||
getDependencyUpdatesForCommand: (args) =>
|
||||
getDependencyUpdatesForCommand(args, true),
|
||||
};
|
||||
}
|
||||
|
||||
function getDependencyUpdatesForCommand(args, isPnpx) {
|
||||
if (isPnpx) {
|
||||
return scanner.scan(args);
|
||||
}
|
||||
if (args.includes("dlx")) {
|
||||
// dlx is not always the first argument (eg: `pnpm --package=yo --package=generator-webapp dlx yo webapp`)
|
||||
// so we need to filter it out instead of slicing the array
|
||||
// documentation: https://pnpm.io/cli/dlx#--package-name
|
||||
return scanner.scan(args.filter((arg) => arg !== "dlx"));
|
||||
}
|
||||
return scanner.scan(args.slice(1));
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
||||
|
||||
export function commandArgumentScanner() {
|
||||
return {
|
||||
scan: (args) => scanDependencies(args),
|
||||
shouldScan: () => true, // There's no dry run for pnpm, so we always scan
|
||||
};
|
||||
}
|
||||
|
||||
async function scanDependencies(args) {
|
||||
const changes = [];
|
||||
const packageUpdates = parsePackagesFromArguments(args);
|
||||
|
||||
for (const packageUpdate of packageUpdates) {
|
||||
var exactVersion = await resolvePackageVersion(
|
||||
packageUpdate.name,
|
||||
packageUpdate.version
|
||||
);
|
||||
if (exactVersion) {
|
||||
packageUpdate.version = exactVersion;
|
||||
}
|
||||
|
||||
changes.push({ ...packageUpdate, type: "add" });
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
export function parsePackagesFromArguments(args) {
|
||||
const changes = [];
|
||||
let defaultTag = "latest";
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const option = getOption(arg);
|
||||
|
||||
if (option) {
|
||||
// If the option has a parameter, skip the next argument as well
|
||||
i += option.numberOfParameters;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDetails = parsePackagename(arg, defaultTag);
|
||||
if (packageDetails) {
|
||||
changes.push(packageDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function getOption(arg) {
|
||||
if (isOptionWithParameter(arg)) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Arguments starting with "-" or "--" are considered options
|
||||
// except for "--package=" which contains the package name
|
||||
if (arg.startsWith("-") && !arg.startsWith("--package=")) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isOptionWithParameter(arg) {
|
||||
const optionsWithParameters = ["--C", "--dir"];
|
||||
|
||||
return optionsWithParameters.includes(arg);
|
||||
}
|
||||
|
||||
function parsePackagename(arg, defaultTag) {
|
||||
// format can be --package=name@version
|
||||
// in that case, we need to remove the --package= part
|
||||
if (arg.startsWith("--package=")) {
|
||||
arg = arg.slice(10);
|
||||
}
|
||||
|
||||
arg = removeAlias(arg);
|
||||
|
||||
// Split at the last "@" to separate the package name and version
|
||||
const lastAtIndex = arg.lastIndexOf("@");
|
||||
|
||||
let name, version;
|
||||
// The index of the last "@" should be greater than 0
|
||||
// If the index is 0, it means the package name starts with "@" (eg: "@aikidosec/package-name")
|
||||
if (lastAtIndex > 0) {
|
||||
name = arg.slice(0, lastAtIndex);
|
||||
version = arg.slice(lastAtIndex + 1);
|
||||
} else {
|
||||
name = arg;
|
||||
version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
function removeAlias(arg) {
|
||||
// removes the alias.
|
||||
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
||||
const aliasIndex = arg.indexOf("@npm:");
|
||||
if (aliasIndex !== -1) {
|
||||
return arg.slice(aliasIndex + 5);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js";
|
||||
|
||||
describe("standardPnpmArgumentParser", () => {
|
||||
it("should return an empty array for no changes", () => {
|
||||
const args = [];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("should return an array of changes for one package", () => {
|
||||
const args = ["axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should return the package with latest tag if absent", () => {
|
||||
const args = ["axios"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return the package in the format @vercel/otel", () => {
|
||||
const args = ["@vercel/otel"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should return the package with latest tag if the version is absent and package starts with @", () => {
|
||||
const args = ["@aikidosec/package-name"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "@aikidosec/package-name", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return the package with the specified tag if the package starts with @ and includes the version", () => {
|
||||
const args = ["@aikidosec/package-name@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "@aikidosec/package-name", version: "1.0.0" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should only return all packages", () => {
|
||||
const args = ["axios", "jest"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "axios", version: "latest" },
|
||||
{ name: "jest", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore options with parameters and return an array of changes", () => {
|
||||
const args = ["--C", "/Users/johnsmith/dev/project", "axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse version even for aliased packages", () => {
|
||||
const args = ["server@npm:axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse scoped packages", () => {
|
||||
const args = ["@scope/package@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]);
|
||||
});
|
||||
|
||||
it("should parse packages with version ranges", () => {
|
||||
const args = ["axios@^1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "^1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse package folders", () => {
|
||||
const args = ["./local-package"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should parse tarballs", () => {
|
||||
const args = ["file:./local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "file:./local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse tarball URLs", () => {
|
||||
const args = ["https://example.com/local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "https://example.com/local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse git URLs", () => {
|
||||
const args = ["git://github.com/http-party/http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "git://github.com/http-party/http-server", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse packages with --package={packageName}", () => {
|
||||
const args = ["--package=axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { spawnSync } from "child_process";
|
||||
import { ui } from "../../environment/userInteraction.js";
|
||||
|
||||
export function runPnpmCommand(args, toolName = "pnpm") {
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (toolName === "pnpm") {
|
||||
result = spawnSync("pnpm", args, { stdio: "inherit" });
|
||||
} else if (toolName === "pnpx") {
|
||||
result = spawnSync("pnpx", args, { stdio: "inherit" });
|
||||
} else {
|
||||
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
|
||||
}
|
||||
|
||||
if (result.status !== null) {
|
||||
return { status: result.status };
|
||||
}
|
||||
} catch (error) {
|
||||
ui.writeError("Error executing command:", error.message);
|
||||
return { status: 1 };
|
||||
}
|
||||
return { status: 0 };
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||
import { runYarnCommand } from "./runYarnCommand.js";
|
||||
|
||||
const scanner = commandArgumentScanner();
|
||||
|
||||
export function createYarnPackageManager() {
|
||||
return {
|
||||
getWarningMessage: () => null,
|
||||
runCommand: runYarnCommand,
|
||||
isSupportedCommand: (args) =>
|
||||
matchesCommand(args, "add") ||
|
||||
matchesCommand(args, "global", "add") ||
|
||||
matchesCommand(args, "install") ||
|
||||
matchesCommand(args, "up") ||
|
||||
matchesCommand(args, "upgrade") ||
|
||||
matchesCommand(args, "global", "upgrade") ||
|
||||
matchesCommand(args, "dlx"),
|
||||
getDependencyUpdatesForCommand: (args) => scanner.scan(args),
|
||||
};
|
||||
}
|
||||
|
||||
function matchesCommand(args, ...commandArgs) {
|
||||
if (args.length < commandArgs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < commandArgs.length; i++) {
|
||||
if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
||||
|
||||
export function commandArgumentScanner() {
|
||||
return {
|
||||
scan: (args) => scanDependencies(args),
|
||||
shouldScan: () => true, // There's no dry run for yarn, so we always scan
|
||||
};
|
||||
}
|
||||
|
||||
async function scanDependencies(args) {
|
||||
const changes = [];
|
||||
const packageUpdates = parsePackagesFromArguments(args);
|
||||
|
||||
for (const packageUpdate of packageUpdates) {
|
||||
var exactVersion = await resolvePackageVersion(
|
||||
packageUpdate.name,
|
||||
packageUpdate.version
|
||||
);
|
||||
if (exactVersion) {
|
||||
packageUpdate.version = exactVersion;
|
||||
}
|
||||
|
||||
changes.push({ ...packageUpdate, type: "add" });
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
export function parsePackagesFromArguments(args) {
|
||||
const changes = [];
|
||||
let defaultTag = "latest";
|
||||
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const option = getOption(arg);
|
||||
|
||||
if (option) {
|
||||
// If the option has a parameter, skip the next argument as well
|
||||
i += option.numberOfParameters;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDetails = parsePackagename(arg, defaultTag);
|
||||
if (packageDetails) {
|
||||
changes.push(packageDetails);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function getOption(arg) {
|
||||
if (isOptionWithParameter(arg)) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Arguments starting with "-" or "--" are considered options
|
||||
// except for "--package=" which contains the package name
|
||||
if (arg.startsWith("-")) {
|
||||
return {
|
||||
name: arg,
|
||||
numberOfParameters: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isOptionWithParameter(arg) {
|
||||
const optionsWithParameters = [
|
||||
"--use-yarnrc",
|
||||
"--link-folder",
|
||||
"--global-folder",
|
||||
"--modules-folder",
|
||||
"--preferred-cache-folder",
|
||||
"--cache-folder",
|
||||
"--mutex",
|
||||
"--cwd",
|
||||
"--proxy",
|
||||
"--https-proxy",
|
||||
"--registry",
|
||||
"--network-concurrency",
|
||||
"--network-timeout",
|
||||
"--scripts-prepend-node-path",
|
||||
"--otp",
|
||||
];
|
||||
|
||||
return optionsWithParameters.includes(arg);
|
||||
}
|
||||
|
||||
function parsePackagename(arg, defaultTag) {
|
||||
// format can be --package=name@version
|
||||
// in that case, we need to remove the --package= part
|
||||
if (arg.startsWith("--package=")) {
|
||||
arg = arg.slice(10);
|
||||
}
|
||||
|
||||
arg = removeAlias(arg);
|
||||
|
||||
// Split at the last "@" to separate the package name and version
|
||||
const lastAtIndex = arg.lastIndexOf("@");
|
||||
|
||||
let name, version;
|
||||
// The index of the last "@" should be greater than 0
|
||||
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
||||
if (lastAtIndex > 0) {
|
||||
name = arg.slice(0, lastAtIndex);
|
||||
version = arg.slice(lastAtIndex + 1);
|
||||
} else {
|
||||
name = arg;
|
||||
version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
function removeAlias(arg) {
|
||||
// removes the alias.
|
||||
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
||||
const aliasIndex = arg.indexOf("@npm:");
|
||||
if (aliasIndex !== -1) {
|
||||
return arg.slice(aliasIndex + 5);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js";
|
||||
|
||||
describe("standardYarnArgumentParser", () => {
|
||||
it("should return an empty array for no changes", () => {
|
||||
const args = ["add"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("should return an array of changes for one package", () => {
|
||||
const args = ["add", "axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should return the package with latest tag if absent", () => {
|
||||
const args = ["add", "axios"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should only return all packages", () => {
|
||||
const args = ["add", "axios", "jest"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "axios", version: "latest" },
|
||||
{ name: "jest", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return the package in the format @vercel/otel", () => {
|
||||
const args = ["add", "@vercel/otel"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should ignore options with parameters and return an array of changes", () => {
|
||||
const args = ["add", "--proxy", "http://localhost", "axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse version even for aliased packages", () => {
|
||||
const args = ["add", "server@npm:axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse scoped packages", () => {
|
||||
const args = ["add", "@scope/package@1.0.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]);
|
||||
});
|
||||
|
||||
it("should parse packages with version ranges", () => {
|
||||
const args = ["add", "axios@^1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "^1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse package folders", () => {
|
||||
const args = ["add", "./local-package"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]);
|
||||
});
|
||||
|
||||
it("should parse tarballs", () => {
|
||||
const args = ["add", "file:./local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "file:./local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse tarball URLs", () => {
|
||||
const args = ["add", "https://example.com/local-package.tgz"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "https://example.com/local-package.tgz", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse git URLs", () => {
|
||||
const args = ["add", "git://github.com/http-party/http-server"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{ name: "git://github.com/http-party/http-server", version: "latest" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse packages with -p {packageName}", () => {
|
||||
const args = ["dlx", "-p", "axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
|
||||
it("should parse packages with --package {packageName}", () => {
|
||||
const args = ["dlx", "--package", "axios@1.9.0"];
|
||||
|
||||
const result = parsePackagesFromArguments(args);
|
||||
|
||||
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { execSync } from "child_process";
|
||||
import { ui } from "../../environment/userInteraction.js";
|
||||
|
||||
export function runYarnCommand(args) {
|
||||
try {
|
||||
const npxCommand = `yarn ${args.join(" ")}`;
|
||||
execSync(npxCommand, { stdio: "inherit" });
|
||||
} catch (error) {
|
||||
if (error.status) {
|
||||
return { status: error.status };
|
||||
} else {
|
||||
ui.writeError("Error executing command:", error.message);
|
||||
return { status: 1 };
|
||||
}
|
||||
}
|
||||
return { status: 0 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue