chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+597
View File
@@ -0,0 +1,597 @@
import {
output,
PackageManager,
ProjectConfiguration,
TargetConfiguration,
} from '@nx/devkit';
import { ChildProcess, exec, execSync, ExecSyncOptions } from 'child_process';
import { existsSync } from 'fs-extra';
import * as isCI from 'is-ci';
import { join } from 'node:path';
import { stripVTControlCharacters } from 'node:util';
import { gte } from 'semver';
import { packageInstall, tmpProjPath } from './create-project-utils';
import {
ensureCypressInstallation,
ensurePlaywrightBrowsersInstallation,
} from './ensure-browser-installation';
import { fileExists, readJson, updateJson } from './file-utils';
import {
detectPackageManager,
getNpmMajorVersion,
getPnpmVersion,
getPublishedVersion,
getStrippedEnvironmentVariables,
getYarnMajorVersion,
isVerboseE2ERun,
} from './get-env-info';
import { logError, logInfo } from './log-utils';
export interface RunCmdOpts {
silenceError?: boolean;
env?: Record<string, string | undefined>;
cwd?: string;
silent?: boolean;
verbose?: boolean;
redirectStderr?: boolean;
timeout?: number;
/** Override daemon mode for this call. Defaults to `true`; set `false` to exercise the non-daemon path. */
daemon?: boolean;
}
/**
* Caps maxWorkers in CI for node/web/jest builds so they don't spawn ~34 workers.
*/
export function setMaxWorkers(projectJsonPath: string) {
if (isCI) {
updateJson<ProjectConfiguration>(projectJsonPath, (project) => {
const { build } = project.targets as {
[targetName: string]: TargetConfiguration<any>;
};
if (!build) {
return project;
}
const executor = build.executor as string;
if (
executor.startsWith('@nx/node') ||
executor.startsWith('@nx/web') ||
executor.startsWith('@nx/jest')
) {
build.options.maxWorkers = 4;
}
return project;
});
}
}
export function runCommand(
command: string,
options?: Partial<ExecSyncOptions> & { failOnError?: boolean }
): string {
const {
failOnError,
env: optionsEnv,
...childProcessOptions
} = options ?? {};
try {
const r = execSync(command, {
cwd: tmpProjPath(),
stdio: 'pipe',
env: {
// Use new versioning by default in e2e tests
NX_INTERNAL_USE_LEGACY_VERSIONING: 'false',
...getStrippedEnvironmentVariables(),
...optionsEnv,
FORCE_COLOR: 'false',
},
encoding: 'utf-8',
...childProcessOptions,
});
if (isVerboseE2ERun()) {
output.log({
title: `Command: ${command}`,
bodyLines: [r as string],
color: 'green',
});
}
return stripVTControlCharacters(r as string);
} catch (e) {
// Intentional: some commands (e.g. `npm ls`) exit non-zero but still produce useful output.
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
if (!failOnError && (e.stdout || e.stderr)) {
return stripVTControlCharacters(e.stdout + e.stderr);
}
throw e;
}
}
export function getPackageManagerCommand({
path = tmpProjPath(),
packageManager = detectPackageManager(path),
} = {}): {
createWorkspace: string;
run: (script: string, args: string) => string;
runNx: string;
runNxSilent: string;
runUninstalledPackage: string;
install: string;
ciInstall: string;
addProd: string;
addDev: string;
list: string;
runLerna: string;
exec: string;
} {
const npmMajorVersion = getNpmMajorVersion();
const yarnMajorVersion = getYarnMajorVersion(path);
const pnpmVersion = getPnpmVersion();
const publishedVersion = getPublishedVersion();
const isYarnWorkspace = fileExists(join(path, 'package.json'))
? readJson('package.json').workspaces
: false;
const isPnpmWorkspace = existsSync(join(path, 'pnpm-workspace.yaml'));
return {
npm: {
createWorkspace: `npx ${
npmMajorVersion && +npmMajorVersion >= 7 ? '--yes' : ''
} create-nx-workspace@${publishedVersion}`,
run: (script: string, args: string) => `npm run ${script} -- ${args}`,
runNx: `npx nx`,
runNxSilent: `npx nx`,
runUninstalledPackage: `npx --yes`,
install: 'npm install',
ciInstall: 'npm ci',
addProd: `npm install`,
addDev: `npm install -D`,
list: 'npm ls --depth 10',
runLerna: `npx lerna`,
exec: 'npx',
},
yarn: {
createWorkspace: `npx ${
npmMajorVersion && +npmMajorVersion >= 7 ? '--yes' : ''
} create-nx-workspace@${publishedVersion}`,
run: (script: string, args: string) => `yarn ${script} ${args}`,
runNx: `yarn nx`,
runNxSilent:
yarnMajorVersion && +yarnMajorVersion >= 2
? 'yarn nx'
: `yarn --silent nx`,
runUninstalledPackage: 'npx --yes',
install: 'yarn',
ciInstall: 'yarn --frozen-lockfile',
addProd: isYarnWorkspace ? 'yarn add -W' : 'yarn add',
addDev: isYarnWorkspace ? 'yarn add -DW' : 'yarn add -D',
list: 'yarn list --pattern',
runLerna:
yarnMajorVersion && +yarnMajorVersion >= 2
? 'yarn lerna'
: `yarn --silent lerna`,
exec: 'yarn',
},
pnpm: {
createWorkspace: `pnpm dlx create-nx-workspace@${publishedVersion}`,
run: (script: string, args: string) => `pnpm run ${script} -- ${args}`,
runNx: `pnpm exec nx`,
runNxSilent: `pnpm exec nx`,
runUninstalledPackage: 'pnpm dlx',
// --no-frozen-lockfile: pnpm detects CI and would otherwise default to --frozen-lockfile.
install: 'pnpm install --no-frozen-lockfile',
ciInstall: 'pnpm install --frozen-lockfile',
addProd: isPnpmWorkspace ? 'pnpm add -w' : 'pnpm add',
addDev: isPnpmWorkspace ? 'pnpm add -Dw' : 'pnpm add -D',
list: 'pnpm ls --depth 10',
runLerna: `pnpm exec lerna`,
exec: pnpmVersion && gte(pnpmVersion, '6.13.0') ? 'pnpm exec' : 'pnpx',
},
bun: {
// See note in runCreateWorkspace in create-project-utils.ts for why we don't set @{version} for `bunx create-nx-workspace` right now
createWorkspace: `bunx create-nx-workspace`,
run: (script: string, args: string) => `bun run ${script} -- ${args}`,
runNx: `bunx nx`,
runNxSilent: `bunx nx`,
runUninstalledPackage: `bunx --yes`,
install: 'bun install',
ciInstall: 'bun install --no-cache',
addProd: 'bun install',
addDev: 'bun install -D',
list: 'bun pm ls',
runLerna: `bunx lerna`,
exec: 'bun',
},
}[packageManager.trim() as PackageManager];
}
export function runE2ETests(runner?: 'cypress' | 'playwright') {
if (process.env.NX_E2E_RUN_E2E === 'true') {
if (!runner || runner === 'cypress') {
ensureCypressInstallation();
}
if (!runner || runner === 'playwright') {
ensurePlaywrightBrowsersInstallation();
}
return true;
}
console.warn(
'Not running E2E tests because NX_E2E_RUN_E2E is not set to true.'
);
if (process.env.NX_E2E_RUN_CYPRESS) {
console.warn(
'NX_E2E_RUN_CYPRESS is deprecated, use NX_E2E_RUN_E2E instead.'
);
}
return false;
}
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: opts.cwd || tmpProjPath(),
env: {
CI: 'true',
// Force daemon on under CI (matches runCLI); override via opts.daemon = false.
NX_DAEMON: opts.daemon === false ? 'false' : 'true',
// Use new versioning by default in e2e tests
NX_INTERNAL_USE_LEGACY_VERSIONING: 'false',
...(opts.env || getStrippedEnvironmentVariables()),
FORCE_COLOR: 'false',
},
encoding: 'utf-8',
},
(err, stdout, stderr) => {
if (!opts.silenceError && err) {
logError(`Original command: ${command}`, `${stdout}\n\n${stderr}`);
reject(err);
}
const outputs = {
stdout: stripVTControlCharacters(stdout),
stderr: stripVTControlCharacters(stderr),
combinedOutput: stripVTControlCharacters(`${stdout}${stderr}`),
};
if (opts.verbose ?? isVerboseE2ERun()) {
output.log({
title: `Original command: ${command}`,
bodyLines: [outputs.combinedOutput],
color: 'green',
});
}
resolve(outputs);
}
);
});
}
export function runCommandUntil(
command: string,
criteria: (output: string) => boolean,
opts: RunCmdOpts & { timeout?: number } = {}
): Promise<ChildProcess> {
const pm = getPackageManagerCommand();
const timeout = opts.timeout ?? 30_000;
const p = exec(`${pm.runNx} ${command}`, {
cwd: tmpProjPath(),
encoding: 'utf-8',
env: {
CI: 'true',
NX_DAEMON: 'true',
// Use new versioning by default in e2e tests
NX_INTERNAL_USE_LEGACY_VERSIONING: 'false',
...getStrippedEnvironmentVariables(),
...opts.env,
FORCE_COLOR: 'false',
},
windowsHide: false,
});
return new Promise((res, rej) => {
let output = '';
let complete = false;
const timeoutId = setTimeout(() => {
if (!complete) {
complete = true;
p.kill();
logError(
`Output did not meet the criteria:`,
output
.split('\n')
.map((l) => ` ${l}`)
.join('\n')
);
rej(new Error(`Timed out after ${timeout}ms waiting for criteria`));
}
}, timeout);
function checkCriteria(c) {
output += c.toString();
const strippedOutput = stripVTControlCharacters(output);
if (criteria(strippedOutput) && !complete) {
complete = true;
clearTimeout(timeoutId);
res(p);
}
}
p.stdout?.on('data', checkCriteria);
p.stderr?.on('data', checkCriteria);
p.on('close', (code) => {
if (!complete) {
complete = true;
clearTimeout(timeoutId);
logError(
`Original output:`,
output
.split('\n')
.map((l) => ` ${l}`)
.join('\n')
);
rej(new Error(`Exited with ${code}`));
}
});
});
}
export function runCLIAsync(
command: string,
opts: RunCmdOpts = {
silenceError: false,
env: getStrippedEnvironmentVariables(),
silent: false,
}
): Promise<{ stdout: string; stderr: string; combinedOutput: string }> {
const pm = getPackageManagerCommand();
const commandToRun = `${opts.silent ? pm.runNxSilent : pm.runNx} ${command} ${
(opts.verbose ?? isVerboseE2ERun()) ? ' --verbose' : ''
}${opts.redirectStderr ? ' 2>&1' : ''}`;
return runCommandAsync(commandToRun, opts);
}
export function runNgAdd(
packageName: string,
command?: string,
version: string = getPublishedVersion(),
opts: RunCmdOpts = {
silenceError: false,
env: undefined,
cwd: tmpProjPath(),
}
): string {
try {
const pmc = getPackageManagerCommand();
packageInstall(packageName, undefined, version);
const fullCommand = pmc.run(`ng g ${packageName}:ng-add`, command ?? '');
const result = execSync(fullCommand, {
cwd: tmpProjPath(),
stdio: 'pipe',
env: { ...(opts.env || getStrippedEnvironmentVariables()) },
encoding: 'utf-8',
});
const r = stripVTControlCharacters(result);
if (opts.verbose ?? isVerboseE2ERun()) {
output.log({
title: `Original command: ${fullCommand}`,
bodyLines: [result as string],
color: 'green',
});
}
return r;
} catch (e) {
if (opts.silenceError) {
return e.stdout;
} else {
logError(`Ng Add failed: ${command}`, `${e.stdout}\n\n${e.stderr}`);
throw e;
}
}
}
/**
* Replaces the run-to-run durations / core counts in Nx's performance report with
* stable placeholders so the report can stay in snapshots. Scoped to the report
* block (from `Run duration:` to the next `NX` section header or end of output);
* no-op when no report is present.
*/
export function normalizePerformanceReport(output: string): string {
return output.replace(
/\n[ \t]*Run duration:[\s\S]*?(?=\n[ \t]*\n(?:[ \t]*\n)*[ \t]*NX |\s*$)/g,
(block) =>
block
// Durations: match the minute form ("1m 30s") first so its "30s" isn't matched
// alone; the optional "<" also captures a "<1ms" (sub-millisecond) duration.
.replace(/<?(?:\b\d+m \d+s\b|\b\d+(?:\.\d+)?m?s\b)/g, '{DURATION}')
.replace(/\b\d+(?= cores?\b)/g, '{CORES}')
// Longest-tasks list right-aligns durations (padStart); collapse the varying
// id→duration gap back to a fixed 4-space separator so the table is deterministic.
.replace(/^([ \t]+\S+) {4,}(\{DURATION\})$/gm, '$1 $2')
// Stat rows align values by padding the label column, whose width may change;
// collapse the label→value gap so the snapshot doesn't depend on that padding.
.replace(
/^([ \t]*(?:Run duration|Cache|Critical path|Recoverable time):) +/gm,
'$1 '
)
);
}
export function runCLI(
command: string,
opts: RunCmdOpts = {
silenceError: false,
env: undefined,
verbose: undefined,
redirectStderr: undefined,
}
): string {
const timeoutMs = opts.timeout ?? 5 * 60 * 1000;
try {
const pm = getPackageManagerCommand();
const commandToRun = `${pm.runNxSilent} ${command} ${
(opts.verbose ?? isVerboseE2ERun()) ? ' --verbose' : ''
}${opts.redirectStderr ? ' 2>&1' : ''}`;
logInfo(`Run Command: ${command}`);
const startTime = performance.now();
const result = execSync(commandToRun, {
cwd: opts.cwd || tmpProjPath(),
env: {
CI: 'true',
// Daemon is normally off under CI; force it on so e2e exercises the same
// daemon-driven graph + watcher path real users hit. Override via opts.daemon = false.
NX_DAEMON: opts.daemon === false ? 'false' : 'true',
// Use new versioning by default in e2e tests
NX_INTERNAL_USE_LEGACY_VERSIONING: 'false',
...getStrippedEnvironmentVariables(),
...opts.env,
},
encoding: 'utf-8',
stdio: 'pipe',
maxBuffer: 50 * 1024 * 1024,
timeout: timeoutMs,
});
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
logInfo(`Run Command: ${command} (${elapsed}s)`);
if (opts.verbose ?? isVerboseE2ERun()) {
output.log({
title: `Original command: ${command}`,
bodyLines: [result as string],
color: 'green',
});
}
const r = stripVTControlCharacters(result);
runCLI.lastExitCode = 0;
return r;
} catch (e) {
if (e.killed || e.signal) {
const timeoutSec = Math.round(timeoutMs / 1000);
const processOutput = stripVTControlCharacters(
`${e.stdout ?? ''}\n\n${e.stderr ?? ''}`
).trim();
const msg = `Command timed out after ${timeoutSec}s: ${command}\n\nProcess output:\n${processOutput}`;
logError(`Command timed out`, msg);
throw new Error(msg);
}
if (opts.silenceError) {
runCLI.lastExitCode = (e.status ?? 1) as number;
// Without redirectStderr the shell didn't merge stderr into stdout, so concat both.
const output = opts.redirectStderr ? e.stdout : e.stdout + e.stderr;
return stripVTControlCharacters(output);
} else {
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
throw e;
}
}
}
runCLI.lastExitCode = 0 as number;
export function runLernaCLI(
command: string,
opts: RunCmdOpts = {
silenceError: false,
env: undefined,
}
): string {
const timeoutMs = opts.timeout ?? 2 * 60 * 1000;
try {
const pm = getPackageManagerCommand();
const fullCommand = `${pm.runLerna} ${command}`;
const logs = execSync(fullCommand, {
cwd: opts.cwd || tmpProjPath(),
env: {
CI: 'true',
...(opts.env || getStrippedEnvironmentVariables()),
},
encoding: 'utf-8',
stdio: 'pipe',
maxBuffer: 50 * 1024 * 1024,
timeout: timeoutMs,
});
if (opts.verbose ?? isVerboseE2ERun()) {
output.log({
title: `Original command: ${fullCommand}`,
bodyLines: [logs as string],
color: 'green',
});
}
const r = stripVTControlCharacters(logs);
return r;
} catch (e) {
if (e.killed || e.signal) {
const timeoutSec = Math.round(timeoutMs / 1000);
const processOutput = stripVTControlCharacters(
`${e.stdout ?? ''}\n\n${e.stderr ?? ''}`
).trim();
const msg = `Command timed out after ${timeoutSec}s: ${command}\n\nProcess output:\n${processOutput}`;
logError(`Command timed out`, msg);
throw new Error(msg);
}
if (opts.silenceError) {
return stripVTControlCharacters(e.stdout + e.stderr);
} else {
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
throw e;
}
}
}
export function waitUntil(
predicate: () => boolean,
opts: { timeout: number; ms: number; allowError?: boolean } = {
timeout: 5000,
ms: 50,
}
): Promise<void> {
return new Promise((resolve, reject) => {
const t = setInterval(() => {
const run = () => {};
try {
run();
if (predicate()) {
clearInterval(t);
resolve();
}
} catch (e) {
if (opts.allowError) reject(e);
}
}, opts.ms);
setTimeout(() => {
clearInterval(t);
reject(new Error(`Timed out waiting for condition to return true`));
}, opts.timeout);
});
}
export function isDockerAvailable() {
try {
const dockerVersionInfo = runCommand(`docker info -f json`);
return !dockerVersionInfo.includes('Cannot connect to the Docker daemon');
} catch {
return false;
}
}
+773
View File
@@ -0,0 +1,773 @@
import { copySync, ensureDirSync, moveSync, removeSync } from 'fs-extra';
import * as isCI from 'is-ci';
import {
createFile,
directoryExists,
tmpBackupNgCliProjPath,
tmpBackupProjPath,
updateFile,
updateJson,
} from './file-utils';
import {
e2eCwd,
getLatestLernaVersion,
getPublishedVersion,
getSelectedPackageManager,
getStrippedEnvironmentVariables,
isVerbose,
isVerboseE2ERun,
} from './get-env-info';
import { output, readJsonFile } from '@nx/devkit';
import { angularDevkitVersion as defaultAngularCliVersion } from '@nx/angular/internal';
import { dump } from '@zkochan/js-yaml';
import { execSync, ExecSyncOptions } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { performance, PerformanceMeasure } from 'node:perf_hooks';
import { resetWorkspaceContext } from 'nx/src/utils/workspace-context';
import {
getPackageManagerCommand,
runCLI,
RunCmdOpts,
runCommand,
} from './command-utils';
import { logError, logInfo } from './log-utils';
let projName: string;
// TODO(jack): we should tag the projects (e.g. tags: ['package']) and filter from that rather than hard-code packages.
const nxPackages = [
`@nx/angular`,
`@nx/cypress`,
`@nx/docker`,
`@nx/eslint-plugin`,
`@nx/express`,
`@nx/esbuild`,
`@nx/gradle`,
`@nx/jest`,
`@nx/js`,
`@nx/eslint`,
'@nx/maven',
`@nx/nest`,
`@nx/next`,
`@nx/node`,
`@nx/nuxt`,
`@nx/plugin`,
`@nx/playwright`,
`@nx/rollup`,
`@nx/react`,
`@nx/remix`,
`@nx/rsbuild`,
`@nx/rspack`,
`@nx/storybook`,
`@nx/vue`,
`@nx/vite`,
`@nx/vitest`,
`@nx/web`,
`@nx/webpack`,
`@nx/react-native`,
`@nx/expo`,
'@nx/dotnet',
`@nx/module-federation`,
`@nx/workspace`,
] as const;
type NxPackage = (typeof nxPackages)[number];
export function openInEditor(projectDirectory: string = tmpProjPath()) {
if (process.env.NX_E2E_EDITOR) {
const editor = process.env.NX_E2E_EDITOR;
execSync(`${editor} ${projectDirectory}`, {
stdio: 'inherit',
});
}
}
/**
* Sets up a new project in the temporary project path
* for the currently selected CLI.
*/
export function newProject({
name = uniq('proj'),
packageManager = getSelectedPackageManager(),
packages,
preset = 'apps',
}: {
name?: string;
packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun';
readonly packages?: Array<NxPackage>;
preset?: string;
} = {}): string {
const newProjectStart = performance.mark('new-project:start');
try {
const projScope = 'proj';
let createNxWorkspaceMeasure: PerformanceMeasure;
let packageInstallMeasure: PerformanceMeasure;
// Namespace by package manager to avoid conflicts in test suites which include multiple package managers
const backupPath = tmpBackupProjPath(packageManager);
if (!directoryExists(backupPath)) {
const createNxWorkspaceStart = performance.mark(
'create-nx-workspace:start'
);
runCreateWorkspace(projScope, {
preset,
packageManager,
});
const createNxWorkspaceEnd = performance.mark('create-nx-workspace:end');
createNxWorkspaceMeasure = performance.measure(
'create-nx-workspace',
createNxWorkspaceStart.name,
createNxWorkspaceEnd.name
);
// Workaround: pnpm defaults to frozen-lockfile in CI, but e2e tests
// dynamically add packages after workspace creation
if (isCI && packageManager === 'pnpm') {
updateFile('.npmrc', 'prefer-frozen-lockfile=false');
}
let packagesToInstall: Array<NxPackage> = [];
if (!packages) {
console.warn(
'ATTENTION: All packages are installed into the new workspace. To make this test faster, please pass the subset of packages that this test needs by passing a packages array in the options'
);
packagesToInstall = [...nxPackages];
} else if (packages.length > 0) {
packagesToInstall = packages;
}
if (packagesToInstall.length) {
const packageInstallStart = performance.mark('packageInstall:start');
packageInstall(packagesToInstall.join(` `), projScope);
const packageInstallEnd = performance.mark('packageInstall:end');
packageInstallMeasure = performance.measure(
'packageInstall',
packageInstallStart.name,
packageInstallEnd.name
);
} else {
console.info('No packages to install');
}
// stop the daemon
execSync(`${getPackageManagerCommand({ packageManager }).runNx} reset`, {
cwd: `${e2eCwd}/proj`,
stdio: isVerbose() ? 'inherit' : 'pipe',
});
moveSync(`${e2eCwd}/proj`, backupPath);
}
projName = name;
const projectDirectory = tmpProjPath();
copySync(backupPath, projectDirectory);
const dependencies = readJsonFile(
`${projectDirectory}/package.json`
).devDependencies;
const missingPackages = (packages || []).filter((p) => !dependencies[p]);
if (missingPackages.length > 0) {
packageInstall(missingPackages.join(` `), projName);
} else if (packageManager === 'pnpm') {
// pnpm creates sym links to the pnpm store,
// we need to run the install again after copying the temp folder
try {
execSync(getPackageManagerCommand().install, {
cwd: projectDirectory,
stdio: 'pipe',
env: { CI: 'true', ...process.env },
encoding: 'utf-8',
});
} catch (e) {
console.log('newProject() - reinstall pnpm dependencies failed');
console.error('Full error:', e);
throw e;
}
}
const newProjectEnd = performance.mark('new-project:end');
const perfMeasure = performance.measure(
'newProject',
newProjectStart.name,
newProjectEnd.name
);
logInfo(
`NX`,
`E2E created a project: ${projectDirectory} in ${
perfMeasure.duration / 1000
} seconds
${
createNxWorkspaceMeasure
? `create-nx-workspace: ${
createNxWorkspaceMeasure.duration / 1000
} seconds\n`
: ''
}${
packageInstallMeasure
? `packageInstall: ${packageInstallMeasure.duration / 1000} seconds\n`
: ''
}`
);
openInEditor(projectDirectory);
return projScope;
} catch (e) {
logError(`Failed to set up project for e2e tests.`, e.message);
throw e;
}
}
export function runCreateWorkspace(
name: string,
{
preset,
template,
appName,
style,
base,
packageManager,
extraArgs,
useDetectedPm = false,
cwd = e2eCwd,
bundler,
routing,
useReactRouter,
standaloneApi,
docker,
nextAppDir,
nextSrcDir,
linter = 'eslint',
formatter = 'prettier',
unitTestRunner,
e2eTestRunner,
ssr,
framework,
prefix,
}: {
preset?: string;
template?: string;
appName?: string;
style?: string;
base?: string;
packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun';
extraArgs?: string;
useDetectedPm?: boolean;
cwd?: string;
bundler?: 'webpack' | 'vite';
standaloneApi?: boolean;
routing?: boolean;
useReactRouter?: boolean;
docker?: boolean;
nextAppDir?: boolean;
nextSrcDir?: boolean;
linter?: 'none' | 'eslint';
unitTestRunner?:
| 'jest'
| 'vitest'
| 'vitest-angular'
| 'vitest-analog'
| 'none';
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';
formatter?: 'prettier' | 'none';
ssr?: boolean;
framework?: string;
prefix?: string;
}
) {
if (preset && template) {
throw new Error(
'Cannot specify both preset and template. Use one or the other.'
);
}
if (!preset && !template) {
throw new Error('Must specify either preset or template.');
}
projName = name;
const pm = getPackageManagerCommand({ packageManager });
// Needed for bun workarounds, see below
const registry = execSync('npm config get registry').toString().trim();
let command = `${pm.createWorkspace} ${name} --no-interactive`;
if (preset) {
command += ` --preset=${preset}`;
}
if (template) {
command += ` --template=${template}`;
}
if (appName) {
command += ` --appName=${appName}`;
}
if (style) {
command += ` --style=${style}`;
}
if (bundler) {
command += ` --bundler=${bundler}`;
}
if (nextAppDir !== undefined) {
command += ` --nextAppDir=${nextAppDir}`;
}
if (nextSrcDir !== undefined) {
command += ` --nextSrcDir=${nextSrcDir}`;
}
if (docker !== undefined) {
command += ` --docker=${docker}`;
}
if (standaloneApi !== undefined) {
command += ` --standaloneApi=${standaloneApi}`;
}
if (routing !== undefined) {
command += ` --routing=${routing}`;
}
if (useReactRouter !== undefined) {
command += ` --useReactRouter=${useReactRouter}`;
}
if (base) {
command += ` --defaultBase="${base}"`;
}
if (packageManager && !useDetectedPm) {
command += ` --package-manager=${packageManager}`;
}
if (linter) {
command += ` --linter=${linter}`;
}
if (formatter) {
command += ` --formatter=${formatter}`;
}
if (unitTestRunner) {
command += ` --unitTestRunner=${unitTestRunner}`;
}
if (e2eTestRunner) {
command += ` --e2eTestRunner=${e2eTestRunner}`;
}
if (framework) {
command += ` --framework=${framework}`;
}
if (extraArgs) {
command += ` ${extraArgs}`;
}
if (isCI) {
command += ` --verbose`;
}
if (ssr !== undefined) {
command += ` --ssr=${ssr}`;
}
if (prefix !== undefined) {
command += ` --prefix=${prefix}`;
}
try {
const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, {
cwd,
stdio: 'pipe',
env: {
CI: 'true',
NX_VERBOSE_LOGGING: isCI ? 'true' : 'false',
...getStrippedEnvironmentVariables(),
},
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' | 'bun';
extraArgs?: string;
useDetectedPm?: boolean;
}
) {
projName = name;
const pm = getPackageManagerCommand({ packageManager });
let command = `${
pm.runUninstalledPackage
} create-nx-plugin@${getPublishedVersion()} ${name} --nxCloud=skip --no-interactive`;
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' : 'pipe',
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];
// The cached Angular CLI workspace includes symlinked node_modules/.bin entries.
// If cleanup left the destination behind, fs-extra will fail while overwriting
// those links, so always start from a clean restore target.
removeSync(tmpProjPath());
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',
});
// ensure angular packages are installed with ~ instead of ^ to prevent
// potential failures when new minor versions are released
function updateAngularDependencies(dependencies: any): void {
Object.keys(dependencies).forEach((key) => {
if (key.startsWith('@angular/') || key.startsWith('@angular-devkit/')) {
dependencies[key] = dependencies[key].replace(/^\^/, '~');
}
});
}
updateJson('package.json', (json) => {
updateAngularDependencies(json.dependencies ?? {});
updateAngularDependencies(json.devDependencies ?? {});
return json;
});
execSync(pmc.install, {
cwd: join(e2eCwd, projName),
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,
'@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 if (packageManager === 'bun') {
json.overrides = {
...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 createEmptyProjectDirectory(name = uniq('proj')) {
projName = name;
ensureDirSync(tmpProjPath());
}
export function uniq(prefix: string): string {
// We need to ensure that the length of the random section of the name is of consistent length to avoid flakiness in tests
const randomSevenDigitNumber = Math.floor(Math.random() * 10000000)
.toString()
.padStart(7, '0');
return `${prefix}${randomSevenDigitNumber}`;
}
// 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 (process.env.NX_E2E_SKIP_PROJECT_CLEANUP) {
resetWorkspaceContext();
return;
}
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 {}
}
resetWorkspaceContext();
}
+204
View File
@@ -0,0 +1,204 @@
import { execSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
readFileSync,
statSync,
unlinkSync,
writeFileSync,
openSync,
closeSync,
} from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { tmpProjPath } from './create-project-utils';
import { e2eConsoleLogger } from './log-utils';
import { isVerbose } from './get-env-info';
// Helper files to prevent multiple `npx playwright install` on the same machine.
// Doing so will cause `apt` to fail on Linux machines.
const LOCK_DIR = join(tmpdir(), 'nx-e2e-locks');
const PLAYWRIGHT_LOCK_FILE = join(LOCK_DIR, 'playwright-install.lock');
const PLAYWRIGHT_STATUS_FILE = join(LOCK_DIR, 'playwright-install.status');
const POLL_INTERVAL_MS = 5000;
const MAX_WAIT_MS = 5 * 60 * 1000;
interface InstallStatus {
status: 'installing' | 'success' | 'failed';
pid: number;
timestamp: number;
error?: string;
}
function ensureLockDir() {
if (!existsSync(LOCK_DIR)) {
mkdirSync(LOCK_DIR, { recursive: true });
}
}
function tryAcquireLock(lockFile: string, statusFile: string): boolean {
ensureLockDir();
try {
// Atomic operation - fails if file exists
const fd = openSync(lockFile, 'wx');
closeSync(fd);
const status: InstallStatus = {
status: 'installing',
pid: process.pid,
timestamp: Date.now(),
};
writeFileSync(statusFile, JSON.stringify(status));
return true;
} catch (err) {
if (err.code === 'EEXIST') {
return false; // Lock already held
}
throw err; // Unexpected error
}
}
function releaseLock(
lockFile: string,
statusFile: string,
success: boolean,
error?: string
) {
try {
const status: InstallStatus = {
status: success ? 'success' : 'failed',
pid: process.pid,
timestamp: Date.now(),
error: error,
};
writeFileSync(statusFile, JSON.stringify(status));
} finally {
try {
unlinkSync(lockFile);
} catch {
// Best effort
}
}
}
function readInstallStatus(statusFile: string): InstallStatus | null {
try {
if (!existsSync(statusFile)) {
return null;
}
const content = readFileSync(statusFile, 'utf-8');
return JSON.parse(content);
} catch {
return null;
}
}
async function waitForInstallation(
lockFile: string,
statusFile: string,
toolName: string
): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < MAX_WAIT_MS) {
// Check if lock is released
if (!existsSync(lockFile)) {
const status = readInstallStatus(statusFile);
if (status?.status === 'success') {
e2eConsoleLogger(
`${toolName} browsers installed by process ${status.pid}`
);
return;
}
if (status?.status === 'failed') {
const errorMsg = `${toolName} browser installation failed in process ${
status.pid
}: ${status.error || 'Unknown error'}`;
e2eConsoleLogger(errorMsg);
throw new Error(errorMsg);
}
}
// Wait before polling again
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error(
`Timeout waiting for ${toolName} browser installation to complete`
);
}
export function ensureCypressInstallation() {
let cypressVerified = true;
try {
const r = execSync('npx cypress verify', {
stdio: isVerbose() ? 'inherit' : 'pipe',
encoding: 'utf-8',
cwd: tmpProjPath(),
});
if (r.indexOf('Verified Cypress!') === -1) {
cypressVerified = false;
}
} catch {
cypressVerified = false;
} finally {
if (!cypressVerified) {
e2eConsoleLogger('Cypress was not verified. Installing Cypress now.');
execSync('npx cypress install', {
stdio: isVerbose() ? 'inherit' : 'pipe',
encoding: 'utf-8',
cwd: tmpProjPath(),
});
}
}
}
export async function ensurePlaywrightBrowsersInstallation() {
// If lock is acquired, perform installation, otherwise it must in progress or already done
if (tryAcquireLock(PLAYWRIGHT_LOCK_FILE, PLAYWRIGHT_STATUS_FILE)) {
// We got the lock - we're responsible for installation
e2eConsoleLogger(
`Process ${process.pid} acquired lock, installing Playwright browsers...`
);
const playwrightInstallArgs =
process.env.PLAYWRIGHT_INSTALL_ARGS || '--with-deps';
try {
execSync(`npx playwright install ${playwrightInstallArgs}`, {
stdio: isVerbose() ? 'inherit' : 'pipe',
encoding: 'utf-8',
cwd: tmpProjPath(),
});
const version = execSync('npx playwright --version').toString().trim();
e2eConsoleLogger(
`Playwright browsers ${version} installed successfully.`
);
releaseLock(PLAYWRIGHT_LOCK_FILE, PLAYWRIGHT_STATUS_FILE, true);
} catch (error) {
e2eConsoleLogger('Failed to install Playwright browsers:', error);
releaseLock(
PLAYWRIGHT_LOCK_FILE,
PLAYWRIGHT_STATUS_FILE,
false,
error.message
);
throw error;
}
} else {
e2eConsoleLogger(
`Process ${process.pid} waiting for Playwright browser installation...`
);
await waitForInstallation(
PLAYWRIGHT_LOCK_FILE,
PLAYWRIGHT_STATUS_FILE,
'Playwright'
);
}
}
+150
View File
@@ -0,0 +1,150 @@
import { parseJson } from '@nx/devkit';
import {
createFileSync,
ensureDirSync,
readdirSync,
readFileSync,
removeSync,
renameSync,
statSync,
writeFileSync,
} from 'fs-extra';
import * as path from 'path';
import { e2eCwd } from './get-env-info';
import { tmpProjPath } from './create-project-utils';
import { join } from 'path';
export function createFile(f: string, content: string = ''): void {
const path = tmpProjPath(f);
createFileSync(path);
if (content) {
updateFile(f, 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 checkFilesMatchingPatternExist(
pattern: string,
cwd = tmpProjPath()
) {
const files = readdirSync(cwd, {
withFileTypes: true,
recursive: true,
});
const regex = new RegExp(pattern);
const matchedFiles = files
.filter((f) => f.isFile() && regex.test(join(f.parentPath, f.name)))
.map((f) => f.name);
if (matchedFiles.length === 0) {
throw new Error(
`No files matching pattern '${pattern}' were found.` +
`
Existing files: \n
${files.map((f) => join(f.parentPath, f.name)).join('\n')}`
);
}
}
export function updateJson<T extends object = any, U extends object = T>(
f: string,
updater: (value: T) => U
) {
updateFile(f, (s) => {
const json = JSON.parse(s);
return JSON.stringify(updater(json), null, 2);
});
}
export function checkFilesDoNotExist(...expectedFiles: string[]) {
expectedFiles.forEach((f) => {
const ff = f.startsWith('/') ? f : tmpProjPath(f);
if (exists(ff)) {
throw new Error(`File '${ff}' should not exist`);
}
});
}
export function listFiles(dirName: string) {
return readdirSync(tmpProjPath(dirName));
}
export function readJson<T extends Object = any>(f: string): T {
const content = readFile(f);
return parseJson<T>(content);
}
export function readFile(f: string) {
const ff = f.startsWith('/') ? f : tmpProjPath(f);
return readFileSync(ff, 'utf-8');
}
export function removeFile(f: string) {
const ff = f.startsWith('/') ? f : tmpProjPath(f);
removeSync(ff);
}
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 tmpBackupProjPath(path?: string) {
return path ? `${e2eCwd}/proj-backup/${path}` : `${e2eCwd}/proj-backup`;
}
export function tmpBackupNgCliProjPath(path?: string) {
return path
? `${e2eCwd}/ng-cli-proj-backup/${path}`
: `${e2eCwd}/ng-cli-proj-backup`;
}
+200
View File
@@ -0,0 +1,200 @@
import { readJsonFile, workspaceRoot } from '@nx/devkit';
import { existsSync } from 'fs-extra';
import { execSync } from 'node:child_process';
import { join } from 'path';
import { gte } from 'semver';
import { dirSync } from 'tmp';
import * as isCI from 'is-ci';
import { PackageManager } from 'nx/src/utils/package-manager';
import { tmpProjPath } from './create-project-utils';
import { e2eConsoleLogger } from './log-utils';
export const isWindows = require('is-windows');
export function getPublishedVersion(): string {
process.env.PUBLISHED_VERSION =
process.env.PUBLISHED_VERSION ||
// read version of built nx package
readJsonFile(join(workspaceRoot, `./dist/packages/nx/package.json`))
.version ||
// fallback to latest if built nx package is missing
'latest';
return process.env.PUBLISHED_VERSION as string;
}
export function detectPackageManager(dir: string = ''): PackageManager {
return existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock'))
? 'bun'
: existsSync(join(dir, 'yarn.lock'))
? 'yarn'
: existsSync(join(dir, 'pnpm-lock.yaml')) ||
existsSync(join(dir, 'pnpm-workspace.yaml'))
? 'pnpm'
: 'npm';
}
export function isNotWindows() {
return !isWindows();
}
export function isOSX() {
return process.platform === 'darwin';
}
export function isAndroid() {
return (
process.platform === 'linux' &&
process.env.ANDROID_HOME &&
process.env.ANDROID_SDK_ROOT
);
}
export const e2eRoot = isCI
? dirSync({ prefix: 'nx-e2e-' }).name
: '/tmp/nx-e2e';
export function isVerbose() {
return (
process.env.NX_VERBOSE_LOGGING === 'true' ||
process.argv.includes('--verbose')
);
}
export function isVerboseE2ERun() {
return process.env.NX_E2E_VERBOSE_LOGGING === 'true' || isVerbose();
}
export const e2eCwd = `${e2eRoot}/nx`;
export function getSelectedPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' {
return (process.env.SELECTED_PM as 'npm' | 'yarn' | 'pnpm' | 'bun') || 'npm';
}
export function getNpmMajorVersion(): string | undefined {
try {
const [npmMajorVersion] = execSync(`npm -v`).toString().split('.');
return npmMajorVersion;
} catch {
return undefined;
}
}
export function getYarnMajorVersion(path: string): string | undefined {
try {
// this fails if path is not yet created
const [yarnMajorVersion] = execSync(`yarn -v`, {
cwd: path,
encoding: 'utf-8',
}).split('.');
return yarnMajorVersion;
} catch {
try {
const [yarnMajorVersion] = execSync(`yarn -v`, {
encoding: 'utf-8',
}).split('.');
return yarnMajorVersion;
} catch {
return undefined;
}
}
}
export function getPnpmVersion(): string | undefined {
try {
const pnpmVersion = execSync(`pnpm -v`, {
encoding: 'utf-8',
}).trim();
return pnpmVersion;
} catch {
return undefined;
}
}
export function getLatestLernaVersion(): string {
const lernaVersion = execSync(`npm view lerna version`, {
encoding: 'utf-8',
}).trim();
return lernaVersion;
}
export const packageManagerLockFile = {
npm: 'package-lock.json',
yarn: 'yarn.lock',
pnpm: 'pnpm-lock.yaml',
bun: (() => {
try {
// In version 1.2.0, bun switched to a text based lockfile format by default
return gte(execSync('bun --version').toString().trim(), '1.2.0')
? 'bun.lock'
: 'bun.lockb';
} catch {
return 'bun.lockb';
}
})(),
};
// Re-export browser installation utilities from ensure-browser-installation.ts
export {
ensureCypressInstallation,
ensurePlaywrightBrowsersInstallation,
} from './ensure-browser-installation';
export function getStrippedEnvironmentVariables() {
return Object.fromEntries(
Object.entries(process.env).filter(([key]) => {
if (key.startsWith('NX_E2E_')) {
return true;
}
const allowedKeys = [
'NX_ADD_PLUGINS',
'NX_ISOLATE_PLUGINS',
'NX_VERBOSE_LOGGING',
'NX_NATIVE_LOGGING',
'NX_USE_LOCAL',
];
if (key.startsWith('NX_') && !allowedKeys.includes(key)) {
return false;
}
if (key === 'JEST_WORKER_ID') {
return false;
}
// Remove NODE_PATH to prevent module resolution conflicts with original workspace.
// NODE_PATH is inherited from Jest (which runs from the original workspace) and contains
// pnpm paths that cause require.resolve() to find workspace packages instead of e2e test versions.
if (key === 'NODE_PATH') {
return false;
}
// Remove GITHUB_STEP_SUMMARY so e2e subprocesses don't append to the real CI job
// summary. The stripper drops NX_TASK_TARGET_PROJECT, so a child nx looks top-level
// and would otherwise write its performance report (once per nx command) into the
// runner's summary. GITHUB_ACTIONS is kept so log grouping still gets exercised.
if (key === 'GITHUB_STEP_SUMMARY') {
return false;
}
// Remove AI agent detection env vars to prevent the test runner's
// environment (e.g., running inside Claude Code) from leaking into
// e2e test subprocesses. Tests that need these pass them explicitly.
const aiAgentEnvVars = [
'CLAUDECODE',
'CLAUDE_CODE',
'OPENCODE',
'GEMINI_CLI',
'CURSOR_TRACE_ID',
'COMPOSER_NO_INTERACTION',
'REPL_ID',
];
if (aiAgentEnvVars.includes(key)) {
return false;
}
return true;
})
);
}
+152
View File
@@ -0,0 +1,152 @@
import { Config } from '@jest/types';
import { existsSync, removeSync } from 'fs-extra';
import * as isCI from 'is-ci';
import { exec, execSync } from 'node:child_process';
import { join } from 'node:path';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { registerTsConfigPaths } from '../../packages/nx/src/plugins/js/utils/register';
import { runLocalRelease } from '../../scripts/local-registry/populate-storage';
export default async function (globalConfig: Config.ConfigGlobals) {
try {
// TEMP DIAGNOSTIC (cache-miss hunt): log the exact checkout + any working-tree
// drift on the agent. Distinguishes "runs are on different SHAs" from
// "same SHA but a task mutated the tree". Safe to remove once resolved.
if (process.env.CI) {
try {
const head = execSync('git rev-parse HEAD').toString().trim();
const porcelain = execSync('git status --porcelain').toString().trim();
console.log(
`\n=== [tree-state] target=${process.env.NX_TASK_TARGET_TARGET} HEAD=${head} ===\n` +
`git status --porcelain:\n${porcelain || '(clean)'}\n`
);
} catch (err) {
console.log(
`[tree-state] diagnostic failed: ${(err as Error).message}`
);
}
}
const isVerbose: boolean =
process.env.NX_VERBOSE_LOGGING === 'true' || !!globalConfig.verbose;
/**
* For e2e-ci, e2e-local and macos-local-e2e we populate the verdaccio storage up front, but for other workflows we need
* to run the full local release process before running tests.
*/
const prefixes = ['e2e-ci', 'e2e-macos-local', 'e2e-local'];
const requiresLocalRelease = !prefixes.some((prefix) =>
process.env.NX_TASK_TARGET_TARGET?.startsWith(prefix)
);
const listenAddress = 'localhost';
const port = process.env.NX_LOCAL_REGISTRY_PORT ?? '4873';
const registry = `http://${listenAddress}:${port}`;
const authToken = 'secretVerdaccioToken';
while (true) {
await new Promise((resolve) => setTimeout(resolve, 250));
try {
await assertLocalRegistryIsRunning(registry);
break;
} catch {
console.log(`Waiting for Local registry to start on ${registry}...`);
}
}
process.env.npm_config_registry = registry;
// Use environment variable instead of npm config command to avoid polluting other tests
process.env[`npm_config_//${listenAddress}:${port}/:_authToken`] =
authToken;
// bun
process.env.BUN_CONFIG_REGISTRY = registry;
process.env.BUN_CONFIG_TOKEN = authToken;
// yarnv1
process.env.YARN_REGISTRY = registry;
// yarnv2
process.env.YARN_NPM_REGISTRY_SERVER = registry;
process.env.YARN_UNSAFE_HTTP_WHITELIST = listenAddress;
// Use fresh cache directories to avoid serving stale packages when the
// same version is republished to the local registry.
const e2eCacheDir = mkdtempSync(join(tmpdir(), 'nx-e2e-cache-'));
process.env.npm_config_cache = join(e2eCacheDir, 'npm');
// yarnv1
process.env.YARN_CACHE_FOLDER = join(e2eCacheDir, 'yarn');
// yarnv2
process.env.YARN_ENABLE_GLOBAL_CACHE = 'false';
process.env.NX_SKIP_PROVENANCE_CHECK = 'true';
global.e2eTeardown = () => {
// Clean up environment variable instead of npm config command
delete process.env[`npm_config_//${listenAddress}:${port}/:_authToken`];
};
/**
* Set the published version based on what has previously been loaded into the
* verdaccio storage.
*/
if (!requiresLocalRelease) {
let publishedVersion = await getPublishedVersion();
console.log(`Testing Published version: Nx ${publishedVersion}`);
if (publishedVersion) {
process.env.PUBLISHED_VERSION = publishedVersion;
}
}
if (
process.env.NX_E2E_SKIP_GLOBAL_CLEANUP !== 'true' ||
!existsSync('./build')
) {
if (!isCI) {
registerTsConfigPaths(join(__dirname, '../../tsconfig.base.json'));
const { e2eCwd } = await import('./get-env-info');
removeSync(e2eCwd);
}
if (requiresLocalRelease) {
console.log('Publishing packages to local registry');
const publishVersion = process.env.PUBLISHED_VERSION ?? 'major';
// Always show full release logs on CI, they should only happen once via e2e-ci
await runLocalRelease(publishVersion, isCI || isVerbose);
}
}
} catch (err) {
// Clean up registry if possible after setup related errors
if (typeof global.e2eTeardown === 'function') {
global.e2eTeardown();
console.log('Killed local registry process due to an error during setup');
}
throw err;
}
}
function getPublishedVersion(): Promise<string | undefined> {
execSync(`npm config get registry`, {
stdio: 'inherit',
});
return new Promise((resolve) => {
// Resolve the published nx version from verdaccio
exec(
'npm view nx@latest version',
{
windowsHide: false,
},
(error, stdout, stderr) => {
if (error) {
return resolve(undefined);
}
return resolve(stdout.trim());
}
);
});
}
async function assertLocalRegistryIsRunning(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
+6
View File
@@ -0,0 +1,6 @@
export default function () {
if (global.e2eTeardown) {
global.e2eTeardown();
console.log('Killed local registry process');
}
}
+14
View File
@@ -0,0 +1,14 @@
import { ensureDirSync } from 'fs-extra';
import { e2eCwd } from './get-env-info';
ensureDirSync(e2eCwd);
export * from './command-utils';
export * from './create-project-utils';
export * from './file-utils';
export * from './get-env-info';
export * from './log-utils';
export * from './test-utils';
export * from './process-utils';
export * from './strip-indents';
export * from './port-utils';
+70
View File
@@ -0,0 +1,70 @@
import { styleText } from 'node:util';
const orangeColor = (text) => `\x1b[38;5;214m${text}\x1b[39m`;
export const E2E_LOG_PREFIX = `${styleText(
['reset', 'inverse', 'bold'],
orangeColor(' E2E ')
)}`;
export function e2eConsoleLogger(message: string, body?: string) {
process.stdout.write('\n');
process.stdout.write(`${E2E_LOG_PREFIX} ${message}\n`);
if (body) {
process.stdout.write(`${body}\n`);
}
process.stdout.write('\n');
}
export function logInfo(title: string, body?: string) {
const message = `${styleText(
['reset', 'inverse', 'bold', 'white'],
' INFO '
)} ${styleText(['bold', 'white'], title)}`;
return e2eConsoleLogger(message, body);
}
export function logError(title: string, body?: string) {
const message = `${styleText(
['reset', 'inverse', 'bold', 'red'],
' ERROR '
)} ${styleText(['bold', 'red'], title)}`;
return e2eConsoleLogger(message, body);
}
export function logSuccess(title: string, body?: string) {
const message = `${styleText(
['reset', 'inverse', 'bold', 'green'],
' SUCCESS '
)} ${styleText(['bold', 'green'], title)}`;
return e2eConsoleLogger(message, body);
}
/**
* Trims a daemon.log to the lines useful for diagnosing daemon/plugin issues
* (restarts, plugin loads, stale-graph discards, errors) plus the tail,
* marking where noise was dropped. Keeps e2e CI output small but debuggable.
*/
export function trimDaemonLog(contents: string, tailLines = 30): string {
const signal =
/New daemon|Server stopped|Restarting daemon|Started new daemon|Daemon outdated|lock file hash|loadSpecifiedNxPlugins|loadDefaultNxPlugins|Load Nx Plugin:|Failed to load|AggregateError|Unable to find local plugin|Discarding stale|No in-memory cached project graph|Graph recompute necessary|recomputing project graph|Cannot find module|with an error|Error:/;
const lines = contents.split('\n');
const tailStart = Math.max(0, lines.length - tailLines);
const out: string[] = [];
let dropped = 0;
lines.forEach((line, i) => {
if (signal.test(line) || i >= tailStart) {
if (dropped > 0) {
out.push(`${dropped} line(s) trimmed …`);
dropped = 0;
}
out.push(line);
} else {
dropped++;
}
});
if (dropped > 0) {
out.push(`${dropped} line(s) trimmed …`);
}
return out.join('\n');
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@nx/e2e-utils",
"version": "0.0.1",
"private": true,
"main": "index.ts",
"types": "index.ts",
"dependencies": {
"nx": "workspace:*",
"@nx/devkit": "workspace:*",
"@nx/angular": "workspace:*"
}
}
+230
View File
@@ -0,0 +1,230 @@
import * as fs from 'fs';
import * as net from 'net';
import * as os from 'os';
import * as path from 'path';
/**
* Reserves a port across parallel processes on the same host via an atomic
* lock file. Avoids the TOCTOU race of probing port 0 and then binding it
* seconds later — another parallel process could be handed the same port in
* between.
*
* Starts at 6100 (outside the framework-default zone of 3000/4200/5173/8080/etc.)
* so reserved ports never collide with parallel tests that generate apps
* without explicitly pinning the dev-server port.
*
* Caveat: this reserves the port *number* against other `reservePort()`
* callers; it does not hold the OS socket open. A consumer that binds the
* port much later (e.g. after a long webpack compile) is still racing
* anything that does not participate in this scheme.
*/
const LOCK_DIR = path.join(os.tmpdir(), 'nx-e2e-port-locks');
fs.mkdirSync(LOCK_DIR, { recursive: true });
const RANGE_FLOOR = 6100;
const RANGE_CEILING = 65000;
// Spread of the randomised scan origin (see reservePort). Wide enough that
// reservations scatter thinly across nearly the whole range, so a squatter
// (a leaked dev server, or anything binding a port mid-compile) is unlikely
// to land on the specific port a test reserved.
const SCAN_SPREAD = 55000;
// A lock older than this is treated as abandoned even when its PID still
// resolves (the PID may have been recycled). Comfortably above the longest
// e2e test timeout so a legitimately long-held port is never stolen.
const STALE_LOCK_MS = 60 * 60 * 1000;
// Whether to reclaim abandoned lock files. In CI each agent is an ephemeral
// container, so stale locks cannot carry across runs; combined with the wide
// scan spread, the few a timeout-killed task may leave are immaterial. An
// existing lock is simply treated as taken. Reclamation only earns its keep
// on a developer's machine, where /tmp persists across runs.
const RECLAIM_ABANDONED_LOCKS = !process.env.CI;
// Lock files held by THIS process. A single exit handler frees them all,
// rather than registering one listener per reservePort() call.
const heldLocks = new Set<string>();
process.once('exit', () => {
for (const lock of heldLocks) {
try {
fs.unlinkSync(lock);
} catch {}
}
});
function lockPath(port: number): string {
return path.join(LOCK_DIR, `${port}.lock`);
}
/**
* A lock is abandoned if its owning process is gone, or it is older than
* STALE_LOCK_MS. A SIGKILLed e2e process never runs its exit handler, so
* without this its locks would block those ports for every later run.
*/
function isAbandonedLock(lock: string): boolean {
let stat: fs.Stats;
let pid: number;
try {
stat = fs.statSync(lock);
pid = parseInt(fs.readFileSync(lock, 'utf8').trim(), 10);
} catch {
return true; // vanished between checks — treat as free
}
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) return true;
if (!pid) return false;
try {
process.kill(pid, 0); // signal 0: existence check, does not kill
return false; // owner still alive
} catch (err) {
// ESRCH: no such process → abandoned. EPERM: exists, not ours → alive.
return (err as NodeJS.ErrnoException).code === 'ESRCH';
}
}
/** Atomically claim the lock for `port`; returns true on success. */
function claimLock(port: number): boolean {
const lock = lockPath(port);
for (let attempt = 0; attempt < 2; attempt++) {
try {
fs.writeFileSync(lock, String(process.pid), { flag: 'wx' });
heldLocks.add(lock);
return true;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
}
// Lock exists. Outside CI, reclaim it once if abandoned then retry the
// claim; in CI an existing lock is simply treated as taken.
if (!RECLAIM_ABANDONED_LOCKS || !isAbandonedLock(lock)) return false;
try {
fs.unlinkSync(lock);
} catch {}
}
return false;
}
function releaseLock(port: number): void {
const lock = lockPath(port);
heldLocks.delete(lock);
try {
fs.unlinkSync(lock);
} catch {}
}
export async function reservePort(start = RANGE_FLOOR): Promise<number> {
// Randomise the scan origin so parallel e2e processes spread across the
// range instead of all converging on — and fighting over — the low ports.
// The scan wraps around so the whole [start, RANGE_CEILING) range is covered
// even when the origin lands high; otherwise the ports below it are skipped
// and we could throw spuriously while ports are still free.
const span = RANGE_CEILING - start;
const origin = Math.floor(Math.random() * Math.min(SCAN_SPREAD, span));
for (let i = 0; i < span; i++) {
const port = start + ((origin + i) % span);
if (!claimLock(port)) continue;
// Lock claimed; verify the OS port is actually free. Another e2e test on
// the same agent may be using it via a generator default (i.e. without
// participating in the lock scheme), so an exclusive lock is not enough.
if (await isPortAvailable(port)) {
return port;
}
releaseLock(port);
}
throw new Error('No available ports');
}
/**
* Reserves `count` *consecutive* ports and returns them in ascending order.
*
* The contiguity matters: module federation e2e tests pass `ports[0]` as the
* host's `--devServerPort`, and the generator then wires the host to its
* remotes at `ports[0] + 1`, `+ 2`, ... — so the reserved ports must be a
* contiguous run for the host and remotes to line up.
*/
export async function reservePorts(count: number): Promise<number[]> {
if (count <= 1) {
return count === 1 ? [await reservePort()] : [];
}
// Randomise the scan origin (same rationale as reservePort) and wrap around
// so every valid window start is tried, then return the first window of
// `count` consecutive ports that are all claimable and actually free.
const lastStart = RANGE_CEILING - count;
const numStarts = lastStart - RANGE_FLOOR + 1;
const origin = Math.floor(Math.random() * Math.min(SCAN_SPREAD, numStarts));
for (let i = 0; i < numStarts; i++) {
const start = RANGE_FLOOR + ((origin + i) % numStarts);
const claimed: number[] = [];
for (let port = start; port < start + count; port++) {
if (!claimLock(port)) break;
if (!(await isPortAvailable(port))) {
releaseLock(port);
break;
}
claimed.push(port);
}
if (claimed.length === count) {
return claimed;
}
// Window failed partway — release whatever we managed to claim in it.
for (const port of claimed) {
releaseLock(port);
}
}
throw new Error(`No available run of ${count} consecutive ports`);
}
/**
* @deprecated Use {@link reservePort} — probing the OS for port 0 opens a
* TOCTOU race across parallel e2e processes. Kept for backwards compatibility.
*/
export async function getAvailablePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on('error', reject);
server.listen(0, () => {
const addressInfo = server.address();
if (!addressInfo || typeof addressInfo === 'string') {
reject(new Error('Failed to get server address'));
return;
}
const port = addressInfo.port;
server.close(() => {
resolve(port);
});
});
});
}
/**
* @deprecated Use {@link reservePorts}.
*/
export async function getAvailablePorts(count: number): Promise<number[]> {
const ports: number[] = [];
for (let i = 0; i < count; i++) {
const port = await getAvailablePort();
ports.push(port);
}
return ports;
}
/**
* Checks if a specific port is available
*
* @param port - Port number to check
* @returns Promise<boolean> - True if port is available, false otherwise
*/
export async function isPortAvailable(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', (err: NodeJS.ErrnoException) => {
resolve(false);
});
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port);
});
}
+68
View File
@@ -0,0 +1,68 @@
import { promisify } from 'util';
import treeKill = require('tree-kill');
import { logError, logInfo, logSuccess } from './log-utils';
import { check as portCheck } from 'tcp-port-used';
export const kill = require('kill-port');
const KILL_PORT_DELAY = 5000;
export const promisifiedTreeKill: (
pid: number,
signal: string
) => Promise<void> = promisify(treeKill);
export async function killPort(port: number): Promise<boolean> {
let inUse: boolean;
try {
inUse = await portCheck(port);
} catch {
// tcp-port-used's check() rejects on any connect error other than
// ECONNREFUSED. A port whose process was just killed can reset the probe
// (ECONNRESET) instead of cleanly refusing it; treat "can't probe" as
// freed rather than letting it throw and fail the caller's teardown.
return true;
}
if (inUse) {
let killPortResult;
try {
logInfo(`Attempting to close port ${port}`);
killPortResult = await kill(port);
await new Promise<void>((resolve) =>
setTimeout(() => resolve(), KILL_PORT_DELAY)
);
if (await portCheck(port)) {
logError(`Port ${port} still open`, JSON.stringify(killPortResult));
} else {
logSuccess(`Port ${port} successfully closed`);
return true;
}
} catch {
logError(`Port ${port} closing failed`);
}
return false;
} else {
return true;
}
}
export async function killPorts(port?: number): Promise<boolean> {
return port
? await killPort(port)
: (await killPort(3333)) && (await killPort(4200));
}
export async function killProcessAndPorts(
pid: number | undefined,
...ports: number[]
): Promise<void> {
try {
if (pid) {
await promisifiedTreeKill(pid, 'SIGKILL');
}
for (const port of ports) {
await killPort(port);
}
} catch (err) {
expect(err).toBeFalsy();
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "e2e-utils",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/utils",
"projectType": "library",
"// targets": "to see all targets run: nx show project e2e-utils --web",
"targets": {}
}
+10
View File
@@ -0,0 +1,10 @@
export function stripIndents(
strings: TemplateStringsArray,
...values: any[]
): string {
return String.raw(strings, ...values)
.split('\n')
.map((line) => line.trim())
.join('\n')
.trim();
}
+89
View File
@@ -0,0 +1,89 @@
import { join } from 'node:path';
import {
getPackageManagerCommand,
runCLI,
runCLIAsync,
runCommand,
} from './command-utils';
import { uniq } from './create-project-utils';
import { tmpProjPath } from './create-project-utils';
import { readFile, fileExists, updateFile } from './file-utils';
type GeneratorsWithDefaultTests =
| '@nx/js:lib'
| '@nx/node:lib'
| '@nx/react:lib'
| '@nx/react:app'
| '@nx/next:app'
| '@nx/angular:app'
| '@nx/web:app';
/**
* Runs the pass in generator and then runs test on
* the generated project to make sure the default tests pass.
*/
export async function expectJestTestsToPass(
generator: GeneratorsWithDefaultTests | string
) {
const name = uniq('proj');
const generatedResults = runCLI(
`generate ${generator} ${name} --no-interactive`
);
expect(generatedResults).toContain(`jest.config.ts`);
const results = await runCLIAsync(`test ${name}`);
expect(results.combinedOutput).toContain('Test Suites: 1 passed, 1 total');
}
export function expectTestsPass(v: { stdout: string; stderr: string }) {
expect(v.stderr).toContain('Ran all test suites');
expect(v.stderr).not.toContain('fail');
}
// TODO(meeroslav): This is test specific, it should not be in the utils
export function expectNoAngularDevkit() {
const { list } = getPackageManagerCommand();
const result = runCommand(`${list} @angular-devkit/core`);
expect(result).not.toContain('@angular-devkit/core');
}
// TODO(meeroslav): This is test specific, it should not be in the utils
export function expectNoTsJestInJestConfig(appName: string) {
const candidates = [
tmpProjPath(join('apps', appName, 'jest.config.js')),
tmpProjPath(join('apps', appName, 'jest.config.ts')),
tmpProjPath(join('apps', appName, 'jest.config.cts')),
];
let jestConfig: string;
for (const c of candidates) {
if (fileExists(c)) {
jestConfig = readFile(c);
break;
}
}
if (!jestConfig) {
throw new Error(`Could not find jest config for app/lib: ${appName}`);
}
expect(jestConfig).not.toContain('ts-jest');
}
export function expectCodeIsFormatted() {
expect(() => runCLI(`format:check`)).not.toThrow();
}
export function setCypressWebServerTimeout(
cypressConfigPath: string,
timeout = 60_000
) {
const cypressConfig = readFile(cypressConfigPath);
updateFile(
cypressConfigPath,
cypressConfig.replace(
`nxE2EPreset(__filename, {`,
`nxE2EPreset(__filename, {
webServerConfig: {
timeout: ${timeout},
},`
)
);
}
+22
View File
@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"nx": {
"typecheckTargetName": null
}
}
+32
View File
@@ -0,0 +1,32 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "out-tsc",
"declaration": true,
"types": ["node", "jest"],
"composite": true,
"skipLibCheck": true,
"noImplicitAny": false,
"strict": false
},
"exclude": [
"out-tsc",
"**/*.spec.ts",
"**/*.test.ts",
"jest.config.ts",
"global-setup.ts",
"global-teardown.ts"
],
"include": ["**/*.ts", "**/*.js"],
"references": [
{
"path": "../../packages/angular/tsconfig.lib.json"
},
{
"path": "../../packages/devkit/tsconfig.lib.json"
},
{
"path": "../../packages/nx/tsconfig.lib.json"
}
]
}