UNPKG

@salesforce/plugin-release-management

Version:
240 lines 9.8 kB
#!/usr/bin/env node /* * 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