@salesforce/plugin-trust
Version:
validate a digital signature for a npm package
207 lines • 8.06 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import os from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import fs from 'node:fs';
import npmRunPath from 'npm-run-path';
import shelljs from 'shelljs';
import { SfError } from '@salesforce/core';
import { sleep, parseJson } from '@salesforce/kit';
import { Ux } from '@salesforce/sf-plugins-core';
import { setErrorName } from './errors.js';
export class NpmCommand {
static runNpmCmd(cmd, options = {}) {
const nodeExecutable = NpmCommand.findNode(options.cliRoot);
const npmCli = NpmCommand.npmCli();
const command = `"${nodeExecutable}" "${npmCli}" ${cmd} --registry=${options.registry ?? ''}`;
const npmCmdResult = shelljs.exec(command, {
...options,
silent: true,
async: false,
env: npmRunPath.env({ env: process.env }),
});
if (npmCmdResult.code !== 0) {
throw new SfError(npmCmdResult.stderr, 'ShellExecError');
}
return npmCmdResult;
}
static npxCli() {
const require = createRequire(import.meta.url);
const pkgPath = require.resolve('npm/package.json');
const fileData = fs.readFileSync(pkgPath, 'utf8');
const pkgJson = parseJson(fileData, pkgPath);
const prjPath = pkgPath.substring(0, pkgPath.lastIndexOf(path.sep));
return path.join(prjPath, pkgJson.bin['npx']);
}
/**
* Locate node executable and return its absolute path
* First it tries to locate the node executable on the root path passed in
* If not found then tries to use whatver 'node' resolves to on the user's PATH
* If found return absolute path to the executable
* If the node executable cannot be found, an error is thrown
*
* @private
*/
static findNode(root) {
const isExecutable = (filepath) => {
if (os.type() === 'Windows_NT')
return filepath.endsWith('node.exe');
try {
if (filepath.endsWith('node')) {
// This checks if the filepath is executable on Mac or Linux, if it is not it errors.
fs.accessSync(filepath, fs.constants.X_OK);
return true;
}
}
catch {
return false;
}
return false;
};
if (root) {
const sfdxBinDirs = NpmCommand.findSfdxBinDirs(root);
if (sfdxBinDirs.length > 0) {
// Find the node executable
const node = shelljs.find(sfdxBinDirs).filter((file) => isExecutable(file))[0];
if (node) {
return fs.realpathSync(node);
}
}
}
// Check to see if node is installed
const nodeShellString = shelljs.which('node');
if (nodeShellString?.code === 0 && nodeShellString?.stdout)
return nodeShellString.stdout;
throw setErrorName(new SfError('Cannot locate node executable.', 'CannotFindNodeExecutable'), 'CannotFindNodeExecutable');
}
/**
* Returns the path to the npm-cli.js file in this package's node_modules
*
* @private
*/
static npmCli() {
const require = createRequire(import.meta.url);
const pkgPath = require.resolve('npm/package.json');
const fileData = fs.readFileSync(pkgPath, 'utf8');
const pkgJson = parseJson(fileData, pkgPath);
const prjPath = pkgPath.substring(0, pkgPath.lastIndexOf(path.sep));
return path.join(prjPath, pkgJson.bin['npm']);
}
/**
* Finds the bin directory in the sfdx installation root path
*
* @param sfdxPath
* @private
*/
static findSfdxBinDirs(sfdxPath) {
return sfdxPath
? [path.join(sfdxPath, 'bin'), path.join(sfdxPath, 'client', 'bin')].filter((p) => fs.existsSync(p))
: [];
}
}
export class NpmModule {
module;
version;
cliRoot;
npmMeta;
constructor(module, version = 'latest', cliRoot) {
this.module = module;
this.version = version;
this.cliRoot = cliRoot;
this.npmMeta = {
moduleName: module,
};
}
ping(registry) {
return JSON.parse(NpmCommand.runNpmCmd(`ping ${registry ?? ''} --json`, { json: true, cliRoot: this.cliRoot }));
}
run(command) {
return NpmCommand.runNpmCmd(command, { cliRoot: this.cliRoot, json: command.includes('--json') });
}
show(registry) {
const showCmd = NpmCommand.runNpmCmd(`show ${this.module}@${this.version} --json`, {
registry,
cliRoot: this.cliRoot,
});
// `npm show` doesn't return exit code 1 when it fails to get a specific package version
// If `stdout` is empty then no info was found in the registry.
if (showCmd.stdout === '') {
throw setErrorName(new SfError(`Failed to find ${this.module}@${this.version} in the registry`, 'NpmError'), 'NpmError');
}
try {
// `npm show` returns an array of results when the version is a range
const raw = JSON.parse(showCmd.stdout);
if (Array.isArray(raw)) {
// Return the last result in the array since that will be the highest version
// NOTE: .at() possibly returns undefined so instead directly index the array for the last element
return raw[raw.length - 1];
}
return raw;
}
catch (error) {
if (error instanceof Error) {
throw setErrorName(new SfError(error.message, 'ShellParseError'), 'ShellParseError');
}
throw error;
}
}
pack(registry, options) {
try {
NpmCommand.runNpmCmd(`pack ${this.module}@${this.version}`, {
...options,
registry,
cliRoot: this.cliRoot,
});
}
catch (err) {
if (err instanceof Error) {
const sfErr = SfError.wrap(err);
const e = new SfError(`Failed to fetch tarball from the registry: \n${sfErr.message}`, 'NpmError');
throw setErrorName(e, 'NpmError');
}
}
return;
}
async fetchTarball(registry, options) {
await this.pollForAvailability(() => {
this.pack(registry, options);
});
this.pack(registry, options);
}
// leave it because it's stubbed in the test
// eslint-disable-next-line class-methods-use-this
async pollForAvailability(checkFn) {
const isNonTTY = process.env.CI !== undefined || process.env.CIRCLECI !== undefined;
let found = false;
let attempts = 0;
const maxAttempts = 300;
const ux = new Ux({ jsonEnabled: isNonTTY });
const start = isNonTTY ? (msg) => ux.log(msg) : (msg) => ux.spinner.start(msg);
const update = isNonTTY ? (msg) => ux.log(msg) : (msg) => (ux.spinner.status = msg);
const stop = isNonTTY ? (msg) => ux.log(msg) : (msg) => ux.spinner.stop(msg);
start('Polling for new version(s) to become available on npm');
while (!found && attempts < maxAttempts) {
attempts += 1;
update(`attempt: ${attempts} of ${maxAttempts}`);
try {
checkFn();
found = true;
}
catch (error) {
if (attempts === maxAttempts) {
throw error;
}
found = false;
}
// eslint-disable-next-line no-await-in-loop
await sleep(1000);
}
stop(attempts >= maxAttempts ? 'failed' : 'done');
}
}
//# sourceMappingURL=npmCommand.js.map