nx/e2e/utils/index.ts
Vivek More 🧐 212fb00548 cleanup(misc): use more es6 features
- Single char alternation (e.g. a|b|c|d) in a RegExp can be simplified to use a character class ([abcd]) instead.
  This usually also provides slightly better matching performance.
- Character escapes that are replaceable with the unescaped character without a change in meaning. Inside the square brackets of a character class, many escapes are unnecessary that would be necessary outside of a character class. For example the regex [\.] is identical to [.]
- If several qualified expressions occur after the qualifier having been checked for nullable, they can be replaced with optional chaining
2021-03-24 20:11:32 -04:00

528 lines
13 KiB
TypeScript

import { detectPackageManager } from '@nrwl/tao/src/shared/package-manager';
import { ChildProcess, exec, execSync } from 'child_process';
import {
copySync,
createFileSync,
ensureDirSync,
moveSync,
readdirSync,
readFileSync,
removeSync,
renameSync,
statSync,
writeFileSync,
} from 'fs-extra';
import * as isCI from 'is-ci';
import * as path from 'path';
interface RunCmdOpts {
silenceError?: boolean;
env?: Record<string, string>;
cwd?: string;
silent?: boolean;
}
export function currentCli() {
return process.env.SELECTED_CLI ?? 'nx';
}
let projName: string;
export function uniq(prefix: string) {
return `${prefix}${Math.floor(Math.random() * 10000000)}`;
}
export function workspaceConfigName() {
return currentCli() === 'angular' ? 'angular.json' : 'workspace.json';
}
export function runCreateWorkspace(
name: string,
{
preset,
appName,
style,
base,
packageManager,
cli,
extraArgs,
}: {
preset: string;
appName?: string;
style?: string;
base?: string;
packageManager?: 'npm' | 'yarn' | 'pnpm';
cli?: string;
extraArgs?: string;
}
) {
projName = name;
const pm = getPackageManagerCommand({ packageManager });
const linterArg =
preset === 'angular' || preset === 'angular-nest' ? ' --linter=tslint' : '';
let command = `${pm.createWorkspace} ${name} --cli=${
cli || currentCli()
} --preset=${preset} ${linterArg} --no-nxCloud --no-interactive`;
if (appName) {
command += ` --appName=${appName}`;
}
if (style) {
command += ` --style=${style}`;
}
if (base) {
command += ` --defaultBase="${base}"`;
}
if (packageManager) {
command += ` --package-manager=${packageManager}`;
}
if (extraArgs) {
command += ` ${extraArgs}`;
}
const create = execSync(command, {
cwd: `./tmp/${currentCli()}`,
stdio: [0, 1, 2],
env: process.env,
});
return create ? create.toString() : '';
}
export function packageInstall(pkg: string, projName?: string) {
const cwd = projName ? `./tmp/${currentCli()}/${projName}` : tmpProjPath();
const pm = getPackageManagerCommand({ path: cwd });
const install = execSync(`${pm.addDev} ${pkg}`, {
cwd,
// ...{ stdio: ['pipe', 'pipe', 'pipe'] },
...{ stdio: [0, 1, 2] },
env: process.env,
});
return install ? install.toString() : '';
}
export function runNgNew(): string {
return execSync(`../../node_modules/.bin/ng new proj --no-interactive`, {
cwd: `./tmp/${currentCli()}`,
env: process.env,
}).toString();
}
export function getSelectedPackageManager(): 'npm' | 'yarn' | 'pnpm' {
return process.env.SELECTED_PM as 'npm' | 'yarn' | 'pnpm';
}
/**
* Sets up a new project in the temporary project path
* for the currently selected CLI.
*/
export function newProject({ name = uniq('proj') } = {}): string {
const packageManager = getSelectedPackageManager();
try {
const useBackupProject = packageManager !== 'pnpm';
const projScope = useBackupProject ? 'proj' : name;
if (!useBackupProject || !directoryExists(tmpBackupProjPath())) {
runCreateWorkspace(projScope, { preset: 'empty', packageManager });
// Temporary hack to prevent installing with `--frozen-lockfile`
if (isCI && packageManager === 'pnpm') {
updateFile('.npmrc', 'prefer-frozen-lockfile=false');
}
const packages = [
`@nrwl/angular`,
`@nrwl/express`,
`@nrwl/jest`,
`@nrwl/linter`,
`@nrwl/nest`,
`@nrwl/next`,
`@nrwl/gatsby`,
`@nrwl/node`,
`@nrwl/react`,
`@nrwl/storybook`,
`@nrwl/nx-plugin`,
`@nrwl/eslint-plugin-nx`,
`@nrwl/web`,
];
packageInstall(packages.join(` `), projScope);
if (useBackupProject) {
moveSync(`./tmp/${currentCli()}/proj`, `${tmpBackupProjPath()}`);
}
}
projName = name;
if (useBackupProject) {
copySync(`${tmpBackupProjPath()}`, `${tmpProjPath()}`);
}
return projScope;
} catch (e) {
console.log(`Failed to set up project for e2e tests.`);
console.log(e.message);
throw e;
}
}
// Useful in order to cleanup space during CI to prevent `No space left on device` exceptions
export function removeProject({ onlyOnCI = false } = {}) {
if (onlyOnCI && !isCI) {
return;
}
removeSync(tmpProjPath());
}
export function supportUi() {
return false;
// return !process.env.NO_CHROME;
}
export function runCommandAsync(
command: string,
opts: RunCmdOpts = {
silenceError: false,
env: process.env,
}
): Promise<{ stdout: string; stderr: string; combinedOutput: string }> {
return new Promise((resolve, reject) => {
exec(
command,
{
cwd: tmpProjPath(),
env: { ...process.env, FORCE_COLOR: 'false' },
},
(err, stdout, stderr) => {
if (!opts.silenceError && err) {
reject(err);
}
resolve({ stdout, stderr, combinedOutput: `${stdout}${stderr}` });
}
);
});
}
export function runCommandUntil(
command: string,
criteria: (output: string) => boolean,
{ kill = true } = {}
): Promise<{ process: ChildProcess }> {
const pm = getPackageManagerCommand();
const p = exec(`${pm.runNx} ${command}`, {
cwd: tmpProjPath(),
env: { ...process.env, FORCE_COLOR: 'false' },
});
return new Promise((res, rej) => {
let output = '';
let complete = false;
function checkCriteria(c) {
output += c.toString();
if (criteria(output)) {
complete = true;
res({ process: p });
if (kill) {
p.kill();
}
}
}
p.stdout.on('data', checkCriteria);
p.stderr.on('data', checkCriteria);
p.on('exit', (code) => {
if (code !== 0 && !complete) {
console.log(output);
}
rej(`Exited with ${code}`);
});
});
}
export function runCLIAsync(
command: string,
opts: RunCmdOpts = {
silenceError: false,
env: process.env,
silent: false,
}
): Promise<{ stdout: string; stderr: string; combinedOutput: string }> {
const pm = getPackageManagerCommand();
return runCommandAsync(
`${opts.silent ? pm.runNxSilent : pm.runNx} ${command}`,
opts
);
}
export function runNgAdd(
command?: string,
opts: RunCmdOpts = {
silenceError: false,
env: process.env,
cwd: tmpProjPath(),
}
): string {
try {
packageInstall('@nrwl/workspace');
return execSync(
`./node_modules/.bin/ng g @nrwl/workspace:ng-add ${command}`,
{
cwd: tmpProjPath(),
env: opts.env as any,
}
)
.toString()
.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
);
} catch (e) {
if (opts.silenceError) {
return e.stdout.toString();
} else {
console.log(e.stdout.toString(), e.stderr.toString());
throw e;
}
}
}
export function runCLI(
command?: string,
opts: RunCmdOpts = {
silenceError: false,
env: process.env,
}
): string {
try {
const pm = getPackageManagerCommand();
let r = execSync(`${pm.runNx} ${command}`, {
cwd: opts.cwd || tmpProjPath(),
env: opts.env,
}).toString();
r = r.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
);
if (process.env.VERBOSE_OUTPUT) {
console.log(r);
}
const needsMaxWorkers = /g.*(express|nest|node|web|react):app.*/;
if (needsMaxWorkers.test(command)) {
setMaxWorkers();
}
return r;
} catch (e) {
if (opts.silenceError) {
return e.stdout.toString();
} else {
console.log('original command', command);
console.log(e.stdout?.toString(), e.stderr?.toString());
throw e;
}
}
}
export function expectTestsPass(v: { stdout: string; stderr: string }) {
expect(v.stderr).toContain('Ran all test suites');
expect(v.stderr).not.toContain('fail');
}
export function runCommand(command: string): string {
try {
const r = execSync(command, {
cwd: tmpProjPath(),
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, FORCE_COLOR: 'false' },
}).toString();
if (process.env.VERBOSE_OUTPUT) {
console.log(r);
}
return r;
} catch (e) {
return e.stdout.toString() + e.stderr.toString();
}
}
/**
* Sets maxWorkers in CI on all projects that require it
* so that it doesn't try to run it with 34 workers
*
* maxWorkers required for: node, web, jest
*/
function setMaxWorkers() {
if (isCI) {
const workspaceFile = workspaceConfigName();
const workspace = readJson(workspaceFile);
Object.keys(workspace.projects).forEach((appName) => {
const project = workspace.projects[appName];
const { build } = project.targets ?? project.architect;
if (!build) {
return;
}
const executor = build.builder ?? build.executor;
if (
executor.startsWith('@nrwl/node') ||
executor.startsWith('@nrwl/web') ||
executor.startsWith('@nrwl/jest')
) {
build.options.maxWorkers = 4;
}
});
updateFile(workspaceFile, JSON.stringify(workspace));
}
}
export function createFile(f: string, content: string = ''): void {
const path = tmpProjPath(f);
createFileSync(path);
if (content) {
updateFile(path, content);
}
}
export function updateFile(
f: string,
content: string | ((content: string) => string)
): void {
ensureDirSync(path.dirname(tmpProjPath(f)));
if (typeof content === 'string') {
writeFileSync(tmpProjPath(f), content);
} else {
writeFileSync(
tmpProjPath(f),
content(readFileSync(tmpProjPath(f)).toString())
);
}
}
export function renameFile(f: string, newPath: string): void {
ensureDirSync(path.dirname(tmpProjPath(newPath)));
renameSync(tmpProjPath(f), tmpProjPath(newPath));
}
export function checkFilesExist(...expectedFiles: string[]) {
expectedFiles.forEach((f) => {
const ff = f.startsWith('/') ? f : tmpProjPath(f);
if (!exists(ff)) {
throw new Error(`File '${ff}' does not exist`);
}
});
}
export function checkFilesDoNotExist(...expectedFiles: string[]) {
expectedFiles.forEach((f) => {
const ff = f.startsWith('/') ? f : tmpProjPath(f);
if (exists(ff)) {
throw new Error(`File '${ff}' does not exist`);
}
});
}
export function listFiles(dirName: string) {
return readdirSync(tmpProjPath(dirName));
}
export function readJson(f: string): any {
return JSON.parse(readFile(f));
}
export function readFile(f: string) {
const ff = f.startsWith('/') ? f : tmpProjPath(f);
return readFileSync(ff).toString();
}
export function rmDist() {
removeSync(`${tmpProjPath()}/dist`);
}
export function directoryExists(filePath: string): boolean {
try {
return statSync(filePath).isDirectory();
} catch (err) {
return false;
}
}
export function fileExists(filePath: string): boolean {
try {
return statSync(filePath).isFile();
} catch (err) {
return false;
}
}
export function exists(filePath: string): boolean {
return directoryExists(filePath) || fileExists(filePath);
}
export function getSize(filePath: string): number {
return statSync(filePath).size;
}
export function tmpProjPath(path?: string) {
return path
? `./tmp/${currentCli()}/${projName}/${path}`
: `./tmp/${currentCli()}/${projName}`;
}
function tmpBackupProjPath(path?: string) {
return path
? `./tmp/${currentCli()}/proj-backup/${path}`
: `./tmp/${currentCli()}/proj-backup`;
}
export function getPackageManagerCommand({
path = tmpProjPath(),
packageManager = detectPackageManager(path),
scriptsPrependNodePath = true,
} = {}): {
createWorkspace: string;
runNx: string;
runNxSilent: string;
addDev: string;
list: string;
} {
const scriptsPrependNodePathFlag = scriptsPrependNodePath
? ' --scripts-prepend-node-path '
: '';
return {
npm: {
createWorkspace: `npx create-nx-workspace@${process.env.PUBLISHED_VERSION}`,
runNx: `npm run nx${scriptsPrependNodePathFlag} --`,
runNxSilent: `npm run nx --silent${scriptsPrependNodePathFlag} --`,
addDev: `npm install -D`,
list: 'npm ls --depth 10',
},
yarn: {
// `yarn create nx-workspace` is failing due to wrong global path
createWorkspace: `yarn global add create-nx-workspace@${process.env.PUBLISHED_VERSION} && create-nx-workspace`,
runNx: `yarn nx`,
runNxSilent: `yarn --silent nx`,
addDev: `yarn add -D`,
list: 'npm ls --depth 10',
},
pnpm: {
createWorkspace: `pnpx create-nx-workspace@${process.env.PUBLISHED_VERSION}`,
runNx: `pnpm run nx --`,
runNxSilent: `pnpm run nx --silent --`,
addDev: `pnpm add -D`,
list: 'npm ls --depth 10',
},
}[packageManager];
}
export function expectNoAngularDevkit() {
const { list } = getPackageManagerCommand();
const result = runCommand(`${list} @angular-devkit/core`);
expect(result).not.toContain('@angular-devkit/core');
}