diff --git a/.circleci/config.yml b/.circleci/config.yml index 93c9adfdcc..6aead28e41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -147,6 +147,11 @@ jobs: NX_E2E_RUN_CYPRESS: 'false' NX_VERBOSE_LOGGING: 'false' steps: + - run: + name: Configure git metadata (needed for lerna smoke tests) + command: | + git config --global user.email test@test.com + git config --global user.name "Test Test" - run: name: Set dynamic nx run variable command: | @@ -192,7 +197,7 @@ jobs: pids+=($!) (yarn nx affected --target=build --base=$NX_BASE --head=$NX_HEAD --parallel=3 && - npx nx affected --target=e2e --base=$NX_BASE --head=$NX_HEAD --exclude=e2e-storybook,e2e-storybook-angular,e2e-react-native,e2e-detox --parallel=1) & + npx nx affected --target=e2e --base=$NX_BASE --head=$NX_HEAD --exclude=e2e-storybook,e2e-storybook-angular --parallel=1) & pids+=($!) for pid in "${pids[@]}"; do @@ -213,27 +218,11 @@ jobs: echo "export NX_RUN_GROUP=\"run-group-macos-$CIRCLE_WORKFLOW_ID\";" >> $BASH_ENV - setup: os: macos - - nx/set-shas: - main-branch-name: 'master' - run: - name: Check if "detox" or "react-native" were modified directly + name: Run E2E Tests for macOS + # FIXME: remove --exclude=e2e-detox once we have a fix for the detox tests command: | - COUNT=`git diff --name-only $NX_BASE $NX_HEAD | (grep -E 'packages/detox|packages/react-native|e2e/detox|e2e/react-native' || true) | wc -l` - if [[ $COUNT -gt 0 ]]; then - echo "React Native and Detox were touched directly" - echo "export E2E_AFFECTED=true;" >> $BASH_ENV - else - echo "React Native and Detox were not touched directly" - echo "export E2E_AFFECTED=false;" >> $BASH_ENV - fi - - run: - name: Run E2E Tests - command: | - if $E2E_AFFECTED; then - npx nx affected --target=e2e --base=$NX_BASE --head=$NX_HEAD --exclude=e2e-make-angular-cli-faster,e2e-detox,e2e-js,e2e-next,e2e-workspace-create,e2e-nx-run,e2e-nx-misc,e2e-react,e2e-web,e2e-webpack,e2e-rollup,e2e-esbuild,e2e-angular-extensions,e2e-angular-core,e2e-nx-plugin,e2e-cypress,e2e-node,e2e-linter,e2e-jest,e2e-add-nx-to-monorepo,nx-dev-e2e,e2e-nx-init,e2e-graph-client,e2e-vite,e2e-cra-to-nx,e2e-storybook,e2e-storybook-angular --parallel=1; - else - echo "Skipping E2E tests"; - fi + npx nx run-many -t e2e-macos --parallel=1 --exclude=e2e-detox no_output_timeout: 45m # ------------------------- diff --git a/e2e/detox/project.json b/e2e/detox/project.json index a8ea73a670..87352a5825 100644 --- a/e2e/detox/project.json +++ b/e2e/detox/project.json @@ -4,7 +4,7 @@ "sourceRoot": "e2e/detox", "projectType": "application", "targets": { - "e2e": {}, + "e2e-macos": {}, "run-e2e-tests": {} }, "implicitDependencies": ["detox"] diff --git a/e2e/lerna-smoke-tests/jest.config.ts b/e2e/lerna-smoke-tests/jest.config.ts new file mode 100644 index 0000000000..5b15cb5a17 --- /dev/null +++ b/e2e/lerna-smoke-tests/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + maxWorkers: 1, + globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' } }, + displayName: 'e2e-lerna-smoke-tests', + preset: '../../jest.preset.js', +}; diff --git a/e2e/lerna-smoke-tests/project.json b/e2e/lerna-smoke-tests/project.json new file mode 100644 index 0000000000..be96e4daf6 --- /dev/null +++ b/e2e/lerna-smoke-tests/project.json @@ -0,0 +1,11 @@ +{ + "name": "e2e-lerna-smoke-tests", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/lerna-smoke-tests", + "projectType": "application", + "targets": { + "e2e": {}, + "run-e2e-tests": {} + }, + "implicitDependencies": ["nx", "devkit"] +} diff --git a/e2e/lerna-smoke-tests/src/lerna-smoke-tests.test.ts b/e2e/lerna-smoke-tests/src/lerna-smoke-tests.test.ts new file mode 100644 index 0000000000..760877077a --- /dev/null +++ b/e2e/lerna-smoke-tests/src/lerna-smoke-tests.test.ts @@ -0,0 +1,79 @@ +/** + * These minimal smoke tests are here to ensure that we do not break assumptions on the lerna side + * when making updates to nx or @nrwl/devkit. + */ + +import { + cleanupLernaWorkspace, + newLernaWorkspace, + runLernaCLI, + tmpProjPath, + updateJson, +} from '@nrwl/e2e/utils'; + +expect.addSnapshotSerializer({ + serialize(str: string) { + return ( + str + // Not all package managers print the package.json path in the output + .replace(tmpProjPath(), '') + .replace('/private', '') + .replace('/packages/package-1', '') + // We trim each line to reduce the chances of snapshot flakiness + .split('\n') + .map((r) => r.trim()) + .join('\n') + ); + }, + test(val: string) { + return val != null && typeof val === 'string'; + }, +}); + +describe('Lerna Smoke Tests', () => { + beforeAll(() => newLernaWorkspace()); + afterAll(() => cleanupLernaWorkspace()); + + // `lerna repair` builds on top of `nx repair` and runs all of its generators + describe('lerna repair', () => { + // If this snapshot fails it means that nx repair generators are making assumptions which don't hold true for lerna workspaces + it('should complete successfully on a new lerna workspace', async () => { + expect(runLernaCLI(`repair`)).toMatchInlineSnapshot(` + + > Lerna No changes were necessary. This workspace is up to date! + + + `); + }, 1000000); + }); + + // `lerna run` delegates to the nx task runner behind the scenes + describe('lerna run', () => { + it('should complete successfully on a new lerna workspace', async () => { + runLernaCLI('create package-1 -y'); + updateJson('packages/package-1/package.json', (json) => ({ + ...json, + scripts: { + ...(json.scripts || {}), + 'print-name': 'echo test-package-1', + }, + })); + + expect(runLernaCLI(`run print-name`)).toMatchInlineSnapshot(` + + > package-1:print-name + + > package-1@0.0.0 print-name + > echo test-package-1 + test-package-1 + + + + > Lerna (powered by Nx) Successfully ran target print-name for project package-1 + + + + `); + }, 1000000); + }); +}); diff --git a/e2e/lerna-smoke-tests/tsconfig.json b/e2e/lerna-smoke-tests/tsconfig.json new file mode 100644 index 0000000000..6d5abf8483 --- /dev/null +++ b/e2e/lerna-smoke-tests/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/e2e/lerna-smoke-tests/tsconfig.spec.json b/e2e/lerna-smoke-tests/tsconfig.spec.json new file mode 100644 index 0000000000..1a24bfb0a1 --- /dev/null +++ b/e2e/lerna-smoke-tests/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.d.ts", + "jest.config.ts" + ] +} diff --git a/e2e/react-native/project.json b/e2e/react-native/project.json index 0db879c557..d482495994 100644 --- a/e2e/react-native/project.json +++ b/e2e/react-native/project.json @@ -4,7 +4,7 @@ "sourceRoot": "e2e/react-native", "projectType": "application", "targets": { - "e2e": {}, + "e2e-macos": {}, "run-e2e-tests": {} }, "implicitDependencies": ["react-native"] diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index 259bdedbd4..28f8281c58 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -7,6 +7,7 @@ import { workspaceRoot, } from '@nrwl/devkit'; import { angularCliVersion } from '@nrwl/workspace/src/utils/versions'; +import { dump } from '@zkochan/js-yaml'; import { ChildProcess, exec, execSync, ExecSyncOptions } from 'child_process'; import { copySync, @@ -46,7 +47,8 @@ export function getPublishedVersion(): string { export function detectPackageManager(dir: string = ''): PackageManager { return existsSync(join(dir, 'yarn.lock')) ? 'yarn' - : existsSync(join(dir, 'pnpm-lock.yaml')) + : existsSync(join(dir, 'pnpm-lock.yaml')) || + existsSync(join(dir, 'pnpm-workspace.yaml')) ? 'pnpm' : 'npm'; } @@ -374,6 +376,106 @@ export function newProject({ } } +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 (process.env.NX_VERBOSE_LOGGING == 'true') { + 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, + }; + 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', + }); + + execSync(pm.install, { + cwd: tmpProjPath(), + stdio: isVerbose() ? 'inherit' : 'pipe', + env: { CI: 'true', ...process.env }, + encoding: 'utf-8', + }); + + return projScope; + } catch (e) { + logError(`Failed to set up lerna workspace for e2e tests.`, e.message); + throw e; + } +} + const KILL_PORT_DELAY = 5000; export async function killPort(port: number): Promise { @@ -416,6 +518,14 @@ export async function cleanupProject(opts?: RunCmdOpts) { } } +export function cleanupLernaWorkspace() { + if (isCI) { + try { + removeSync(tmpProjPath()); + } catch (e) {} + } +} + export function runCypressTests() { if (process.env.NX_E2E_RUN_CYPRESS === 'true') { ensureCypressInstallation(); @@ -629,6 +739,42 @@ export function runCLI( } } +export function runLernaCLI( + command: string, + opts: RunCmdOpts = { + silenceError: false, + env: undefined, + } +): string { + try { + const pm = getPackageManagerCommand(); + const logs = execSync(`${pm.runLerna} ${command}`, { + cwd: opts.cwd || tmpProjPath(), + env: { CI: 'true', ...(opts.env || getStrippedEnvironmentVariables()) }, + encoding: 'utf-8', + stdio: 'pipe', + maxBuffer: 50 * 1024 * 1024, + }); + const r = stripConsoleColors(logs); + + if (isVerbose()) { + console.log(logs); + } + + return r; + } catch (e) { + if (opts.silenceError) { + return stripConsoleColors(e.stdout?.toString() + e.stderr?.toString()); + } else { + logError( + `Original command: ${command}`, + `${e.stdout?.toString()}\n\n${e.stderr?.toString()}` + ); + throw e; + } + } +} + /** * Remove log colors for fail proof string search * @param log @@ -882,6 +1028,7 @@ export function getPackageManagerCommand({ addProd: string; addDev: string; list: string; + runLerna: string; } { const npmMajorVersion = getNpmMajorVersion(); const publishedVersion = getPublishedVersion(); @@ -899,6 +1046,7 @@ export function getPackageManagerCommand({ addProd: `npm install --legacy-peer-deps`, addDev: `npm install --legacy-peer-deps -D`, list: 'npm ls --depth 10', + runLerna: `npx lerna`, }, yarn: { // `yarn create nx-workspace` is failing due to wrong global path @@ -911,6 +1059,7 @@ export function getPackageManagerCommand({ addProd: `yarn add`, addDev: `yarn add -D`, list: 'npm ls --depth 10', + runLerna: `yarn lerna`, }, // Pnpm 3.5+ adds nx to pnpm: { @@ -923,6 +1072,7 @@ export function getPackageManagerCommand({ addProd: `pnpm add`, addDev: `pnpm add -D`, list: 'npm ls --depth 10', + runLerna: `pnpm exec lerna`, }, }[packageManager.trim() as PackageManager]; } @@ -932,6 +1082,11 @@ function getNpmMajorVersion(): string { return npmMajorVersion; } +function getLatestLernaVersion(): string { + const lernaVersion = execSync(`npm view lerna version`).toString().trim(); + return lernaVersion; +} + export const packageManagerLockFile = { npm: 'package-lock.json', yarn: 'yarn.lock', diff --git a/nx.json b/nx.json index 305c41a691..6bfec4a0a0 100644 --- a/nx.json +++ b/nx.json @@ -95,6 +95,39 @@ { "env": "SELECTED_CLI" }, + { + "env": "SELECTED_PM" + }, + { + "env": "NX_E2E_CI_CACHE_KEY" + } + ], + "options": { + "commands": [ + { + "command": "yarn e2e-start-local-registry" + }, + { + "command": "yarn e2e-build-package-publish" + }, + { + "command": "nx run-e2e-tests {projectName}" + } + ], + "parallel": false + } + }, + "e2e-macos": { + "executor": "nx:run-commands", + "inputs": [ + "default", + "^production", + { + "env": "SELECTED_CLI" + }, + { + "env": "SELECTED_PM" + }, { "env": "NX_E2E_CI_CACHE_KEY" }