Type check safe-chain package

This commit is contained in:
Hans Ott 2025-11-01 13:06:06 +01:00
parent d5dc801c00
commit c88b1a624f
60 changed files with 1179 additions and 33 deletions

View file

@ -1,3 +1,8 @@
/**
* @param {string[]} args
* @param {...string} commandArgs
* @returns {boolean}
*/
export function matchesCommand(args, ...commandArgs) {
if (args.length < commandArgs.length) {
return false;

View file

@ -2,6 +2,9 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createBunPackageManager() {
return {
runCommand: (args) => runBunCommand("bun", args),
@ -9,10 +12,13 @@ export function createBunPackageManager() {
// For bun, we use the proxy-only approach to block package downloads,
// so we don't need to analyze commands.
isSupportedCommand: () => false,
getDependencyUpdatesForCommand: () => [],
getDependencyUpdatesForCommand: async () => [],
};
}
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createBunxPackageManager() {
return {
runCommand: (args) => runBunCommand("bunx", args),
@ -20,18 +26,24 @@ export function createBunxPackageManager() {
// For bunx, we use the proxy-only approach to block package downloads,
// so we don't need to analyze commands.
isSupportedCommand: () => false,
getDependencyUpdatesForCommand: () => [],
getDependencyUpdatesForCommand: async () => [],
};
}
/**
* @param {string} command
* @param {string[]} args
* @returns {Promise<{status: number}>}
*/
async function runBunCommand(command, args) {
try {
const result = await safeSpawn(command, args, {
stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
return { status: result.status };
} catch (error) {
} catch (/** @type any */ error) {
if (error.status) {
return { status: error.status };
} else {

View file

@ -10,10 +10,25 @@ import {
} from "./pnpm/createPackageManager.js";
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
/**
* @type {{packageManagerName: PackageManager | null}}
*/
const state = {
packageManagerName: null,
};
/**
* @typedef PackageManager
* @property {(args: string[]) => Promise<{ status: number }>} runCommand
* @property {(args: string[]) => boolean} isSupportedCommand
* @property {(args: string[]) => Promise<{name: string, version: string, type: string}[]>} getDependencyUpdatesForCommand
*/
/**
* @param {string} packageManagerName
*
* @return {PackageManager}
*/
export function initializePackageManager(packageManagerName) {
if (packageManagerName === "npm") {
state.packageManagerName = createNpmPackageManager();

View file

@ -8,7 +8,15 @@ import {
npmExecCommand,
} from "./utils/npmCommands.js";
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createNpmPackageManager() {
/**
* @param {string[]} args
*
* @returns {boolean}
*/
function isSupportedCommand(args) {
const scanner = findDependencyScannerForCommand(
commandScannerMapping,
@ -17,6 +25,11 @@ export function createNpmPackageManager() {
return scanner.shouldScan(args);
}
/**
* @param {string[]} args
*
* @returns {Promise<{name: string, version: string, type: string}[]>}
*/
function getDependencyUpdatesForCommand(args) {
const scanner = findDependencyScannerForCommand(
commandScannerMapping,
@ -32,12 +45,22 @@ export function createNpmPackageManager() {
};
}
/**
* @type {Record<string, import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner>}
*/
const commandScannerMapping = {
[npmInstallCommand]: commandArgumentScanner(),
[npmUpdateCommand]: commandArgumentScanner(),
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
};
/**
*
* @param {Record<string, import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner>} scanners
* @param {string[]} args
*
* @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
*/
function findDependencyScannerForCommand(scanners, args) {
const command = getNpmCommandForArgs(args);
if (!command) {

View file

@ -2,6 +2,29 @@ import { resolvePackageVersion } from "../../../api/npmApi.js";
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
import { hasDryRunArg } from "../utils/npmCommands.js";
/**
* @typedef {Object} ScanResult
* @property {string} name
* @property {string} version
* @property {string} type
*/
/**
* @typedef {Object} ScannerOptions
* @property {boolean} [ignoreDryRun]
*/
/**
* @typedef CommandArgumentScanner
* @property {(args: string[]) => Promise<ScanResult[]>} scan
* @property {(args: string[]) => boolean} shouldScan
*/
/**
* @param {ScannerOptions} [opts]
*
* @returns {CommandArgumentScanner}
*/
export function commandArgumentScanner(opts) {
const ignoreDryRun = opts?.ignoreDryRun ?? false;
@ -10,14 +33,28 @@ export function commandArgumentScanner(opts) {
shouldScan: (args) => shouldScanDependencies(args, ignoreDryRun),
};
}
/**
* @param {string[]} args
* @returns {Promise<ScanResult[]>}
*/
function scanDependencies(args) {
return checkChangesFromArgs(args);
}
/**
* @param {string[]} args
* @param {boolean} ignoreDryRun
* @returns {boolean}
*/
function shouldScanDependencies(args, ignoreDryRun) {
return ignoreDryRun || !hasDryRunArg(args);
}
/**
* @param {string[]} args
* @returns {Promise<ScanResult[]>}
*/
export async function checkChangesFromArgs(args) {
const changes = [];
const packageUpdates = parsePackagesFromInstallArgs(args);

View file

@ -1,6 +1,9 @@
/**
* @returns {import("./commandArgumentScanner.js").CommandArgumentScanner}
*/
export function nullScanner() {
return {
scan: () => [],
scan: async () => [],
shouldScan: () => false,
};
}

View file

@ -1,5 +1,22 @@
/**
* @typedef {Object} PackageDetail
* @property {string} name
* @property {string} version
*/
/**
* @typedef {Object} NpmOption
* @property {string} name
* @property {number} numberOfParameters
*/
/**
* @param {string[]} args
* @returns {PackageDetail[]}
*/
export function parsePackagesFromInstallArgs(args) {
const changes = [];
/** @type {{name: string, version: string | null}[]} */
const changes = [];
let defaultTag = "latest";
// Skip first argument (install command)
@ -32,9 +49,13 @@ export function parsePackagesFromInstallArgs(args) {
}
}
return changes;
return /** @type {PackageDetail[]} */ (changes);
}
/**
* @param {string} arg
* @returns {NpmOption | undefined}
*/
function getNpmOption(arg) {
if (isNpmOptionWithParameter(arg)) {
return {
@ -54,6 +75,10 @@ function getNpmOption(arg) {
return undefined;
}
/**
* @param {string} arg
* @returns {boolean}
*/
function isNpmOptionWithParameter(arg) {
const optionsWithParameters = [
"--access",
@ -81,6 +106,10 @@ function isNpmOptionWithParameter(arg) {
return optionsWithParameters.includes(arg);
}
/**
* @param {string} arg
* @returns {{name: string, version: string | null}}
*/
function parsePackagename(arg) {
arg = removeAlias(arg);
const lastAtIndex = arg.lastIndexOf("@");
@ -102,6 +131,10 @@ function parsePackagename(arg) {
};
}
/**
* @param {string} arg
* @returns {string}
*/
function removeAlias(arg) {
const aliasIndex = arg.indexOf("@npm:");
if (aliasIndex !== -1) {

View file

@ -2,14 +2,20 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
/**
* @param {string[]} args
*
* @returns {Promise<{status: number}>}
*/
export async function runNpm(args) {
try {
const result = await safeSpawn("npm", args, {
stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
return { status: result.status };
} catch (error) {
} catch (/** @type any */ error) {
if (error.status) {
return { status: error.status };
} else {
@ -19,6 +25,10 @@ export async function runNpm(args) {
}
}
/**
* @param {string[]} args
* @returns {Promise<{status: number, output?: string}>}
*/
export async function dryRunNpmCommandAndOutput(args) {
try {
const result = await safeSpawn(
@ -26,6 +36,7 @@ export async function dryRunNpmCommandAndOutput(args) {
[...args, "--ignore-scripts", "--dry-run"],
{
stdio: "pipe",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env),
}
);
@ -33,7 +44,7 @@ export async function dryRunNpmCommandAndOutput(args) {
status: result.status,
output: result.status === 0 ? result.stdout : result.stderr,
};
} catch (error) {
} catch (/** @type any */ error) {
if (error.status) {
const output =
error.stdout?.toString() ??

View file

@ -1,5 +1,6 @@
// This was ran with the abbrev package to generate the abbrevs object below
// console.log(abbrev(commands.concat(Object.keys(aliases))));
/** @type {Record<string, string>} */
export const abbrevs = {
ac: "access",
acc: "access",

View file

@ -73,6 +73,7 @@ const commands = [
];
// These must resolve to an entry in commands
/** @type {Record<string, string>} */
const aliases = {
// aliases
author: "owner",
@ -138,6 +139,10 @@ const aliases = {
"add-user": "adduser",
};
/**
* @param {string} c
* @returns {string | undefined}
*/
export function deref(c) {
if (!c) {
return;

View file

@ -1,5 +1,9 @@
import { deref } from "./cmd-list.js";
/**
* @param {string[]} args
* @returns {string | null}
*/
export function getNpmCommandForArgs(args) {
if (args.length === 0) {
return null;
@ -13,6 +17,10 @@ export function getNpmCommandForArgs(args) {
return argCommand;
}
/**
* @param {string[]} args
* @returns {boolean}
*/
export function hasDryRunArg(args) {
return args.some((arg) => arg === "--dry-run");
}

View file

@ -1,6 +1,9 @@
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
import { runNpx } from "./runNpxCommand.js";
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createNpxPackageManager() {
const scanner = commandArgumentScanner();

View file

@ -1,16 +1,28 @@
import { resolvePackageVersion } from "../../../api/npmApi.js";
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
/**
* @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
*/
export function commandArgumentScanner() {
return {
scan: (args) => scanDependencies(args),
shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run
shouldScan: (args) => true, // all npx commands need to be scanned, npx doesn't have dry-run
};
}
/**
* @param {string[]} args
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
*/
function scanDependencies(args) {
return checkChangesFromArgs(args);
}
/**
* @param {string[]} args
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
*/
export async function checkChangesFromArgs(args) {
const changes = [];
const packageUpdates = parsePackagesFromArguments(args);

View file

@ -1,3 +1,8 @@
/**
* @param {string[]} args
*
* @returns {{name: string, version: string}[]}
*/
export function parsePackagesFromArguments(args) {
let defaultTag = "latest";
@ -21,6 +26,10 @@ export function parsePackagesFromArguments(args) {
return [];
}
/**
* @param {string} arg
* @returns {{name: string, numberOfParameters: number} | undefined}
*/
function getOption(arg) {
if (isOptionWithParameter(arg)) {
return {
@ -41,6 +50,10 @@ function getOption(arg) {
return undefined;
}
/**
* @param {string} arg
* @returns {boolean}
*/
function isOptionWithParameter(arg) {
const optionsWithParameters = [
"--access",
@ -68,6 +81,11 @@ function isOptionWithParameter(arg) {
return optionsWithParameters.includes(arg);
}
/**
* @param {string} arg
* @param {string} defaultTag
* @returns {{name: string, version: string}}
*/
function parsePackagename(arg, defaultTag) {
// format can be --package=name@version
// in that case, we need to remove the --package= part
@ -97,6 +115,10 @@ function parsePackagename(arg, defaultTag) {
};
}
/**
* @param {string} arg
* @returns {string}
*/
function removeAlias(arg) {
// removes the alias.
// Eg.: server@npm:http-server@latest becomes http-server@latest

View file

@ -2,14 +2,20 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
/**
* @param {string[]} args
*
* @returns {Promise<{status: number}>}
*/
export async function runNpx(args) {
try {
const result = await safeSpawn("npx", args, {
stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
return { status: result.status };
} catch (error) {
} catch (/** @type any */ error) {
if (error.status) {
return { status: error.status };
} else {

View file

@ -4,6 +4,9 @@ import { runPnpmCommand } from "./runPnpmCommand.js";
const scanner = commandArgumentScanner();
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createPnpmPackageManager() {
return {
runCommand: (args) => runPnpmCommand(args, "pnpm"),
@ -23,6 +26,9 @@ export function createPnpmPackageManager() {
};
}
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createPnpxPackageManager() {
return {
runCommand: (args) => runPnpmCommand(args, "pnpx"),
@ -32,6 +38,11 @@ export function createPnpxPackageManager() {
};
}
/**
* @param {string[]} args
* @param {boolean} isPnpx
* @returns {Promise<import("../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
*/
function getDependencyUpdatesForCommand(args, isPnpx) {
if (isPnpx) {
return scanner.scan(args);

View file

@ -1,6 +1,9 @@
import { resolvePackageVersion } from "../../../api/npmApi.js";
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
/**
* @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
*/
export function commandArgumentScanner() {
return {
scan: (args) => scanDependencies(args),
@ -8,6 +11,10 @@ export function commandArgumentScanner() {
};
}
/**
* @param {string[]} args
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
*/
async function scanDependencies(args) {
const changes = [];
const packageUpdates = parsePackagesFromArguments(args);

View file

@ -1,3 +1,7 @@
/**
* @param {string[]} args
* @returns {{name: string, version: string}[]}
*/
export function parsePackagesFromArguments(args) {
const changes = [];
let defaultTag = "latest";
@ -22,6 +26,10 @@ export function parsePackagesFromArguments(args) {
return changes;
}
/**
* @param {string} arg
* @returns {{name: string, numberOfParameters: number} | undefined}
*/
function getOption(arg) {
if (isOptionWithParameter(arg)) {
return {
@ -42,12 +50,21 @@ function getOption(arg) {
return undefined;
}
/**
* @param {string} arg
* @returns {boolean}
*/
function isOptionWithParameter(arg) {
const optionsWithParameters = ["--C", "--dir"];
return optionsWithParameters.includes(arg);
}
/**
* @param {string} arg
* @param {string} defaultTag
* @returns {{name: string, version: string}}
*/
function parsePackagename(arg, defaultTag) {
// format can be --package=name@version
// in that case, we need to remove the --package= part
@ -77,6 +94,10 @@ function parsePackagename(arg, defaultTag) {
};
}
/**
* @param {string} arg
* @returns {string}
*/
function removeAlias(arg) {
// removes the alias.
// Eg.: server@npm:http-server@latest becomes http-server@latest

View file

@ -2,17 +2,24 @@ import { ui } from "../../environment/userInteraction.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
/**
* @param {string[]} args
* @param {string} [toolName]
* @returns {Promise<{status: number}>}
*/
export async function runPnpmCommand(args, toolName = "pnpm") {
try {
let result;
if (toolName === "pnpm") {
result = await safeSpawn("pnpm", args, {
stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
} else if (toolName === "pnpx") {
result = await safeSpawn("pnpx", args, {
stdio: "inherit",
// @ts-expect-error values of process.env can be string | undefined
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
} else {
@ -20,7 +27,7 @@ export async function runPnpmCommand(args, toolName = "pnpm") {
}
return { status: result.status };
} catch (error) {
} catch (/** @type any */ error) {
if (error.status) {
return { status: error.status };
} else {

View file

@ -3,6 +3,9 @@ import { runYarnCommand } from "./runYarnCommand.js";
const scanner = commandArgumentScanner();
/**
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createYarnPackageManager() {
return {
runCommand: runYarnCommand,
@ -18,6 +21,11 @@ export function createYarnPackageManager() {
};
}
/**
* @param {string[]} args
* @param {...string} commandArgs
* @returns {boolean}
*/
function matchesCommand(args, ...commandArgs) {
if (args.length < commandArgs.length) {
return false;

View file

@ -1,6 +1,9 @@
import { resolvePackageVersion } from "../../../api/npmApi.js";
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
/**
* @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
*/
export function commandArgumentScanner() {
return {
scan: (args) => scanDependencies(args),
@ -8,6 +11,10 @@ export function commandArgumentScanner() {
};
}
/**
* @param {string[]} args
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
*/
async function scanDependencies(args) {
const changes = [];
const packageUpdates = parsePackagesFromArguments(args);

View file

@ -1,3 +1,7 @@
/**
* @param {string[]} args
* @returns {{name: string, version: string}[]}
*/
export function parsePackagesFromArguments(args) {
const changes = [];
let defaultTag = "latest";
@ -22,6 +26,11 @@ export function parsePackagesFromArguments(args) {
return changes;
}
/**
* @param {string} arg
*
* @returns {{name: string, numberOfParameters: number} | undefined}
*/
function getOption(arg) {
if (isOptionWithParameter(arg)) {
return {
@ -42,6 +51,11 @@ function getOption(arg) {
return undefined;
}
/**
* @param {string} arg
*
* @returns {boolean}
*/
function isOptionWithParameter(arg) {
const optionsWithParameters = [
"--use-yarnrc",
@ -64,6 +78,12 @@ function isOptionWithParameter(arg) {
return optionsWithParameters.includes(arg);
}
/**
* @param {string} arg
* @param {string} defaultTag
*
* @returns {{name: string, version: string}}
*/
function parsePackagename(arg, defaultTag) {
// format can be --package=name@version
// in that case, we need to remove the --package= part
@ -93,6 +113,10 @@ function parsePackagename(arg, defaultTag) {
};
}
/**
* @param {string} arg
* @returns {string}
*/
function removeAlias(arg) {
// removes the alias.
// Eg.: server@npm:http-server@latest becomes http-server@latest

View file

@ -2,8 +2,14 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
/**
* @param {string[]} args
*
* @returns {Promise<{status: number}>}
*/
export async function runYarnCommand(args) {
try {
// @ts-expect-error values of process.env can be string | undefined
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
await fixYarnProxyEnvironmentVariables(env);
@ -12,7 +18,7 @@ export async function runYarnCommand(args) {
env,
});
return { status: result.status };
} catch (error) {
} catch (/** @type any */ error) {
if (error.status) {
return { status: error.status };
} else {
@ -22,6 +28,11 @@ export async function runYarnCommand(args) {
}
}
/**
* @param {Record<string, string>} env
*
* @returns {Promise<void>}
*/
async function fixYarnProxyEnvironmentVariables(env) {
// Yarn ignores standard proxy environment variable HTTPS_PROXY
// It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though.