@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
240 lines • 9.8 kB
JavaScript
/*
* Copyright (c) 2018, 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 fs from 'node:fs/promises';
import cp from 'node:child_process';
import { EOL } from 'node:os';
import { join as pathJoin } from 'node:path';
import { Logger, SfError } from '@salesforce/core';
import { ProxyAgent } from 'proxy-agent';
import { parseNpmName } from '@salesforce/plugin-trust/npmName';
import { signVerifyUpload as sign2, getSfdxProperty } from './SimplifiedSigning.js';
class PathGetter {
static packageJson = 'package.json';
#packageJson;
#packageJsonBak;
#target;
#cwd;
constructor(target) {
this.#cwd = process.cwd();
if (!target) {
this.#target = this.#cwd;
}
else if (target?.includes(this.#cwd)) {
this.#target = target;
}
else {
this.#target = pathJoin(this.#cwd, target);
}
this.#packageJson = pathJoin(this.#target, PathGetter.packageJson);
this.#packageJsonBak = pathJoin(this.#target, `${PathGetter.packageJson}.bak`);
}
get packageJson() {
return this.#packageJson;
}
get packageJsonBak() {
return this.#packageJsonBak;
}
get target() {
return this.#target;
}
getFile(filename) {
return pathJoin(this.#target, filename);
}
getIgnoreFile(filename) {
return pathJoin(this.#cwd, filename);
}
}
let cliUx;
let pathGetter;
export const api = {
setUx(ux) {
cliUx = ux;
},
/**
* call out to npm pack;
*/
pack() {
if (!pathGetter)
pathGetter = new PathGetter();
return new Promise((resolve, reject) => {
const command = 'npm pack -p';
cp.exec(command, { cwd: pathGetter.target, maxBuffer: 1024 * 4096 },
// we expect an error code from this command, so we're adding it to the normal Error type
(error, stdout, stderr) => {
if (error?.code) {
return reject(new SfError(`Exec'd subprocess ${command} failed with error code '${error['code']}' and message '${stderr}'.`, 'SubProcessError'));
}
else {
const output = stdout.split(EOL);
if (output.length > 1) {
// note the output end with a newline;
const path = output[output.length - 2];
if (path?.endsWith('tgz')) {
return resolve(pathGetter.getFile(path));
}
else {
return reject(new SfError(`Npm pack did not return an expected tgz filename result: [${path}]`, 'UnexpectedNpmFormat'));
}
}
else {
return reject(new SfError(`The output from the npm utility is unexpected [${stdout}]`, 'UnexpectedNpmFormat'));
}
}
});
});
},
/**
* read the package.json file for the target npm to be signed.
*/
retrievePackageJson() {
return fs.readFile(pathGetter.packageJson, { encoding: 'utf8' });
},
/**
* read the npm ignore file for the target npm
*
* @param filename - local path to the npmignore file
*/
retrieveIgnoreFile(filename) {
return fs.readFile(pathGetter.getIgnoreFile(filename), { encoding: 'utf8' });
},
/**
* checks the ignore content for the code signing patterns. *.tgz, *.sig package.json.bak
*
* @param content
*/
validateNpmIgnorePatterns(content) {
const validate = (pattern) => {
if (!content) {
throw new SfError('Missing .npmignore file. The following patterns are required in for code signing: *.tgz, *.sig, package.json.bak.', 'MissingNpmIgnoreFile');
}
if (!content.includes(pattern)) {
throw new SfError(`.npmignore is missing ${pattern}. The following patterns are required for code signing: *.tgz, *.sig, package.json.bak`, 'MissingNpmIgnorePattern');
}
};
validate('*.tgz');
validate('*.sig');
validate('package.json.bak');
},
/**
* checks the ignore content for the code signing patterns. *.tgz, *.sig package.json.bak
*
* @param content
*/
validateNpmFilePatterns(patterns) {
const validate = (pattern) => {
if (patterns.includes(pattern)) {
throw new SfError('the files property in package.json should not include the following: *.tgz, *.sig, package.json.bak', 'ForbiddenFilePattern');
}
};
validate('*.tgz');
validate('*.sig');
validate('package.json.bak');
},
/**
* makes a backup copy pf package.json
*
* @param src - the package.json to backup
* @param dest - package.json.bak
*/
async copyPackageDotJson(src, dest) {
await fs.copyFile(src, dest);
},
/**
* used to update the contents of package.json
*
* @param pJson - the updated json content to write to disk
*/
writePackageJson(pJson) {
return fs.writeFile(pathGetter.packageJson, JSON.stringify(pJson, null, 4));
},
async revertPackageJsonIfExists() {
try {
// Restore the package.json file so it doesn't show a git diff.
await fs.access(pathGetter.packageJsonBak);
cliUx.log(`Restoring package.json from ${pathGetter.packageJsonBak}`);
await api.copyPackageDotJson(pathGetter.packageJsonBak, pathGetter.packageJson);
await fs.unlink(pathGetter.packageJsonBak);
}
catch {
// It's okay that the backup doesn't exist - do nothing
}
},
/**
* main method to pack and sign an npm.
*
* @param args - reference to process.argv
* @param ux - The cli ux interface usually provided by oclif.
* @return {Promise<SigningResponse>} The SigningResponse
*/
async packSignVerifyModifyPackageJSON(targetPackagePath) {
const logger = await Logger.child('packAndSign');
pathGetter = new PathGetter(targetPackagePath);
try {
// read package.json info
const packageJsonContent = await api.retrievePackageJson();
const packageJson = JSON.parse(packageJsonContent);
logger.debug('parsed the package.json content');
if (packageJson.files) {
// validate that files property does not include forbidden patterns
api.validateNpmFilePatterns(packageJson.files);
}
else {
// validate npm ignore has what we name.
const npmIgnoreContent = await api.retrieveIgnoreFile('.npmignore');
api.validateNpmIgnorePatterns(npmIgnoreContent);
logger.debug('validated the expected npm ignore patterns');
}
// Recommend updating git ignore to match npmignore.
const filename = '.gitignore';
const gitIgnoreContent = await api.retrieveIgnoreFile(filename);
try {
api.validateNpmIgnorePatterns(gitIgnoreContent);
logger.debug('validated the expected git ignore patterns');
}
catch (e) {
cliUx.warn(`WARNING: The following patterns are recommended in ${filename} for code signing: *.tgz, *.sig, package.json.bak.`);
}
// get the packageJson name/version
const npmName = parseNpmName(packageJson.name);
logger.debug(`parsed the following npmName components: ${JSON.stringify(npmName, null, 4)}`);
npmName.tag = packageJson.version;
// make a backup of the packageJson
await api.copyPackageDotJson(pathGetter.packageJson, pathGetter.packageJsonBak);
logger.debug('made a backup of the package.json file.');
cliUx.log(`Backed up ${pathGetter.packageJson} to ${pathGetter.packageJsonBak}`);
const packageNameWithOrWithoutScope = npmName.scope ? `@${npmName.scope}/${npmName.name}` : npmName.name;
// we have to modify package.json with security URLs BEFORE packing
// update the package.json object with the signature urls and write it to disk.
packageJson.sfdx = getSfdxProperty(packageNameWithOrWithoutScope, npmName.tag);
await api.writePackageJson(packageJson);
cliUx.log('Successfully updated package.json with public key and signature file locations.');
cliUx.styledJSON(packageJson.sfdx);
const filepath = await api.pack();
cliUx.log(`Packed tgz to ${filepath}`);
const signResponse = await sign2({
upload: true,
targetFileToSign: filepath,
packageName: packageNameWithOrWithoutScope,
packageVersion: npmName.tag,
});
return signResponse;
}
finally {
// prevent any publish-time changes from persisting to git
await api.revertPackageJsonIfExists();
}
},
// preserve previous behavior when the param was used.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAgentForUri(url) {
const agent = new ProxyAgent();
/* eslint-disable @typescript-eslint/no-unsafe-call */
return { https: agent, http: agent };
},
};
//# sourceMappingURL=packAndSign.js.map