565 lines
14 KiB
TypeScript
565 lines
14 KiB
TypeScript
import { copySync, ensureDirSync, moveSync, removeSync } from 'fs-extra';
|
|
import {
|
|
createFile,
|
|
directoryExists,
|
|
tmpBackupNgCliProjPath,
|
|
tmpBackupProjPath,
|
|
updateFile,
|
|
updateJson,
|
|
} from './file-utils';
|
|
import {
|
|
e2eCwd,
|
|
getLatestLernaVersion,
|
|
getPublishedVersion,
|
|
getSelectedPackageManager,
|
|
isVerbose,
|
|
isVerboseE2ERun,
|
|
} from './get-env-info';
|
|
import * as isCI from 'is-ci';
|
|
|
|
import { angularCliVersion as defaultAngularCliVersion } from '@nx/workspace/src/utils/versions';
|
|
import { dump } from '@zkochan/js-yaml';
|
|
import { execSync, ExecSyncOptions } from 'child_process';
|
|
|
|
import { logError, logInfo } from './log-utils';
|
|
import {
|
|
getPackageManagerCommand,
|
|
runCLI,
|
|
RunCmdOpts,
|
|
runCommand,
|
|
} from './command-utils';
|
|
import { NxJsonConfiguration, output } from '@nx/devkit';
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
let projName: string;
|
|
|
|
/**
|
|
* Sets up a new project in the temporary project path
|
|
* for the currently selected CLI.
|
|
*/
|
|
export function newProject({
|
|
name = uniq('proj'),
|
|
packageManager = getSelectedPackageManager(),
|
|
unsetProjectNameAndRootFormat = true,
|
|
} = {}): string {
|
|
try {
|
|
const projScope = 'proj';
|
|
|
|
if (!directoryExists(tmpBackupProjPath())) {
|
|
runCreateWorkspace(projScope, {
|
|
preset: 'apps',
|
|
packageManager,
|
|
});
|
|
|
|
if (unsetProjectNameAndRootFormat) {
|
|
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
|
|
delete nxJson.workspaceLayout;
|
|
return nxJson;
|
|
});
|
|
}
|
|
|
|
// Temporary hack to prevent installing with `--frozen-lockfile`
|
|
if (isCI && packageManager === 'pnpm') {
|
|
updateFile(
|
|
'.npmrc',
|
|
'prefer-frozen-lockfile=false\nstrict-peer-dependencies=false\nauto-install-peers=true'
|
|
);
|
|
}
|
|
|
|
// TODO(jack): we should tag the projects (e.g. tags: ['package']) and filter from that rather than hard-code packages.
|
|
const packages = [
|
|
`@nx/angular`,
|
|
`@nx/eslint-plugin`,
|
|
`@nx/express`,
|
|
`@nx/esbuild`,
|
|
`@nx/jest`,
|
|
`@nx/js`,
|
|
`@nx/linter`,
|
|
`@nx/nest`,
|
|
`@nx/next`,
|
|
`@nx/node`,
|
|
`@nx/plugin`,
|
|
`@nx/playwright`,
|
|
`@nx/rollup`,
|
|
`@nx/react`,
|
|
`@nx/storybook`,
|
|
`@nx/vite`,
|
|
`@nx/web`,
|
|
`@nx/webpack`,
|
|
`@nx/react-native`,
|
|
`@nx/expo`,
|
|
];
|
|
packageInstall(packages.join(` `), projScope);
|
|
|
|
// stop the daemon
|
|
execSync(`${getPackageManagerCommand().runNx} reset`, {
|
|
cwd: `${e2eCwd}/proj`,
|
|
stdio: isVerbose() ? 'inherit' : 'pipe',
|
|
});
|
|
|
|
moveSync(`${e2eCwd}/proj`, `${tmpBackupProjPath()}`);
|
|
}
|
|
projName = name;
|
|
copySync(`${tmpBackupProjPath()}`, `${tmpProjPath()}`);
|
|
if (isVerbose()) {
|
|
logInfo(`NX`, `E2E created a project: ${tmpProjPath()}`);
|
|
}
|
|
|
|
if (packageManager === 'pnpm') {
|
|
execSync(getPackageManagerCommand().install, {
|
|
cwd: tmpProjPath(),
|
|
stdio: 'pipe',
|
|
env: { CI: 'true', ...process.env },
|
|
encoding: 'utf-8',
|
|
});
|
|
}
|
|
return projScope;
|
|
} catch (e) {
|
|
logError(`Failed to set up project for e2e tests.`, e.message);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// pnpm v7 sadly doesn't automatically install peer dependencies
|
|
export function addPnpmRc() {
|
|
updateFile(
|
|
'.npmrc',
|
|
'strict-peer-dependencies=false\nauto-install-peers=true'
|
|
);
|
|
}
|
|
|
|
export function runCreateWorkspace(
|
|
name: string,
|
|
{
|
|
preset,
|
|
appName,
|
|
style,
|
|
base,
|
|
packageManager,
|
|
extraArgs,
|
|
ci,
|
|
useDetectedPm = false,
|
|
cwd = e2eCwd,
|
|
bundler,
|
|
routing,
|
|
standaloneApi,
|
|
docker,
|
|
nextAppDir,
|
|
e2eTestRunner,
|
|
}: {
|
|
preset: string;
|
|
appName?: string;
|
|
style?: string;
|
|
base?: string;
|
|
packageManager?: 'npm' | 'yarn' | 'pnpm';
|
|
extraArgs?: string;
|
|
ci?: 'azure' | 'github' | 'circleci';
|
|
useDetectedPm?: boolean;
|
|
cwd?: string;
|
|
bundler?: 'webpack' | 'vite';
|
|
standaloneApi?: boolean;
|
|
routing?: boolean;
|
|
docker?: boolean;
|
|
nextAppDir?: boolean;
|
|
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';
|
|
}
|
|
) {
|
|
projName = name;
|
|
|
|
const pm = getPackageManagerCommand({ packageManager });
|
|
|
|
let command = `${pm.createWorkspace} ${name} --preset=${preset} --no-nxCloud --no-interactive`;
|
|
if (appName) {
|
|
command += ` --appName=${appName}`;
|
|
}
|
|
if (style) {
|
|
command += ` --style=${style}`;
|
|
}
|
|
if (ci) {
|
|
command += ` --ci=${ci}`;
|
|
}
|
|
|
|
if (bundler) {
|
|
command += ` --bundler=${bundler}`;
|
|
}
|
|
|
|
if (nextAppDir !== undefined) {
|
|
command += ` --nextAppDir=${nextAppDir}`;
|
|
}
|
|
|
|
if (docker !== undefined) {
|
|
command += ` --docker=${docker}`;
|
|
}
|
|
|
|
if (standaloneApi !== undefined) {
|
|
command += ` --standaloneApi=${standaloneApi}`;
|
|
}
|
|
|
|
if (routing !== undefined) {
|
|
command += ` --routing=${routing}`;
|
|
}
|
|
|
|
if (base) {
|
|
command += ` --defaultBase="${base}"`;
|
|
}
|
|
|
|
if (packageManager && !useDetectedPm) {
|
|
command += ` --package-manager=${packageManager}`;
|
|
}
|
|
|
|
if (e2eTestRunner) {
|
|
command += ` --e2eTestRunner=${e2eTestRunner}`;
|
|
}
|
|
|
|
if (extraArgs) {
|
|
command += ` ${extraArgs}`;
|
|
}
|
|
|
|
if (isCI) {
|
|
command += ` --verbose`;
|
|
}
|
|
|
|
try {
|
|
const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, {
|
|
cwd,
|
|
stdio: 'pipe',
|
|
env: {
|
|
CI: 'true',
|
|
NX_VERBOSE_LOGGING: isCI ? 'true' : 'false',
|
|
...process.env,
|
|
},
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
if (isVerbose()) {
|
|
output.log({
|
|
title: `Command: ${command}`,
|
|
bodyLines: [create as string],
|
|
color: 'green',
|
|
});
|
|
}
|
|
|
|
return create;
|
|
} catch (e) {
|
|
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function runCreatePlugin(
|
|
name: string,
|
|
{
|
|
packageManager,
|
|
extraArgs,
|
|
useDetectedPm = false,
|
|
}: {
|
|
packageManager?: 'npm' | 'yarn' | 'pnpm';
|
|
extraArgs?: string;
|
|
useDetectedPm?: boolean;
|
|
}
|
|
) {
|
|
projName = name;
|
|
|
|
const pm = getPackageManagerCommand({ packageManager });
|
|
|
|
let command = `${
|
|
pm.runUninstalledPackage
|
|
} create-nx-plugin@${getPublishedVersion()} ${name} --no-nxCloud`;
|
|
|
|
if (packageManager && !useDetectedPm) {
|
|
command += ` --package-manager=${packageManager}`;
|
|
}
|
|
|
|
if (extraArgs) {
|
|
command += ` ${extraArgs}`;
|
|
}
|
|
|
|
try {
|
|
const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, {
|
|
cwd: e2eCwd,
|
|
stdio: 'pipe',
|
|
env: process.env,
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
if (isVerbose()) {
|
|
output.log({
|
|
title: `Command: ${command}`,
|
|
bodyLines: [create as string],
|
|
color: 'green',
|
|
});
|
|
}
|
|
|
|
return create;
|
|
} catch (e) {
|
|
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function packageInstall(
|
|
pkg: string,
|
|
projName?: string,
|
|
version = getPublishedVersion(),
|
|
mode: 'dev' | 'prod' = 'dev'
|
|
) {
|
|
const cwd = projName ? `${e2eCwd}/${projName}` : tmpProjPath();
|
|
const pm = getPackageManagerCommand({ path: cwd });
|
|
const pkgsWithVersions = pkg
|
|
.split(' ')
|
|
.map((pgk) => `${pgk}@${version}`)
|
|
.join(' ');
|
|
|
|
const command = `${
|
|
mode === 'dev' ? pm.addDev : pm.addProd
|
|
} ${pkgsWithVersions}`;
|
|
|
|
try {
|
|
const install = execSync(command, {
|
|
cwd,
|
|
stdio: isVerbose() ? 'inherit' : 'ignore',
|
|
env: process.env,
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
if (isVerbose()) {
|
|
output.log({
|
|
title: `Command: ${command}`,
|
|
bodyLines: [install as string],
|
|
color: 'green',
|
|
});
|
|
}
|
|
|
|
return install;
|
|
} catch (e) {
|
|
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new Angular CLI workspace or restores a cached one if exists.
|
|
* @returns the workspace name
|
|
*/
|
|
export function runNgNew(
|
|
packageManager = getSelectedPackageManager(),
|
|
angularCliVersion = defaultAngularCliVersion
|
|
): string {
|
|
const pmc = getPackageManagerCommand({ packageManager });
|
|
|
|
if (directoryExists(tmpBackupNgCliProjPath())) {
|
|
const angularJson = JSON.parse(
|
|
readFileSync(join(tmpBackupNgCliProjPath(), 'angular.json'), 'utf-8')
|
|
);
|
|
// the name of the workspace matches the name of the generated default app,
|
|
// we need to reuse the same name that's cached in order to avoid issues
|
|
// with tests relying on a different name
|
|
projName = Object.keys(angularJson.projects)[0];
|
|
copySync(tmpBackupNgCliProjPath(), tmpProjPath());
|
|
|
|
if (isVerboseE2ERun()) {
|
|
logInfo(
|
|
`NX`,
|
|
`E2E restored an Angular CLI project from cache: ${tmpProjPath()}`
|
|
);
|
|
}
|
|
|
|
return projName;
|
|
}
|
|
|
|
projName = uniq('ng-proj');
|
|
const command = `${pmc.runUninstalledPackage} @angular/cli@${angularCliVersion} new ${projName} --package-manager=${packageManager}`;
|
|
execSync(command, {
|
|
cwd: e2eCwd,
|
|
stdio: isVerbose() ? 'inherit' : 'pipe',
|
|
env: process.env,
|
|
encoding: 'utf-8',
|
|
});
|
|
copySync(tmpProjPath(), tmpBackupNgCliProjPath());
|
|
|
|
if (isVerboseE2ERun()) {
|
|
logInfo(`NX`, `E2E created an Angular CLI project: ${tmpProjPath()}`);
|
|
}
|
|
|
|
return projName;
|
|
}
|
|
|
|
export function newLernaWorkspace({
|
|
name = uniq('lerna-proj'),
|
|
packageManager = getSelectedPackageManager(),
|
|
} = {}): string {
|
|
try {
|
|
const projScope = name;
|
|
projName = name;
|
|
|
|
const pm = getPackageManagerCommand({ packageManager });
|
|
|
|
createNonNxProjectDirectory(projScope, packageManager !== 'pnpm');
|
|
|
|
if (packageManager === 'pnpm') {
|
|
updateFile(
|
|
'pnpm-workspace.yaml',
|
|
dump({
|
|
packages: ['packages/*'],
|
|
})
|
|
);
|
|
updateFile(
|
|
'.npmrc',
|
|
'prefer-frozen-lockfile=false\nstrict-peer-dependencies=false\nauto-install-peers=true'
|
|
);
|
|
}
|
|
|
|
if (isVerbose()) {
|
|
logInfo(`NX`, `E2E test has created a lerna workspace: ${tmpProjPath()}`);
|
|
}
|
|
|
|
// We need to force the real latest version of lerna to depend on our locally published version of nx
|
|
updateJson(`package.json`, (json) => {
|
|
// yarn workspaces can only be enabled in private projects
|
|
json.private = true;
|
|
|
|
const nxVersion = getPublishedVersion();
|
|
const overrides = {
|
|
...json.overrides,
|
|
nx: nxVersion,
|
|
'@nrwl/devkit': nxVersion,
|
|
'@nx/devkit': nxVersion,
|
|
};
|
|
if (packageManager === 'pnpm') {
|
|
json.pnpm = {
|
|
...json.pnpm,
|
|
overrides: {
|
|
...json.pnpm?.overrides,
|
|
...overrides,
|
|
},
|
|
};
|
|
} else if (packageManager === 'yarn') {
|
|
json.resolutions = {
|
|
...json.resolutions,
|
|
...overrides,
|
|
};
|
|
} else {
|
|
json.overrides = overrides;
|
|
}
|
|
return json;
|
|
});
|
|
|
|
/**
|
|
* Again, in order to ensure we override the required version relationships, we first install lerna as a devDep
|
|
* before running `lerna init`.
|
|
*/
|
|
execSync(
|
|
`${pm.addDev} lerna@${getLatestLernaVersion()}${
|
|
packageManager === 'pnpm'
|
|
? ' --workspace-root'
|
|
: packageManager === 'yarn'
|
|
? ' -W'
|
|
: ''
|
|
}`,
|
|
{
|
|
cwd: tmpProjPath(),
|
|
stdio: isVerbose() ? 'inherit' : 'pipe',
|
|
env: { CI: 'true', ...process.env },
|
|
encoding: 'utf-8',
|
|
}
|
|
);
|
|
|
|
execSync(`${pm.runLerna} init`, {
|
|
cwd: tmpProjPath(),
|
|
stdio: isVerbose() ? 'inherit' : 'pipe',
|
|
env: { CI: 'true', ...process.env },
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
if (packageManager === 'pnpm') {
|
|
// pnpm doesn't use the normal package.json packages field so lerna needs to be told that pnpm is the client.
|
|
updateJson('lerna.json', (json) => {
|
|
json.npmClient = 'pnpm';
|
|
return json;
|
|
});
|
|
}
|
|
|
|
execSync(pm.install, {
|
|
cwd: tmpProjPath(),
|
|
stdio: isVerbose() ? 'inherit' : 'pipe',
|
|
env: { CI: 'true', ...process.env },
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
// Format files to ensure no changes are made during lerna repair
|
|
execSync(`${pm.runUninstalledPackage} prettier . --write`, {
|
|
cwd: tmpProjPath(),
|
|
stdio: 'ignore',
|
|
});
|
|
|
|
return projScope;
|
|
} catch (e) {
|
|
logError(`Failed to set up lerna workspace for e2e tests.`, e.message);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function newWrappedNxWorkspace({
|
|
name = uniq('wrapped'),
|
|
pmc = getPackageManagerCommand(),
|
|
} = {}): (command: string, opts?: Partial<ExecSyncOptions>) => string {
|
|
projName = name;
|
|
ensureDirSync(tmpProjPath());
|
|
runCommand(
|
|
`${pmc.runUninstalledPackage} nx@latest init --use-dot-nx-installation`
|
|
);
|
|
return (command: string, opts: Partial<ExecSyncOptions> | undefined) => {
|
|
if (process.platform === 'win32') {
|
|
return runCommand(`./nx.bat ${command}`, { ...opts, failOnError: true });
|
|
} else {
|
|
return runCommand(`./nx ${command}`, { ...opts, failOnError: true });
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getProjectName(): string {
|
|
return projName;
|
|
}
|
|
|
|
export function tmpProjPath(path?: string) {
|
|
return path ? `${e2eCwd}/${projName}/${path}` : `${e2eCwd}/${projName}`;
|
|
}
|
|
|
|
export function createNonNxProjectDirectory(
|
|
name = uniq('proj'),
|
|
addWorkspaces = true
|
|
) {
|
|
projName = name;
|
|
ensureDirSync(tmpProjPath());
|
|
createFile(
|
|
'package.json',
|
|
JSON.stringify({
|
|
name,
|
|
workspaces: addWorkspaces ? ['packages/*'] : undefined,
|
|
})
|
|
);
|
|
}
|
|
|
|
export function uniq(prefix: string) {
|
|
return `${prefix}${Math.floor(Math.random() * 10000000)}`;
|
|
}
|
|
|
|
// Useful in order to cleanup space during CI to prevent `No space left on device` exceptions
|
|
export function cleanupProject({
|
|
skipReset,
|
|
...opts
|
|
}: RunCmdOpts & { skipReset?: boolean } = {}) {
|
|
if (isCI) {
|
|
// Stopping the daemon is not required for tests to pass, but it cleans up background processes
|
|
try {
|
|
if (!skipReset) {
|
|
runCLI('reset', opts);
|
|
}
|
|
} catch {} // ignore crashed daemon
|
|
try {
|
|
removeSync(tmpProjPath());
|
|
} catch {}
|
|
}
|
|
}
|