@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
261 lines • 12.3 kB
JavaScript
/*
* Copyright (c) 2021, 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 path from 'node:path';
import fg from 'fast-glob';
import shelljs from 'shelljs';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import { ensure, ensureNumber, get } from '@salesforce/ts-types';
import chalk from 'chalk';
import { parseJson } from '@salesforce/kit';
import { CLI } from '../../../types.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-release-management', 'cli.tarballs.verify');
const PASSED = chalk.green.bold('PASSED');
const FAILED = chalk.red.bold('FAILED');
/**
* Checks if a file path exists
*
* @param filePath the file path to check the existence of
*/
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
}
catch (err) {
return false;
}
}
/**
* The functionality of this command is taken entirely from https://github.com/salesforcecli/sfdx-cli/blob/v7.109.0/scripts/verify-tarballs
*/
export default class Verify extends SfCommand {
static summary = messages.getMessage('description');
static description = messages.getMessage('description');
static examples = messages.getMessages('examples');
static flags = {
cli: Flags.custom({
options: Object.values(CLI),
})({
summary: messages.getMessage('cli'),
default: CLI.SFDX,
char: 'c',
}),
['windows-username-buffer']: Flags.integer({
summary: messages.getMessage('flags.windows-username-buffer.summary'),
default: 41,
char: 'w',
}),
};
baseDir;
step = 1;
totalSteps = 1;
flags;
async run() {
const { flags } = await this.parse(Verify);
this.flags = flags;
const cli = ensure(this.flags.cli);
this.baseDir = path.join('tmp', cli);
const cliRunLists = {
[CLI.SFDX]: [
this.ensureNoWebDriverIO.bind(this),
this.ensureNoHerokuCliUtilNyc.bind(this),
this.ensureWindowsPathLengths.bind(this),
this.ensureApexNode.bind(this),
this.ensureTemplatesCommands.bind(this),
this.ensureNoDistTestsOrMaps.bind(this),
this.ensureNoUnexpectedFiles.bind(this),
this.ensureSfIsIncluded.bind(this),
],
[CLI.SF]: [
this.ensureNoDistTestsOrMaps.bind(this),
this.ensureNoUnexpectedFiles.bind(this),
this.ensureWindowsPathLengths.bind(this),
this.ensureMdMessagesExist.bind(this),
],
};
this.totalSteps = cliRunLists[cli].length;
for (const test of cliRunLists[cli]) {
// eslint-disable-next-line no-await-in-loop
await test();
}
}
async execute(msg, validate) {
this.spinner.start(`[${this.step}/${this.totalSteps}] ${msg}`);
if (!(await validate())) {
this.spinner.stop(FAILED);
return false;
}
this.step += 1;
this.spinner.stop(PASSED);
return true;
}
async ensureNoWebDriverIO() {
const webDriverIo = path.join(this.baseDir, 'node_modules', 'webdriverio', 'test');
const validate = async () => !(await fileExists(webDriverIo));
const passed = await this.execute('Ensure webdriverio does not exist', validate);
if (!passed) {
throw new SfError(`${webDriverIo} is present. Was the clean not aggressive enough?`);
}
}
async ensureNoHerokuCliUtilNyc() {
const herokuCliUtil = path.join(this.baseDir, 'node_modules', '@salesforce', 'plugin-templates', 'node_modules', 'salesforce-alm', 'node_modules', 'heroku-cli-util', '.nyc_output');
const validate = async () => !(await fileExists(herokuCliUtil));
const passed = await this.execute('Ensure heroku-cli-util/.nyc_output does not exist', validate);
if (!passed) {
throw new SfError(`${herokuCliUtil} is present. Was the clean not aggressive enough?`);
}
}
/**
* Ensure that the path lengths in the build tree are as windows safe as possible.
*
* The check fails if the path lengths DO NOT allow for a username longer than the --windows-username-buffer
*
* Warnings will be emitted for any path that does not allow for a username longer than 48 characters
*/
async ensureWindowsPathLengths() {
const validate = async () => {
const maxWindowsPath = 259;
const cli = ensure(this.flags.cli);
const supportedUsernameLength = ensureNumber(this.flags['windows-username-buffer']);
const fakeSupportedUsername = 'u'.repeat(supportedUsernameLength);
const supportedBaseWindowsPath = `C:\\Users\\${fakeSupportedUsername}\\AppData\\Local\\${cli}\\tmp\\${cli}-cli-v1.xxx.yyy-abcdef-windows-x64\\`;
const maxUsernameLength = 64;
const fakeMaxUsername = 'u'.repeat(maxUsernameLength);
const maxBaseWindowsPath = `C:\\Users\\${fakeMaxUsername}\\AppData\\Local\\${cli}\\tmp\\${cli}-cli-v1.xxx.yyy-abcdef-windows-x64\\`;
const supportedWindowsPathLength = maxWindowsPath - supportedBaseWindowsPath.length;
const maxWindowsPathLength = maxWindowsPath - maxBaseWindowsPath.length;
this.log('Windows Path Length Test:');
this.log(` - max windows path length: ${maxWindowsPath}`);
this.log(' ---- Upper Limit ----');
this.log(` - ${cli} max username length: ${maxUsernameLength}`);
this.log(` - ${cli} max base path length: ${maxBaseWindowsPath.length}`);
this.log(` - ${cli} max allowable path length: ${maxWindowsPathLength}`);
this.log(' ---- Supported Limit ----');
this.log(` - ${cli} supported username length: ${supportedUsernameLength}`);
this.log(` - ${cli} supported base path length: ${supportedBaseWindowsPath.length}`);
this.log(` - ${cli} supported allowable path length: ${supportedWindowsPathLength}`);
const paths = (await fg(`${this.baseDir}/node_modules/**/*`)).map((p) => p.replace(`${this.baseDir}${path.sep}`, ''));
const warnPaths = paths
.filter((p) => p.length >= maxWindowsPathLength && p.length < supportedWindowsPathLength)
.sort();
const errorPaths = paths.filter((p) => p.length >= supportedWindowsPathLength).sort();
if (warnPaths.length) {
this.log(`${chalk.yellow.bold('WARNING:')} Some paths could result in errors for Windows users with usernames that are ${maxUsernameLength} characters!`);
warnPaths.forEach((p) => this.log(`${p.length} - ${p}`));
}
if (errorPaths.length) {
this.log(`${chalk.red.bold('ERROR:')} Unacceptably long paths detected in base build!`);
errorPaths.forEach((p) => this.log(`${p.length} - ${p}`));
return false;
}
return true;
};
const passed = await this.execute('Ensure windows path lengths', validate);
if (!passed) {
throw new SfError('Unacceptably long paths detected in base build!');
}
}
async ensureApexNode() {
const apexNodePath = path.join(this.baseDir, 'node_modules', '@salesforce', 'apex-node', 'lib', 'src', 'tests');
const validate = async () => fileExists(apexNodePath);
const passed = await this.execute('Ensure apex-node exists', validate);
if (!passed) {
throw new SfError(`${apexNodePath} is missing!. Was the clean too aggressive?`);
}
}
async ensureTemplatesCommands() {
const templatesPath = path.join(this.baseDir, 'node_modules', '@salesforce', 'plugin-templates');
const validate = async () => fileExists(templatesPath);
const passed = await this.execute('Ensure templates commands exist', validate);
if (!passed) {
throw new SfError(`${templatesPath} is missing!. Was the doc clean too aggressive?`);
}
}
async ensureNoDistTestsOrMaps() {
const validate = async () => {
const files = await fg([`${this.baseDir}/dist/*.test.js`, `${this.baseDir}/dist/*.js.map`]);
if (files.length) {
this.log(chalk.red.bold('Found the following in dist:'));
for (const file of files)
this.log(file);
return false;
}
return true;
};
const passed = await this.execute('Ensure no tests or maps in dist', validate);
if (!passed) {
throw new SfError(`Found .test.js and/or .js.map files in ${path.join(this.baseDir, 'dist')}`);
}
}
async ensureNoUnexpectedFiles() {
const validate = async () => {
const expectedFileGlobs = [
`${this.baseDir}/package.json`,
`${this.baseDir}/LICENSE.txt`,
`${this.baseDir}/README.md`,
`${this.baseDir}/CHANGELOG.md`,
`${this.baseDir}/yarn.lock`,
`${this.baseDir}/npm-shrinkwrap.json`,
`${this.baseDir}/oclif.manifest.json`,
`${this.baseDir}/oclif.lock`,
`${this.baseDir}/bin/*`,
`${this.baseDir}/dist/**/*.js`,
`${this.baseDir}/dist/builtins/package.json`,
`${this.baseDir}/scripts/*`,
`${this.baseDir}/sf/**/*`,
`${this.baseDir}/theme.json`,
];
const expectedFiles = await fg(expectedFileGlobs);
const allFiles = await fg([`${this.baseDir}/**/*`, `!${this.baseDir}/node_modules/**/*`]);
const unexpectedFiles = allFiles.filter((f) => !expectedFiles.includes(f));
if (unexpectedFiles.length) {
this.log(chalk.red.bold('Found unexpected files in base build dir:'));
for (const file of unexpectedFiles)
this.log(file);
return false;
}
return true;
};
const passed = await this.execute('Ensure no unexpected files', validate);
if (!passed) {
throw new SfError('Unexpected file found in base build dir!');
}
}
async ensureMdMessagesExist() {
const validate = async () => {
const fileData = await fs.readFile(path.join(this.baseDir, 'package.json'), 'utf8');
const packageJson = parseJson(fileData, path.join(this.baseDir, 'package.json'), false);
const plugins = get(packageJson, 'oclif.plugins', []);
const globs = plugins.map((p) => `${this.baseDir}/node_modules/${p}/messages/*.md`);
const files = await fg(globs);
return Boolean(files.length);
};
const passed = await this.execute('Ensure .md messages exist', validate);
if (!passed) {
throw new SfError('Found no .md message files. Was the clean too aggressive?');
}
}
async ensureSfIsIncluded() {
const validate = async () => {
const sfBin = path.join(this.baseDir, 'bin', 'sf');
const sfBinExists = await fileExists(sfBin);
const sfCmd = path.join(this.baseDir, 'bin', 'sf.cmd');
const sfCmdExists = await fileExists(sfCmd);
const version = shelljs.exec(`${sfBin} --version`, { silent: false });
const help = shelljs.exec(`${sfBin} --help`, { silent: false });
return sfBinExists && sfCmdExists && version.code === 0 && help.code === 0;
};
const passed = await this.execute('Ensure sf is included\n', validate);
if (!passed) {
throw new SfError('sf was not included! Did include-sf.js succeed?');
}
}
}
//# sourceMappingURL=verify.js.map