505 lines
12 KiB
TypeScript
505 lines
12 KiB
TypeScript
import { ChildProcess, exec, execSync } from 'child_process';
|
|
import {
|
|
readdirSync,
|
|
readFileSync,
|
|
renameSync,
|
|
statSync,
|
|
writeFileSync,
|
|
ensureDirSync,
|
|
createFileSync,
|
|
moveSync,
|
|
copySync,
|
|
removeSync,
|
|
} from 'fs-extra';
|
|
import * as path from 'path';
|
|
import { detectPackageManager } from '@nrwl/tao/src/shared/package-manager';
|
|
import * as isCI from 'is-ci';
|
|
|
|
interface RunCmdOpts {
|
|
silenceError?: boolean;
|
|
env?: Record<string, string>;
|
|
cwd?: string;
|
|
silent?: boolean;
|
|
}
|
|
|
|
export function currentCli() {
|
|
return process.env.SELECTED_CLI ? 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.add} ${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();
|
|
}
|
|
|
|
/**
|
|
* Sets up a new project in the temporary project path
|
|
* for the currently selected CLI.
|
|
*/
|
|
export function newProject(): string {
|
|
const packageManager = process.env.SELECTED_PM as any;
|
|
|
|
try {
|
|
const newProjName = uniq('proj');
|
|
const useBackupProject = packageManager !== 'pnpm';
|
|
const projScope = useBackupProject ? 'proj' : newProjName;
|
|
|
|
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/nest`,
|
|
`@nrwl/next`,
|
|
`@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 = newProjName;
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 targets = workspace.projects[appName].targets
|
|
? workspace.projects[appName].targets
|
|
: workspace.projects[appName].architect;
|
|
const build = targets.build;
|
|
|
|
if (!build) {
|
|
return;
|
|
}
|
|
|
|
const executor = build.builder ? 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) => void)
|
|
): 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;
|
|
add: 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} --`,
|
|
add: `npm install`,
|
|
},
|
|
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`,
|
|
add: `yarn add`,
|
|
},
|
|
pnpm: {
|
|
createWorkspace: `pnpx create-nx-workspace@${process.env.PUBLISHED_VERSION}`,
|
|
runNx: `pnpm run nx --`,
|
|
runNxSilent: `pnpm run nx --silent --`,
|
|
add: `pnpm add`,
|
|
},
|
|
}[packageManager];
|
|
}
|