From f9e7107f197fcac8939e7dd8b1b2f8b1b701b984 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Wed, 8 Oct 2025 12:36:22 +0100 Subject: [PATCH 01/11] chore(repo): split slow e2e tests - ng-add, cypress, eslint linter, and affected-graph Split the following test suites into smaller, more focused tests: - ng-add tests (cypress v9/v10, eslint, generate workspace, multiple projects) - cypress tests (component testing for Angular and Next, e2e execution, app generation) - eslint linter tests (dependency checks, flat config, linting errors, module boundaries, root configs) - affected-graph tests (affected star, focus/exclude, html/json output, show projects, git integration) --- e2e/angular/src/ng-add-cypress-v10.test.ts | 100 ++ e2e/angular/src/ng-add-cypress-v9.test.ts | 98 ++ e2e/angular/src/ng-add-eslint.test.ts | 53 + .../src/ng-add-generate-workspace.test.ts | 204 ++++ .../src/ng-add-multiple-projects.test.ts | 90 ++ e2e/angular/src/ng-add-setup.ts | 59 ++ e2e/angular/src/ng-add.test.ts | 486 --------- e2e/cypress/src/cypress-ct-angular.test.ts | 40 + e2e/cypress/src/cypress-ct-next.test.ts | 40 + e2e/cypress/src/cypress-execute-e2e.test.ts | 125 +++ e2e/cypress/src/cypress-generate-app.test.ts | 39 + e2e/cypress/src/cypress-setup.ts | 15 + e2e/cypress/src/cypress.test.ts | 204 ---- .../src/linter-dependency-checks.test.ts | 143 +++ e2e/eslint/src/linter-flat-config.test.ts | 71 ++ e2e/eslint/src/linter-linting-errors.test.ts | 399 +++++++ .../src/linter-module-boundaries.test.ts | 97 ++ e2e/eslint/src/linter-root-angular.test.ts | 105 ++ e2e/eslint/src/linter-root-node.test.ts | 110 ++ e2e/eslint/src/linter-root-react.test.ts | 108 ++ e2e/eslint/src/linter-setup.ts | 39 + .../src/linter-workspace-boundaries.test.ts | 229 ++++ e2e/eslint/src/linter.test.ts | 994 ------------------ .../src/affected-graph-affected-star.test.ts | 162 +++ ...affected-graph-graph-focus-exclude.test.ts | 138 +++ e2e/nx/src/affected-graph-graph-html.test.ts | 116 ++ e2e/nx/src/affected-graph-graph-json.test.ts | 153 +++ e2e/nx/src/affected-graph-setup.ts | 14 + .../src/affected-graph-show-projects.test.ts | 94 ++ e2e/nx/src/affected-graph-with-git.test.ts | 150 +++ e2e/nx/src/affected-graph.test.ts | 614 ----------- 31 files changed, 2991 insertions(+), 2298 deletions(-) create mode 100644 e2e/angular/src/ng-add-cypress-v10.test.ts create mode 100644 e2e/angular/src/ng-add-cypress-v9.test.ts create mode 100644 e2e/angular/src/ng-add-eslint.test.ts create mode 100644 e2e/angular/src/ng-add-generate-workspace.test.ts create mode 100644 e2e/angular/src/ng-add-multiple-projects.test.ts create mode 100644 e2e/angular/src/ng-add-setup.ts delete mode 100644 e2e/angular/src/ng-add.test.ts create mode 100644 e2e/cypress/src/cypress-ct-angular.test.ts create mode 100644 e2e/cypress/src/cypress-ct-next.test.ts create mode 100644 e2e/cypress/src/cypress-execute-e2e.test.ts create mode 100644 e2e/cypress/src/cypress-generate-app.test.ts create mode 100644 e2e/cypress/src/cypress-setup.ts delete mode 100644 e2e/cypress/src/cypress.test.ts create mode 100644 e2e/eslint/src/linter-dependency-checks.test.ts create mode 100644 e2e/eslint/src/linter-flat-config.test.ts create mode 100644 e2e/eslint/src/linter-linting-errors.test.ts create mode 100644 e2e/eslint/src/linter-module-boundaries.test.ts create mode 100644 e2e/eslint/src/linter-root-angular.test.ts create mode 100644 e2e/eslint/src/linter-root-node.test.ts create mode 100644 e2e/eslint/src/linter-root-react.test.ts create mode 100644 e2e/eslint/src/linter-setup.ts create mode 100644 e2e/eslint/src/linter-workspace-boundaries.test.ts delete mode 100644 e2e/eslint/src/linter.test.ts create mode 100644 e2e/nx/src/affected-graph-affected-star.test.ts create mode 100644 e2e/nx/src/affected-graph-graph-focus-exclude.test.ts create mode 100644 e2e/nx/src/affected-graph-graph-html.test.ts create mode 100644 e2e/nx/src/affected-graph-graph-json.test.ts create mode 100644 e2e/nx/src/affected-graph-setup.ts create mode 100644 e2e/nx/src/affected-graph-show-projects.test.ts create mode 100644 e2e/nx/src/affected-graph-with-git.test.ts delete mode 100644 e2e/nx/src/affected-graph.test.ts diff --git a/e2e/angular/src/ng-add-cypress-v10.test.ts b/e2e/angular/src/ng-add-cypress-v10.test.ts new file mode 100644 index 0000000000000..4f8800d20e306 --- /dev/null +++ b/e2e/angular/src/ng-add-cypress-v10.test.ts @@ -0,0 +1,100 @@ +import { + checkFilesDoNotExist, + checkFilesExist, + packageInstall, + readJson, + runCLI, + runNgAdd, +} from '@nx/e2e-utils'; +import { + setupNgAddTest, + cleanupNgAddTest, + NgAddTestContext, +} from './ng-add-setup'; + +function addCypress10(project: string) { + runNgAdd('@cypress/schematic', '--e2e', 'latest'); + // pin latest version of Cypress that's supported by Nx to avoid flakiness + // when a new major version is released + packageInstall('cypress', null, '^14.2.1'); +} + +describe('convert Angular CLI workspace to an Nx workspace', () => { + let context: NgAddTestContext; + + beforeEach(() => { + context = setupNgAddTest(); + }); + + afterEach(() => { + cleanupNgAddTest(); + }); + + it('should handle a workspace with cypress v10', () => { + const { project } = context; + addCypress10(project); + + runCLI('g @nx/angular:ng-add --skip-install'); + + const e2eProject = `${project}-e2e`; + //check e2e project files + checkFilesDoNotExist( + 'cypress.config.ts', + 'cypress/tsconfig.json', + 'cypress/e2e/spec.cy.ts', + 'cypress/fixtures/example.json', + 'cypress/support/commands.ts', + 'cypress/support/e2e.ts' + ); + checkFilesExist( + `apps/${e2eProject}/cypress.config.ts`, + `apps/${e2eProject}/tsconfig.json`, + `apps/${e2eProject}/src/e2e/spec.cy.ts`, + `apps/${e2eProject}/src/fixtures/example.json`, + `apps/${e2eProject}/src/support/commands.ts`, + `apps/${e2eProject}/src/support/e2e.ts` + ); + + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.targets['cypress-run']).toBeUndefined(); + expect(projectConfig.targets['cypress-open']).toBeUndefined(); + expect(projectConfig.targets.e2e).toBeUndefined(); + + // check e2e project config + const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); + expect(e2eProjectConfig.targets['cypress-run']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + cypressConfig: `apps/${e2eProject}/cypress.config.ts`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + expect(e2eProjectConfig.targets['cypress-open']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.config.ts`, + }, + }); + expect(e2eProjectConfig.targets.e2e).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.config.ts`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + }); +}); diff --git a/e2e/angular/src/ng-add-cypress-v9.test.ts b/e2e/angular/src/ng-add-cypress-v9.test.ts new file mode 100644 index 0000000000000..4d775ca5290a0 --- /dev/null +++ b/e2e/angular/src/ng-add-cypress-v9.test.ts @@ -0,0 +1,98 @@ +import { + checkFilesDoNotExist, + checkFilesExist, + packageInstall, + readJson, + runCLI, + runNgAdd, +} from '@nx/e2e-utils'; +import { + setupNgAddTest, + cleanupNgAddTest, + NgAddTestContext, +} from './ng-add-setup'; + +function addCypress9(project: string) { + runNgAdd('@cypress/schematic', '--e2e-update', '1.7.0'); + packageInstall('cypress', null, '^9.0.0'); +} + +describe('convert Angular CLI workspace to an Nx workspace', () => { + let context: NgAddTestContext; + + beforeEach(() => { + context = setupNgAddTest(); + }); + + afterEach(() => { + cleanupNgAddTest(); + }); + + it('should handle a workspace with cypress v9', () => { + const { project } = context; + addCypress9(project); + + runCLI('g @nx/angular:ng-add --skip-install'); + + const e2eProject = `${project}-e2e`; + //check e2e project files + checkFilesDoNotExist( + 'cypress.json', + 'cypress/tsconfig.json', + 'cypress/integration/spec.ts', + 'cypress/plugins/index.ts', + 'cypress/support/commands.ts', + 'cypress/support/index.ts' + ); + checkFilesExist( + `apps/${e2eProject}/cypress.json`, + `apps/${e2eProject}/tsconfig.json`, + `apps/${e2eProject}/src/integration/spec.ts`, + `apps/${e2eProject}/src/plugins/index.ts`, + `apps/${e2eProject}/src/support/commands.ts`, + `apps/${e2eProject}/src/support/index.ts` + ); + + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.targets['cypress-run']).toBeUndefined(); + expect(projectConfig.targets['cypress-open']).toBeUndefined(); + expect(projectConfig.targets.e2e).toBeUndefined(); + + // check e2e project config + const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); + expect(e2eProjectConfig.targets['cypress-run']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + cypressConfig: `apps/${e2eProject}/cypress.json`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + expect(e2eProjectConfig.targets['cypress-open']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.json`, + }, + }); + expect(e2eProjectConfig.targets.e2e).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.json`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + }); +}); diff --git a/e2e/angular/src/ng-add-eslint.test.ts b/e2e/angular/src/ng-add-eslint.test.ts new file mode 100644 index 0000000000000..4a95a88a57fec --- /dev/null +++ b/e2e/angular/src/ng-add-eslint.test.ts @@ -0,0 +1,53 @@ +import { checkFilesExist, readJson, runCLI, runNgAdd } from '@nx/e2e-utils'; +import { + setupNgAddTest, + cleanupNgAddTest, + NgAddTestContext, +} from './ng-add-setup'; + +function addEsLint(project: string) { + runNgAdd('@angular-eslint/schematics', undefined, 'latest'); +} + +describe('convert Angular CLI workspace to an Nx workspace', () => { + let context: NgAddTestContext; + + beforeEach(() => { + context = setupNgAddTest(); + }); + + afterEach(() => { + cleanupNgAddTest(); + }); + + // TODO(leo): The current Verdaccio setup fails to resolve older versions + // of @nx/* packages, the @angular-eslint/builder package depends on an + // older version of @nx/devkit so we skip this test for now. + it.skip('should handle a workspace with ESLint', () => { + const { project } = context; + addEsLint(project); + + runCLI('g @nx/angular:ng-add'); + + checkFilesExist(`apps/${project}/.eslintrc.json`, `.eslintrc.json`); + + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.targets.lint).toStrictEqual({ + executor: '@nx/eslint:lint', + }); + + let output = runCLI(`lint ${project}`); + expect(output).toContain(`> nx run ${project}:lint`); + expect(output).toContain('All files pass linting'); + expect(output).toContain( + `Successfully ran target lint for project ${project}` + ); + + output = runCLI(`lint ${project}`); + expect(output).toContain(`> nx run ${project}:lint [local cache]`); + expect(output).toContain('All files pass linting'); + expect(output).toContain( + `Successfully ran target lint for project ${project}` + ); + }); +}); diff --git a/e2e/angular/src/ng-add-generate-workspace.test.ts b/e2e/angular/src/ng-add-generate-workspace.test.ts new file mode 100644 index 0000000000000..f8cb19e2311e1 --- /dev/null +++ b/e2e/angular/src/ng-add-generate-workspace.test.ts @@ -0,0 +1,204 @@ +import { + checkFilesDoNotExist, + checkFilesExist, + packageInstall, + readJson, + runCLI, + runNgAdd, + updateFile, +} from '@nx/e2e-utils'; +import { + setupNgAddTest, + cleanupNgAddTest, + addProtractor, + NgAddTestContext, +} from './ng-add-setup'; + +describe('convert Angular CLI workspace to an Nx workspace', () => { + let context: NgAddTestContext; + + beforeEach(() => { + context = setupNgAddTest(); + }); + + afterEach(() => { + cleanupNgAddTest(); + }); + + it('should generate a workspace', () => { + const { project } = context; + addProtractor(project); + + // update package.json + const packageJson = readJson('package.json'); + packageJson.description = 'some description'; + updateFile('package.json', JSON.stringify(packageJson, null, 2)); + + // update tsconfig.json + const tsConfig = readJson('tsconfig.json'); + tsConfig.compilerOptions.paths = { a: ['b'] }; + updateFile('tsconfig.json', JSON.stringify(tsConfig, null, 2)); + + // add an extra script file + updateFile('src/scripts.js', 'const x = 1;'); + + // update angular.json + const angularJson = readJson('angular.json'); + angularJson.projects[project].architect.build.options.scripts = + angularJson.projects[project].architect.test.options.scripts = [ + 'src/scripts.js', + ]; + angularJson.projects[project].architect.test.options.styles = [ + 'src/styles.css', + ]; + updateFile('angular.json', JSON.stringify(angularJson, null, 2)); + + // confirm that @nx dependencies do not exist yet + expect(packageJson.devDependencies['@nx/workspace']).not.toBeDefined(); + + // run ng add + runCLI('g @nx/angular:ng-add --default-base main'); + + // check that prettier config exits and that files have been moved + checkFilesExist( + '.vscode/extensions.json', + '.prettierrc', + `apps/${project}/src/main.ts`, + `apps/${project}/src/app/app.config.ts`, + `apps/${project}/src/app/app.ts`, + `apps/${project}/src/app/app.routes.ts` + ); + + // check the right VSCode extensions are recommended + expect(readJson('.vscode/extensions.json').recommendations).toEqual([ + 'angular.ng-template', + 'nrwl.angular-console', + 'dbaeumer.vscode-eslint', + 'esbenp.prettier-vscode', + ]); + + // check package.json + const updatedPackageJson = readJson('package.json'); + expect(updatedPackageJson.description).toEqual('some description'); + expect(updatedPackageJson.scripts).toEqual({ + ng: 'ng', + start: 'nx serve', + build: 'nx build', + watch: 'nx build --watch --configuration development', + test: 'nx test', + }); + expect(updatedPackageJson.devDependencies['@nx/workspace']).toBeDefined(); + expect(updatedPackageJson.devDependencies['@angular/cli']).toBeDefined(); + + // check nx.json + const nxJson = readJson('nx.json'); + expect(nxJson).toEqual({ + defaultBase: 'main', + namedInputs: { + default: ['{projectRoot}/**/*', 'sharedGlobals'], + production: [ + 'default', + '!{projectRoot}/tsconfig.spec.json', + '!{projectRoot}/**/*.spec.[jt]s', + '!{projectRoot}/karma.conf.js', + ], + sharedGlobals: [], + }, + targetDefaults: { + build: { + dependsOn: ['^build'], + inputs: ['production', '^production'], + cache: true, + }, + e2e: { + inputs: ['default', '^production'], + cache: true, + }, + test: { + inputs: ['default', '^production', '{workspaceRoot}/karma.conf.js'], + cache: true, + }, + }, + }); + + // check angular.json does not exist + checkFilesDoNotExist('angular.json'); + + // check project configuration + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.sourceRoot).toEqual(`apps/${project}/src`); + expect(projectConfig.targets.build).toStrictEqual({ + executor: '@angular/build:application', + outputs: ['{options.outputPath}'], + options: { + outputPath: `dist/${project}`, + browser: `apps/${project}/src/main.ts`, + polyfills: [`zone.js`], + tsConfig: `apps/${project}/tsconfig.app.json`, + assets: [{ glob: '**/*', input: `apps/${project}/public` }], + styles: [`apps/${project}/src/styles.css`], + scripts: [`apps/${project}/src/scripts.js`], + }, + configurations: { + production: { + budgets: [ + { + type: 'initial', + maximumWarning: '500kB', + maximumError: '1MB', + }, + { + type: 'anyComponentStyle', + maximumWarning: '4kB', + maximumError: '8kB', + }, + ], + outputHashing: 'all', + }, + development: { + optimization: false, + extractLicenses: false, + sourceMap: true, + }, + }, + defaultConfiguration: 'production', + }); + expect(projectConfig.targets.serve).toEqual({ + executor: '@angular/build:dev-server', + configurations: { + production: { buildTarget: `${project}:build:production` }, + development: { buildTarget: `${project}:build:development` }, + }, + defaultConfiguration: 'development', + }); + expect(projectConfig.targets.test).toStrictEqual({ + executor: '@angular/build:karma', + options: { + polyfills: [`zone.js`, `zone.js/testing`], + tsConfig: `apps/${project}/tsconfig.spec.json`, + assets: [{ glob: '**/*', input: `apps/${project}/public` }], + styles: [`apps/${project}/src/styles.css`], + scripts: [`apps/${project}/src/scripts.js`], + }, + }); + expect(projectConfig.targets.e2e).toBeUndefined(); + + // check e2e project config + const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); + expect(e2eProjectConfig.targets.e2e).toEqual({ + executor: '@angular-devkit/build-angular:protractor', + options: { + protractorConfig: `apps/${project}-e2e/protractor.conf.js`, + devServerTarget: `${project}:serve`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + + runCLI(`build ${project} --configuration production --outputHashing none`); + checkFilesExist(`dist/${project}/browser/main.js`); + }); +}); diff --git a/e2e/angular/src/ng-add-multiple-projects.test.ts b/e2e/angular/src/ng-add-multiple-projects.test.ts new file mode 100644 index 0000000000000..404cc68ca2d2f --- /dev/null +++ b/e2e/angular/src/ng-add-multiple-projects.test.ts @@ -0,0 +1,90 @@ +import { + checkFilesDoNotExist, + checkFilesExist, + runCLI, + runCommand, + uniq, +} from '@nx/e2e-utils'; +import { + setupNgAddTest, + cleanupNgAddTest, + NgAddTestContext, +} from './ng-add-setup'; + +describe('convert Angular CLI workspace to an Nx workspace', () => { + let context: NgAddTestContext; + + beforeEach(() => { + context = setupNgAddTest(); + }); + + afterEach(() => { + cleanupNgAddTest(); + }); + + it('should support a workspace with multiple projects', () => { + const { project } = context; + // add other projects + const app1 = uniq('app1'); + const lib1 = uniq('lib1'); + runCommand(`ng g @schematics/angular:application ${app1} --no-interactive`); + runCommand(`ng g @schematics/angular:library ${lib1} --no-interactive`); + + runCLI('g @nx/angular:ng-add'); + + // check angular.json does not exist + checkFilesDoNotExist('angular.json'); + + // check building project + let output = runCLI(`build ${project} --outputHashing none`); + expect(output).toContain( + `> nx run ${project}:build:production --outputHashing none` + ); + expect(output).toContain( + `Successfully ran target build for project ${project}` + ); + checkFilesExist(`dist/${project}/browser/main.js`); + + output = runCLI(`build ${project} --outputHashing none`); + expect(output).toContain( + `> nx run ${project}:build:production --outputHashing none [local cache]` + ); + expect(output).toContain( + `Successfully ran target build for project ${project}` + ); + + // check building app1 + output = runCLI(`build ${app1} --outputHashing none`); + expect(output).toContain( + `> nx run ${app1}:build:production --outputHashing none` + ); + expect(output).toContain( + `Successfully ran target build for project ${app1}` + ); + checkFilesExist(`dist/${app1}/browser/main.js`); + + output = runCLI(`build ${app1} --outputHashing none`); + expect(output).toContain( + `> nx run ${app1}:build:production --outputHashing none [local cache]` + ); + expect(output).toContain( + `Successfully ran target build for project ${app1}` + ); + + // check building lib1 + output = runCLI(`build ${lib1}`); + expect(output).toContain(`> nx run ${lib1}:build:production`); + expect(output).toContain( + `Successfully ran target build for project ${lib1}` + ); + checkFilesExist(`dist/${lib1}/package.json`); + + output = runCLI(`build ${lib1}`); + expect(output).toContain( + `> nx run ${lib1}:build:production [local cache]` + ); + expect(output).toContain( + `Successfully ran target build for project ${lib1}` + ); + }); +}); diff --git a/e2e/angular/src/ng-add-setup.ts b/e2e/angular/src/ng-add-setup.ts new file mode 100644 index 0000000000000..93ab92a0c84d0 --- /dev/null +++ b/e2e/angular/src/ng-add-setup.ts @@ -0,0 +1,59 @@ +import { + cleanupProject, + getSelectedPackageManager, + packageInstall, + readJson, + runNgNew, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { PackageManager } from 'nx/src/utils/package-manager'; + +export interface NgAddTestContext { + project: string; + packageManager: PackageManager; +} + +export function setupNgAddTest(): NgAddTestContext { + let packageManager = getSelectedPackageManager(); + // TODO: solve issues with pnpm and remove this fallback + packageManager = packageManager === 'pnpm' ? 'yarn' : packageManager; + const project = runNgNew(packageManager); + packageInstall('nx', null, 'latest'); + packageInstall('@nx/angular', null, 'latest'); + + return { project, packageManager }; +} + +export function cleanupNgAddTest() { + cleanupProject(); +} + +export function addProtractor(project: string) { + updateFile('e2e/protractor.conf.js', 'exports.config = {};'); + updateFile( + 'e2e/tsconfig.json', + JSON.stringify({ extends: '../tsconfig.json' }, null, 2) + ); + updateFile( + 'e2e/src/app.e2e-spec.ts', + `describe('app', () => { + it('should pass', () => { + expect(true).toBe(true); + }); + });` + ); + + const angularJson = readJson('angular.json'); + angularJson.projects[project].architect.e2e = { + builder: '@angular-devkit/build-angular:protractor', + options: { + protractorConfig: 'e2e/protractor.conf.js', + devServerTarget: `${project}:serve`, + }, + configurations: { + production: { devServerTarget: `${project}:serve:production` }, + }, + }; + updateFile('angular.json', JSON.stringify(angularJson, null, 2)); +} diff --git a/e2e/angular/src/ng-add.test.ts b/e2e/angular/src/ng-add.test.ts deleted file mode 100644 index c867764c875d7..0000000000000 --- a/e2e/angular/src/ng-add.test.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { - checkFilesDoNotExist, - checkFilesExist, - cleanupProject, - getSelectedPackageManager, - packageInstall, - readJson, - runCLI, - runCommand, - runNgAdd, - runNgNew, - uniq, - updateFile, -} from '@nx/e2e-utils'; -import { PackageManager } from 'nx/src/utils/package-manager'; - -describe('convert Angular CLI workspace to an Nx workspace', () => { - let project: string; - let packageManager: PackageManager; - - // utility to manually add protractor since it's not generated - // in the latest Angular CLI versions, but older projects updated - // to latest versions might still have it - function addProtractor() { - updateFile('e2e/protractor.conf.js', 'exports.config = {};'); - updateFile( - 'e2e/tsconfig.json', - JSON.stringify({ extends: '../tsconfig.json' }, null, 2) - ); - updateFile( - 'e2e/src/app.e2e-spec.ts', - `describe('app', () => { - it('should pass', () => { - expect(true).toBe(true); - }); - });` - ); - - const angularJson = readJson('angular.json'); - angularJson.projects[project].architect.e2e = { - builder: '@angular-devkit/build-angular:protractor', - options: { - protractorConfig: 'e2e/protractor.conf.js', - devServerTarget: `${project}:serve`, - }, - configurations: { - production: { devServerTarget: `${project}:serve:production` }, - }, - }; - updateFile('angular.json', JSON.stringify(angularJson, null, 2)); - } - - function addCypress9() { - runNgAdd('@cypress/schematic', '--e2e-update', '1.7.0'); - packageInstall('cypress', null, '^9.0.0'); - } - - function addCypress10() { - runNgAdd('@cypress/schematic', '--e2e', 'latest'); - // pin latest version of Cypress that's supported by Nx to avoid flakiness - // when a new major version is released - packageInstall('cypress', null, '^14.2.1'); - } - - function addEsLint() { - runNgAdd('@angular-eslint/schematics', undefined, 'latest'); - } - - beforeEach(() => { - packageManager = getSelectedPackageManager(); - // TODO: solve issues with pnpm and remove this fallback - packageManager = packageManager === 'pnpm' ? 'yarn' : packageManager; - project = runNgNew(packageManager); - packageInstall('nx', null, 'latest'); - packageInstall('@nx/angular', null, 'latest'); - }); - - afterEach(() => { - cleanupProject(); - }); - - it('should generate a workspace', () => { - addProtractor(); - - // update package.json - const packageJson = readJson('package.json'); - packageJson.description = 'some description'; - updateFile('package.json', JSON.stringify(packageJson, null, 2)); - - // update tsconfig.json - const tsConfig = readJson('tsconfig.json'); - tsConfig.compilerOptions.paths = { a: ['b'] }; - updateFile('tsconfig.json', JSON.stringify(tsConfig, null, 2)); - - // add an extra script file - updateFile('src/scripts.js', 'const x = 1;'); - - // update angular.json - const angularJson = readJson('angular.json'); - angularJson.projects[project].architect.build.options.scripts = - angularJson.projects[project].architect.test.options.scripts = [ - 'src/scripts.js', - ]; - angularJson.projects[project].architect.test.options.styles = [ - 'src/styles.css', - ]; - updateFile('angular.json', JSON.stringify(angularJson, null, 2)); - - // confirm that @nx dependencies do not exist yet - expect(packageJson.devDependencies['@nx/workspace']).not.toBeDefined(); - - // run ng add - runCLI('g @nx/angular:ng-add --default-base main'); - - // check that prettier config exits and that files have been moved - checkFilesExist( - '.vscode/extensions.json', - '.prettierrc', - `apps/${project}/src/main.ts`, - `apps/${project}/src/app/app.config.ts`, - `apps/${project}/src/app/app.ts`, - `apps/${project}/src/app/app.routes.ts` - ); - - // check the right VSCode extensions are recommended - expect(readJson('.vscode/extensions.json').recommendations).toEqual([ - 'angular.ng-template', - 'nrwl.angular-console', - 'dbaeumer.vscode-eslint', - 'esbenp.prettier-vscode', - ]); - - // check package.json - const updatedPackageJson = readJson('package.json'); - expect(updatedPackageJson.description).toEqual('some description'); - expect(updatedPackageJson.scripts).toEqual({ - ng: 'ng', - start: 'nx serve', - build: 'nx build', - watch: 'nx build --watch --configuration development', - test: 'nx test', - }); - expect(updatedPackageJson.devDependencies['@nx/workspace']).toBeDefined(); - expect(updatedPackageJson.devDependencies['@angular/cli']).toBeDefined(); - - // check nx.json - const nxJson = readJson('nx.json'); - expect(nxJson).toEqual({ - defaultBase: 'main', - namedInputs: { - default: ['{projectRoot}/**/*', 'sharedGlobals'], - production: [ - 'default', - '!{projectRoot}/tsconfig.spec.json', - '!{projectRoot}/**/*.spec.[jt]s', - '!{projectRoot}/karma.conf.js', - ], - sharedGlobals: [], - }, - targetDefaults: { - build: { - dependsOn: ['^build'], - inputs: ['production', '^production'], - cache: true, - }, - e2e: { - inputs: ['default', '^production'], - cache: true, - }, - test: { - inputs: ['default', '^production', '{workspaceRoot}/karma.conf.js'], - cache: true, - }, - }, - }); - - // check angular.json does not exist - checkFilesDoNotExist('angular.json'); - - // check project configuration - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.sourceRoot).toEqual(`apps/${project}/src`); - expect(projectConfig.targets.build).toStrictEqual({ - executor: '@angular/build:application', - outputs: ['{options.outputPath}'], - options: { - outputPath: `dist/${project}`, - browser: `apps/${project}/src/main.ts`, - polyfills: [`zone.js`], - tsConfig: `apps/${project}/tsconfig.app.json`, - assets: [{ glob: '**/*', input: `apps/${project}/public` }], - styles: [`apps/${project}/src/styles.css`], - scripts: [`apps/${project}/src/scripts.js`], - }, - configurations: { - production: { - budgets: [ - { - type: 'initial', - maximumWarning: '500kB', - maximumError: '1MB', - }, - { - type: 'anyComponentStyle', - maximumWarning: '4kB', - maximumError: '8kB', - }, - ], - outputHashing: 'all', - }, - development: { - optimization: false, - extractLicenses: false, - sourceMap: true, - }, - }, - defaultConfiguration: 'production', - }); - expect(projectConfig.targets.serve).toEqual({ - executor: '@angular/build:dev-server', - configurations: { - production: { buildTarget: `${project}:build:production` }, - development: { buildTarget: `${project}:build:development` }, - }, - defaultConfiguration: 'development', - }); - expect(projectConfig.targets.test).toStrictEqual({ - executor: '@angular/build:karma', - options: { - polyfills: [`zone.js`, `zone.js/testing`], - tsConfig: `apps/${project}/tsconfig.spec.json`, - assets: [{ glob: '**/*', input: `apps/${project}/public` }], - styles: [`apps/${project}/src/styles.css`], - scripts: [`apps/${project}/src/scripts.js`], - }, - }); - expect(projectConfig.targets.e2e).toBeUndefined(); - - // check e2e project config - const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); - expect(e2eProjectConfig.targets.e2e).toEqual({ - executor: '@angular-devkit/build-angular:protractor', - options: { - protractorConfig: `apps/${project}-e2e/protractor.conf.js`, - devServerTarget: `${project}:serve`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - - runCLI(`build ${project} --configuration production --outputHashing none`); - checkFilesExist(`dist/${project}/browser/main.js`); - }); - - it('should handle a workspace with cypress v9', () => { - addCypress9(); - - runCLI('g @nx/angular:ng-add --skip-install'); - - const e2eProject = `${project}-e2e`; - //check e2e project files - checkFilesDoNotExist( - 'cypress.json', - 'cypress/tsconfig.json', - 'cypress/integration/spec.ts', - 'cypress/plugins/index.ts', - 'cypress/support/commands.ts', - 'cypress/support/index.ts' - ); - checkFilesExist( - `apps/${e2eProject}/cypress.json`, - `apps/${e2eProject}/tsconfig.json`, - `apps/${e2eProject}/src/integration/spec.ts`, - `apps/${e2eProject}/src/plugins/index.ts`, - `apps/${e2eProject}/src/support/commands.ts`, - `apps/${e2eProject}/src/support/index.ts` - ); - - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.targets['cypress-run']).toBeUndefined(); - expect(projectConfig.targets['cypress-open']).toBeUndefined(); - expect(projectConfig.targets.e2e).toBeUndefined(); - - // check e2e project config - const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); - expect(e2eProjectConfig.targets['cypress-run']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - cypressConfig: `apps/${e2eProject}/cypress.json`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - expect(e2eProjectConfig.targets['cypress-open']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.json`, - }, - }); - expect(e2eProjectConfig.targets.e2e).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.json`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - }); - - it('should handle a workspace with cypress v10', () => { - addCypress10(); - - runCLI('g @nx/angular:ng-add --skip-install'); - - const e2eProject = `${project}-e2e`; - //check e2e project files - checkFilesDoNotExist( - 'cypress.config.ts', - 'cypress/tsconfig.json', - 'cypress/e2e/spec.cy.ts', - 'cypress/fixtures/example.json', - 'cypress/support/commands.ts', - 'cypress/support/e2e.ts' - ); - checkFilesExist( - `apps/${e2eProject}/cypress.config.ts`, - `apps/${e2eProject}/tsconfig.json`, - `apps/${e2eProject}/src/e2e/spec.cy.ts`, - `apps/${e2eProject}/src/fixtures/example.json`, - `apps/${e2eProject}/src/support/commands.ts`, - `apps/${e2eProject}/src/support/e2e.ts` - ); - - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.targets['cypress-run']).toBeUndefined(); - expect(projectConfig.targets['cypress-open']).toBeUndefined(); - expect(projectConfig.targets.e2e).toBeUndefined(); - - // check e2e project config - const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); - expect(e2eProjectConfig.targets['cypress-run']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - cypressConfig: `apps/${e2eProject}/cypress.config.ts`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - expect(e2eProjectConfig.targets['cypress-open']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.config.ts`, - }, - }); - expect(e2eProjectConfig.targets.e2e).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.config.ts`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - }); - - // TODO(leo): The current Verdaccio setup fails to resolve older versions - // of @nx/* packages, the @angular-eslint/builder package depends on an - // older version of @nx/devkit so we skip this test for now. - it.skip('should handle a workspace with ESLint', () => { - addEsLint(); - - runCLI('g @nx/angular:ng-add'); - - checkFilesExist(`apps/${project}/.eslintrc.json`, `.eslintrc.json`); - - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.targets.lint).toStrictEqual({ - executor: '@nx/eslint:lint', - }); - - let output = runCLI(`lint ${project}`); - expect(output).toContain(`> nx run ${project}:lint`); - expect(output).toContain('All files pass linting'); - expect(output).toContain( - `Successfully ran target lint for project ${project}` - ); - - output = runCLI(`lint ${project}`); - expect(output).toContain(`> nx run ${project}:lint [local cache]`); - expect(output).toContain('All files pass linting'); - expect(output).toContain( - `Successfully ran target lint for project ${project}` - ); - }); - - it('should support a workspace with multiple projects', () => { - // add other projects - const app1 = uniq('app1'); - const lib1 = uniq('lib1'); - runCommand(`ng g @schematics/angular:application ${app1} --no-interactive`); - runCommand(`ng g @schematics/angular:library ${lib1} --no-interactive`); - - runCLI('g @nx/angular:ng-add'); - - // check angular.json does not exist - checkFilesDoNotExist('angular.json'); - - // check building project - let output = runCLI(`build ${project} --outputHashing none`); - expect(output).toContain( - `> nx run ${project}:build:production --outputHashing none` - ); - expect(output).toContain( - `Successfully ran target build for project ${project}` - ); - checkFilesExist(`dist/${project}/browser/main.js`); - - output = runCLI(`build ${project} --outputHashing none`); - expect(output).toContain( - `> nx run ${project}:build:production --outputHashing none [local cache]` - ); - expect(output).toContain( - `Successfully ran target build for project ${project}` - ); - - // check building app1 - output = runCLI(`build ${app1} --outputHashing none`); - expect(output).toContain( - `> nx run ${app1}:build:production --outputHashing none` - ); - expect(output).toContain( - `Successfully ran target build for project ${app1}` - ); - checkFilesExist(`dist/${app1}/browser/main.js`); - - output = runCLI(`build ${app1} --outputHashing none`); - expect(output).toContain( - `> nx run ${app1}:build:production --outputHashing none [local cache]` - ); - expect(output).toContain( - `Successfully ran target build for project ${app1}` - ); - - // check building lib1 - output = runCLI(`build ${lib1}`); - expect(output).toContain(`> nx run ${lib1}:build:production`); - expect(output).toContain( - `Successfully ran target build for project ${lib1}` - ); - checkFilesExist(`dist/${lib1}/package.json`); - - output = runCLI(`build ${lib1}`); - expect(output).toContain( - `> nx run ${lib1}:build:production [local cache]` - ); - expect(output).toContain( - `Successfully ran target build for project ${lib1}` - ); - }); -}); diff --git a/e2e/cypress/src/cypress-ct-angular.test.ts b/e2e/cypress/src/cypress-ct-angular.test.ts new file mode 100644 index 0000000000000..64c6157599ad8 --- /dev/null +++ b/e2e/cypress/src/cypress-ct-angular.test.ts @@ -0,0 +1,40 @@ +import { killPort, runCLI, runE2ETests, uniq } from '@nx/e2e-utils'; +import { setupCypressTest, cleanupCypressTest } from './cypress-setup'; + +const TEN_MINS_MS = 600_000; + +describe('Cypress E2E Test runner', () => { + beforeAll(() => { + setupCypressTest(); + }); + + afterAll(() => cleanupCypressTest()); + + it( + `should allow CT and e2e in same project for an angular project`, + async () => { + let appName = uniq(`angular-cy-app`); + runCLI( + `generate @nx/angular:app apps/${appName} --e2eTestRunner=none --no-interactive --bundler=webpack` + ); + runCLI( + `generate @nx/angular:component apps/${appName}/src/app/btn/btn --no-interactive` + ); + runCLI( + `generate @nx/angular:cypress-component-configuration --project=${appName} --generate-tests --no-interactive` + ); + runCLI( + `generate @nx/cypress:e2e --project=${appName} --baseUrl=http://localhost:4200 --no-interactive` + ); + + if (runE2ETests('cypress')) { + expect(runCLI(`run ${appName}:component-test`)).toContain( + 'All specs passed!' + ); + expect(runCLI(`run ${appName}:e2e`)).toContain('All specs passed!'); + } + expect(await killPort(4200)).toBeTruthy(); + }, + TEN_MINS_MS + ); +}); diff --git a/e2e/cypress/src/cypress-ct-next.test.ts b/e2e/cypress/src/cypress-ct-next.test.ts new file mode 100644 index 0000000000000..b5df8a80f444a --- /dev/null +++ b/e2e/cypress/src/cypress-ct-next.test.ts @@ -0,0 +1,40 @@ +import { killPort, runCLI, runE2ETests, uniq } from '@nx/e2e-utils'; +import { setupCypressTest, cleanupCypressTest } from './cypress-setup'; + +const TEN_MINS_MS = 600_000; + +describe('Cypress E2E Test runner', () => { + beforeAll(() => { + setupCypressTest(); + }); + + afterAll(() => cleanupCypressTest()); + + it( + `should allow CT and e2e in same project for a next project`, + async () => { + const appName = uniq('next-cy-app'); + runCLI( + `generate @nx/next:app apps/${appName} --e2eTestRunner=none --no-interactive ` + ); + runCLI( + `generate @nx/next:component apps/${appName}/components/btn --no-interactive` + ); + runCLI( + `generate @nx/next:cypress-component-configuration --project=${appName} --generate-tests --no-interactive` + ); + runCLI( + `generate @nx/cypress:configuration --project=${appName} --devServerTarget=${appName}:dev --baseUrl=http://localhost:3000 --no-interactive` + ); + + if (runE2ETests('cypress')) { + expect(runCLI(`run ${appName}:component-test`)).toContain( + 'All specs passed!' + ); + expect(runCLI(`run ${appName}:e2e`)).toContain('All specs passed!'); + } + expect(await killPort(4200)).toBeTruthy(); + }, + TEN_MINS_MS + ); +}); diff --git a/e2e/cypress/src/cypress-execute-e2e.test.ts b/e2e/cypress/src/cypress-execute-e2e.test.ts new file mode 100644 index 0000000000000..74111071d903d --- /dev/null +++ b/e2e/cypress/src/cypress-execute-e2e.test.ts @@ -0,0 +1,125 @@ +import { + checkFilesExist, + createFile, + readJson, + runCLI, + runE2ETests, + updateFile, +} from '@nx/e2e-utils'; +import { setupCypressTest, cleanupCypressTest } from './cypress-setup'; + +const TEN_MINS_MS = 600_000; + +describe('Cypress E2E Test runner', () => { + let myapp: string; + + beforeAll(() => { + const context = setupCypressTest(); + myapp = context.myapp; + runCLI( + `generate @nx/react:app apps/${myapp} --e2eTestRunner=cypress --linter=eslint` + ); + }); + + afterAll(() => cleanupCypressTest()); + + it( + 'should execute e2e tests using Cypress', + async () => { + // make sure env vars work + createFile( + `apps/${myapp}-e2e/cypress.env.json`, + ` +{ + "cypressEnvJson": "i am from the cypress.env.json file" +}` + ); + + createFile( + `apps/${myapp}-e2e/src/e2e/env.cy.ts`, + ` +describe('env vars', () => { + it('should have cli args', () => { + assert.equal(Cypress.env('cliArg'), 'i am from the cli args'); + }); + + it('should have cypress.env.json vars', () => { + assert.equal( + Cypress.env('cypressEnvJson'), + 'i am from the cypress.env.json file' + ); + }); +});` + ); + + if (runE2ETests('cypress')) { + // contains the correct output and works + const run1 = runCLI( + `e2e ${myapp}-e2e --config \\'{\\"env\\":{\\"cliArg\\":\\"i am from the cli args\\"}}\\'` + ); + expect(run1).toContain('All specs passed!'); + // tests should not fail because of a config change + updateFile( + `apps/${myapp}-e2e/cypress.config.ts`, + ` +import { defineConfig } from 'cypress'; +import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; + +export default defineConfig({ + e2e: { + ...nxE2EPreset(__filename, { + cypressDir: 'src', + webServerCommands: { + default: 'nx run ${myapp}:serve', + production: 'nx run ${myapp}:preview', + }, + ciWebServerCommand: 'nx run ${myapp}:serve-static', + webServerConfig: { + timeout: 60_000, + }, + }), + baseUrl: 'http://localhost:4200', + }, + env: { + fromCyConfig: 'i am from the cypress config file' + } +});` + ); + + const run2 = runCLI( + `e2e ${myapp}-e2e --config \\'{\\"env\\":{\\"cliArg\\":\\"i am from the cli args\\"}}\\'` + ); + expect(run2).toContain('All specs passed!'); + + // make sure project.json env vars also work + checkFilesExist(`apps/${myapp}-e2e/src/e2e/env.cy.ts`); + updateFile( + `apps/${myapp}-e2e/src/e2e/env.cy.ts`, + ` + describe('env vars', () => { + it('should not have cli args', () => { + assert.equal(Cypress.env('cliArg'), undefined); + }); + + it('should have cypress.env.json vars', () => { + assert.equal( + Cypress.env('cypressEnvJson'), + 'i am from the cypress.env.json file' + ); + }); + + it('should have cypress config vars', () => { + assert.equal( + Cypress.env('fromCyConfig'), + 'i am from the cypress config file' + ); + }); + });` + ); + const run3 = runCLI(`e2e ${myapp}-e2e`); + expect(run3).toContain('All specs passed!'); + } + }, + TEN_MINS_MS + ); +}); diff --git a/e2e/cypress/src/cypress-generate-app.test.ts b/e2e/cypress/src/cypress-generate-app.test.ts new file mode 100644 index 0000000000000..22c36f437fb3b --- /dev/null +++ b/e2e/cypress/src/cypress-generate-app.test.ts @@ -0,0 +1,39 @@ +import { checkFilesExist, readJson, runCLI } from '@nx/e2e-utils'; +import { setupCypressTest, cleanupCypressTest } from './cypress-setup'; + +const TEN_MINS_MS = 600_000; + +describe('Cypress E2E Test runner', () => { + let myapp: string; + + beforeAll(() => { + const context = setupCypressTest(); + myapp = context.myapp; + }); + + afterAll(() => cleanupCypressTest()); + + it( + 'should generate an app with the Cypress as e2e test runner', + () => { + runCLI( + `generate @nx/react:app apps/${myapp} --e2eTestRunner=cypress --linter=eslint` + ); + + // Making sure the package.json file contains the Cypress dependency + const packageJson = readJson('package.json'); + expect(packageJson.devDependencies['cypress']).toBeTruthy(); + + // Making sure the cypress folders & files are created + checkFilesExist(`apps/${myapp}-e2e/cypress.config.ts`); + checkFilesExist(`apps/${myapp}-e2e/tsconfig.json`); + + checkFilesExist(`apps/${myapp}-e2e/src/fixtures/example.json`); + checkFilesExist(`apps/${myapp}-e2e/src/e2e/app.cy.ts`); + checkFilesExist(`apps/${myapp}-e2e/src/support/app.po.ts`); + checkFilesExist(`apps/${myapp}-e2e/src/support/e2e.ts`); + checkFilesExist(`apps/${myapp}-e2e/src/support/commands.ts`); + }, + TEN_MINS_MS + ); +}); diff --git a/e2e/cypress/src/cypress-setup.ts b/e2e/cypress/src/cypress-setup.ts new file mode 100644 index 0000000000000..95db9ffd68fda --- /dev/null +++ b/e2e/cypress/src/cypress-setup.ts @@ -0,0 +1,15 @@ +import { cleanupProject, newProject, uniq } from '@nx/e2e-utils'; + +export interface CypressTestContext { + myapp: string; +} + +export function setupCypressTest(): CypressTestContext { + const myapp = uniq('myapp'); + newProject({ packages: ['@nx/angular', '@nx/next', '@nx/react'] }); + return { myapp }; +} + +export function cleanupCypressTest() { + cleanupProject(); +} diff --git a/e2e/cypress/src/cypress.test.ts b/e2e/cypress/src/cypress.test.ts deleted file mode 100644 index 261898c6f6532..0000000000000 --- a/e2e/cypress/src/cypress.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { - checkFilesExist, - cleanupProject, - createFile, - killPort, - newProject, - readJson, - runCLI, - runE2ETests, - uniq, - updateFile, -} from '@nx/e2e-utils'; - -const TEN_MINS_MS = 600_000; - -describe('Cypress E2E Test runner', () => { - const myapp = uniq('myapp'); - - beforeAll(() => { - newProject({ packages: ['@nx/angular', '@nx/next', '@nx/react'] }); - }); - - afterAll(() => cleanupProject()); - - it( - 'should generate an app with the Cypress as e2e test runner', - () => { - runCLI( - `generate @nx/react:app apps/${myapp} --e2eTestRunner=cypress --linter=eslint` - ); - - // Making sure the package.json file contains the Cypress dependency - const packageJson = readJson('package.json'); - expect(packageJson.devDependencies['cypress']).toBeTruthy(); - - // Making sure the cypress folders & files are created - checkFilesExist(`apps/${myapp}-e2e/cypress.config.ts`); - checkFilesExist(`apps/${myapp}-e2e/tsconfig.json`); - - checkFilesExist(`apps/${myapp}-e2e/src/fixtures/example.json`); - checkFilesExist(`apps/${myapp}-e2e/src/e2e/app.cy.ts`); - checkFilesExist(`apps/${myapp}-e2e/src/support/app.po.ts`); - checkFilesExist(`apps/${myapp}-e2e/src/support/e2e.ts`); - checkFilesExist(`apps/${myapp}-e2e/src/support/commands.ts`); - }, - TEN_MINS_MS - ); - - it( - 'should execute e2e tests using Cypress', - async () => { - // make sure env vars work - createFile( - `apps/${myapp}-e2e/cypress.env.json`, - ` -{ - "cypressEnvJson": "i am from the cypress.env.json file" -}` - ); - - createFile( - `apps/${myapp}-e2e/src/e2e/env.cy.ts`, - ` -describe('env vars', () => { - it('should have cli args', () => { - assert.equal(Cypress.env('cliArg'), 'i am from the cli args'); - }); - - it('should have cypress.env.json vars', () => { - assert.equal( - Cypress.env('cypressEnvJson'), - 'i am from the cypress.env.json file' - ); - }); -});` - ); - - if (runE2ETests('cypress')) { - // contains the correct output and works - const run1 = runCLI( - `e2e ${myapp}-e2e --config \\'{\\"env\\":{\\"cliArg\\":\\"i am from the cli args\\"}}\\'` - ); - expect(run1).toContain('All specs passed!'); - // tests should not fail because of a config change - updateFile( - `apps/${myapp}-e2e/cypress.config.ts`, - ` -import { defineConfig } from 'cypress'; -import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; - -export default defineConfig({ - e2e: { - ...nxE2EPreset(__filename, { - cypressDir: 'src', - webServerCommands: { - default: 'nx run ${myapp}:serve', - production: 'nx run ${myapp}:preview', - }, - ciWebServerCommand: 'nx run ${myapp}:serve-static', - webServerConfig: { - timeout: 60_000, - }, - }), - baseUrl: 'http://localhost:4200', - }, - env: { - fromCyConfig: 'i am from the cypress config file' - } -});` - ); - - const run2 = runCLI( - `e2e ${myapp}-e2e --config \\'{\\"env\\":{\\"cliArg\\":\\"i am from the cli args\\"}}\\'` - ); - expect(run2).toContain('All specs passed!'); - - // make sure project.json env vars also work - checkFilesExist(`apps/${myapp}-e2e/src/e2e/env.cy.ts`); - updateFile( - `apps/${myapp}-e2e/src/e2e/env.cy.ts`, - ` - describe('env vars', () => { - it('should not have cli args', () => { - assert.equal(Cypress.env('cliArg'), undefined); - }); - - it('should have cypress.env.json vars', () => { - assert.equal( - Cypress.env('cypressEnvJson'), - 'i am from the cypress.env.json file' - ); - }); - - it('should have cypress config vars', () => { - assert.equal( - Cypress.env('fromCyConfig'), - 'i am from the cypress config file' - ); - }); - });` - ); - const run3 = runCLI(`e2e ${myapp}-e2e`); - expect(run3).toContain('All specs passed!'); - } - }, - TEN_MINS_MS - ); - - it( - `should allow CT and e2e in same project for a next project`, - async () => { - const appName = uniq('next-cy-app'); - runCLI( - `generate @nx/next:app apps/${appName} --e2eTestRunner=none --no-interactive ` - ); - runCLI( - `generate @nx/next:component apps/${appName}/components/btn --no-interactive` - ); - runCLI( - `generate @nx/next:cypress-component-configuration --project=${appName} --generate-tests --no-interactive` - ); - runCLI( - `generate @nx/cypress:configuration --project=${appName} --devServerTarget=${appName}:dev --baseUrl=http://localhost:3000 --no-interactive` - ); - - if (runE2ETests('cypress')) { - expect(runCLI(`run ${appName}:component-test`)).toContain( - 'All specs passed!' - ); - expect(runCLI(`run ${appName}:e2e`)).toContain('All specs passed!'); - } - expect(await killPort(4200)).toBeTruthy(); - }, - TEN_MINS_MS - ); - - it( - `should allow CT and e2e in same project for an angular project`, - async () => { - let appName = uniq(`angular-cy-app`); - runCLI( - `generate @nx/angular:app apps/${appName} --e2eTestRunner=none --no-interactive --bundler=webpack` - ); - runCLI( - `generate @nx/angular:component apps/${appName}/src/app/btn/btn --no-interactive` - ); - runCLI( - `generate @nx/angular:cypress-component-configuration --project=${appName} --generate-tests --no-interactive` - ); - runCLI( - `generate @nx/cypress:e2e --project=${appName} --baseUrl=http://localhost:4200 --no-interactive` - ); - - if (runE2ETests('cypress')) { - expect(runCLI(`run ${appName}:component-test`)).toContain( - 'All specs passed!' - ); - expect(runCLI(`run ${appName}:e2e`)).toContain('All specs passed!'); - } - expect(await killPort(4200)).toBeTruthy(); - }, - TEN_MINS_MS - ); -}); diff --git a/e2e/eslint/src/linter-dependency-checks.test.ts b/e2e/eslint/src/linter-dependency-checks.test.ts new file mode 100644 index 0000000000000..fb50ef9792435 --- /dev/null +++ b/e2e/eslint/src/linter-dependency-checks.test.ts @@ -0,0 +1,143 @@ +import { readJson, runCLI, updateFile, updateJson } from '@nx/e2e-utils'; +import { + setupLinterIntegratedTest, + cleanupLinterIntegratedTest, + LinterIntegratedTestContext, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Integrated', () => { + let context: LinterIntegratedTestContext; + + beforeAll(() => { + context = setupLinterIntegratedTest(); + }); + afterAll(() => cleanupLinterIntegratedTest()); + + describe('dependency checks', () => { + beforeAll(() => { + const { mylib } = context; + updateJson(`libs/${mylib}/.eslintrc.json`, (json) => { + if (!json.overrides.some((o) => o.rules?.['@nx/dependency-checks'])) { + json.overrides = [ + ...json.overrides, + { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + '@nx/dependency-checks': 'error', + }, + }, + ]; + } + return json; + }); + }); + + afterAll(() => { + const { mylib } = context; + // ensure the rule for dependency checks is removed + // so that it does not affect other tests + updateJson(`libs/${mylib}/.eslintrc.json`, (json) => { + json.overrides = json.overrides.filter( + (o) => !o.rules?.['@nx/dependency-checks'] + ); + return json; + }); + }); + + it('should report dependency check issues', () => { + const { mylib } = context; + const rootPackageJson = readJson('package.json'); + const nxVersion = rootPackageJson.devDependencies.nx; + const tslibVersion = + rootPackageJson.dependencies['tslib'] || + rootPackageJson.devDependencies['tslib']; + + let out = runCLI(`lint ${mylib}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain('Successfully ran target lint'); + + // make an explict dependency to nx + updateFile( + `libs/${mylib}/src/lib/${mylib}.ts`, + (content) => + `import { names } from '@nx/devkit';\n\n` + + content.replace(/=> .*;/, `=> names('${mylib}').className;`) + ); + // intentionally set an obsolete dependency + updateJson(`libs/${mylib}/package.json`, (json) => { + json.dependencies['@nx/js'] = nxVersion; + return json; + }); + + // output should now report missing dependency and obsolete dependency + out = runCLI(`lint ${mylib}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain('they are missing'); + expect(out).toContain('@nx/devkit'); + expect(out).toContain( + `The "@nx/js" package is not used by "${mylib}" project` + ); + + // should fix the missing and obsolete dependency issues + out = runCLI(`lint ${mylib} --fix`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain( + `Successfully ran target lint for project ${mylib}` + ); + const packageJson = readJson(`libs/${mylib}/package.json`); + expect(packageJson).toMatchObject({ + dependencies: { + '@nx/devkit': nxVersion, + tslib: tslibVersion, + }, + main: './src/index.js', + name: `@proj/${mylib}`, + private: true, + type: 'commonjs', + types: './src/index.d.ts', + version: '0.0.1', + }); + + // intentionally set the invalid version + updateJson(`libs/${mylib}/package.json`, (json) => { + json.dependencies['@nx/devkit'] = '100.0.0'; + return json; + }); + out = runCLI(`lint ${mylib}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain( + 'version specifier does not contain the installed version of "@nx/devkit"' + ); + + // should fix the version mismatch issue + out = runCLI(`lint ${mylib} --fix`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain( + `Successfully ran target lint for project ${mylib}` + ); + }); + }); + }); +}); diff --git a/e2e/eslint/src/linter-flat-config.test.ts b/e2e/eslint/src/linter-flat-config.test.ts new file mode 100644 index 0000000000000..834c95520594e --- /dev/null +++ b/e2e/eslint/src/linter-flat-config.test.ts @@ -0,0 +1,71 @@ +import { + checkFilesDoNotExist, + checkFilesExist, + runCLI, + uniq, +} from '@nx/e2e-utils'; +import { + setupLinterIntegratedTest, + cleanupLinterIntegratedTest, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Integrated', () => { + beforeAll(() => { + setupLinterIntegratedTest(); + }); + afterAll(() => cleanupLinterIntegratedTest()); + + describe('flat config', () => { + let envVar: string | undefined; + beforeAll(() => { + runCLI(`generate @nx/eslint:convert-to-flat-config`); + envVar = process.env.ESLINT_USE_FLAT_CONFIG; + // Now that we have converted the existing configs to flat config we need to clear the explicitly set env var to allow it to infer things from the root config file type + delete process.env.ESLINT_USE_FLAT_CONFIG; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = envVar; + }); + + it('should generate new projects using flat config', () => { + const reactLib = uniq('react-lib'); + const jsLib = uniq('js-lib'); + + runCLI(`generate @nx/react:lib ${reactLib} --linter eslint`); + runCLI(`generate @nx/js:lib ${jsLib} --linter eslint`); + + checkFilesExist( + `${reactLib}/eslint.config.mjs`, + `${jsLib}/eslint.config.mjs` + ); + checkFilesDoNotExist( + `${reactLib}/.eslintrc.json`, + `${jsLib}/.eslintrc.json` + ); + + // validate that the new projects are linted successfully + expect(() => + runCLI(`lint ${reactLib}`, { + env: { CI: 'false' }, + }) + ).not.toThrow(); + expect(() => + runCLI(`lint ${jsLib}`, { + env: { CI: 'false' }, + }) + ).not.toThrow(); + }); + }); + }); +}); diff --git a/e2e/eslint/src/linter-linting-errors.test.ts b/e2e/eslint/src/linter-linting-errors.test.ts new file mode 100644 index 0000000000000..23440e26aaf4e --- /dev/null +++ b/e2e/eslint/src/linter-linting-errors.test.ts @@ -0,0 +1,399 @@ +import * as path from 'path'; +import { + checkFilesExist, + readFile, + readJson, + runCLI, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { + setupLinterIntegratedTest, + cleanupLinterIntegratedTest, + LinterIntegratedTestContext, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Integrated', () => { + let context: LinterIntegratedTestContext; + + beforeAll(() => { + context = setupLinterIntegratedTest(); + }); + afterAll(() => cleanupLinterIntegratedTest()); + + describe('linting errors', () => { + let defaultEslintrc; + + beforeAll(() => { + const { myapp } = context; + updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`); + defaultEslintrc = readJson('.eslintrc.json'); + }); + afterEach(() => { + updateFile('.eslintrc.json', JSON.stringify(defaultEslintrc, null, 2)); + }); + + it('should check for linting errors', () => { + const { myapp } = context; + // create faulty file + updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`); + const eslintrc = readJson('.eslintrc.json'); + + // set the eslint rules to error + eslintrc.overrides.forEach((override) => { + if (override.files.includes('*.ts')) { + override.rules['no-console'] = 'error'; + } + }); + updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); + + let out = runCLI(`lint ${myapp}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain('Unexpected console statement'); + + eslintrc.overrides.forEach((override) => { + if (override.files.includes('*.ts')) { + override.rules['no-console'] = undefined; + } + }); + updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); + + // 3. linting should not error when all rules are followed + out = runCLI(`lint ${myapp}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain('Successfully ran target lint'); + }, 1000000); + + it('should cache eslint with --cache', () => { + const { myapp } = context; + function readCacheFile(cacheFile) { + const cacheInfo = readFile(cacheFile); + return process.platform === 'win32' + ? cacheInfo.replace(/\\\\/g, '\\') + : cacheInfo; + } + + // should generate a default cache file + let cachePath = path.join('apps', myapp, '.eslintcache'); + expect(() => checkFilesExist(cachePath)).toThrow(); + runCLI(`lint ${myapp} --cache`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(() => checkFilesExist(cachePath)).not.toThrow(); + expect(readCacheFile(cachePath)).toContain( + path.normalize(`${myapp}/src/app/app.spec.tsx`) + ); + + // should let you specify a cache file location + cachePath = path.join('apps', myapp, 'my-cache'); + expect(() => checkFilesExist(cachePath)).toThrow(); + runCLI(`lint ${myapp} --cache --cache-location="my-cache"`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(() => checkFilesExist(cachePath)).not.toThrow(); + expect(readCacheFile(cachePath)).toContain( + path.normalize(`${myapp}/src/app/app.spec.tsx`) + ); + }); + + it('linting should generate an output file with a specific format', () => { + const { myapp } = context; + const eslintrc = readJson('.eslintrc.json'); + eslintrc.overrides.forEach((override) => { + if (override.files.includes('*.ts')) { + override.rules['no-console'] = 'error'; + } + }); + updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); + + const outputFile = 'a/b/c/lint-output.json'; + const outputFilePath = path.join('apps', myapp, outputFile); + expect(() => { + checkFilesExist(outputFilePath); + }).toThrow(); + const stdout = runCLI( + `lint ${myapp} --output-file="${outputFile}" --format=json`, + { + silenceError: true, + env: { CI: 'false' }, + } + ); + expect(stdout).not.toContain('Unexpected console statement'); + expect(() => checkFilesExist(outputFilePath)).not.toThrow(); + const outputContents = JSON.parse(readFile(outputFilePath)); + const outputForApp: any = Object.values(outputContents).filter( + (result: any) => + result.filePath.includes(path.normalize(`${myapp}/src/main.ts`)) + )[0]; + expect(outputForApp.errorCount).toBe(1); + expect(outputForApp.messages[0].ruleId).toBe('no-console'); + expect(outputForApp.messages[0].message).toBe( + 'Unexpected console statement.' + ); + }, 1000000); + + it('should support creating, testing and using workspace lint rules', () => { + const { myapp, mylib, projScope } = context; + const messageId = 'e2eMessageId'; + const libMethodName = 'getMessageId'; + + // add custom function + updateFile( + `libs/${mylib}/src/lib/${mylib}.ts`, + `export const ${libMethodName} = (): '${messageId}' => '${messageId}';` + ); + + // Generate a new rule (should also scaffold the required workspace project and tests) + const newRuleName = 'e2e-test-rule-name'; + runCLI(`generate @nx/eslint:workspace-rule ${newRuleName}`); + + // TODO(@AgentEnder): This reset gets rid of a lockfile changed error... we should fix this in another way + runCLI(`reset`, { + env: { CI: 'false' }, + }); + + // Ensure that the unit tests for the new rule are runnable + expect(() => + runCLI(`test eslint-rules`, { + env: { CI: 'false' }, + }) + ).not.toThrow(); + + // Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used + const knownLintErrorMessage = 'e2e test known error message'; + const newRulePath = `tools/eslint-rules/rules/${newRuleName}.ts`; + const newRuleGeneratedContents = readFile(newRulePath); + const updatedRuleContents = updateGeneratedRuleImplementation( + newRulePath, + newRuleGeneratedContents, + knownLintErrorMessage, + messageId, + libMethodName, + `@${projScope}/${mylib}` + ); + updateFile(newRulePath, updatedRuleContents); + + const newRuleNameForUsage = `@nx/workspace-${newRuleName}`; + + // Add the new workspace rule to the lint config and run linting + const eslintrc = readJson('.eslintrc.json'); + eslintrc.overrides.forEach((override) => { + if (override.files.includes('*.ts')) { + override.rules[newRuleNameForUsage] = 'error'; + } + }); + updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); + + const lintOutput = runCLI(`lint ${myapp} --verbose`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(lintOutput).toContain(newRuleNameForUsage); + expect(lintOutput).toContain(knownLintErrorMessage); + }, 1000000); + }); + }); +}); + +/** + * Update the generated rule implementation to produce a known lint error from all files. + * + * It is important that we do this surgically via AST transformations, otherwise we will + * drift further and further away from the original generated code and therefore make our + * e2e test less accurate and less valuable. + */ +import * as ts from 'typescript'; + +function updateGeneratedRuleImplementation( + newRulePath: string, + newRuleGeneratedContents: string, + knownLintErrorMessage: string, + messageId, + libMethodName: string, + libPath: string +): string { + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const newRuleSourceFile = ts.createSourceFile( + newRulePath, + newRuleGeneratedContents, + ts.ScriptTarget.Latest, + true + ); + + const transformer = + (context: ts.TransformationContext) => + (rootNode: T) => { + function visit(node: ts.Node): ts.Node { + /** + * Add an ESLint messageId which will show the knownLintErrorMessage + * + * i.e. + * + * messages: { + * e2eMessageId: knownLintErrorMessage + * } + */ + if ( + ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && + node.name.escapedText === 'messages' + ) { + return ts.factory.updatePropertyAssignment( + node, + node.name, + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + messageId, + ts.factory.createStringLiteral(knownLintErrorMessage) + ), + ]) + ); + } + + /** + * Update the rule implementation to report the knownLintErrorMessage on every Program node + * + * During the debugging of the switch from ts-node to swc-node we found out + * that regular rules would work even without explicit path mapping registration, + * but rules that import runtime functionality from within the workspace + * would break the rule registration. + * + * Instead of having a static literal messageId we retreieved it via imported getMessageId method. + * + * i.e. + * + * create(context) { + * return { + * Program(node) { + * context.report({ + * messageId: getMessageId(), + * node, + * }); + * } + * } + * } + */ + if ( + ts.isMethodDeclaration(node) && + ts.isIdentifier(node.name) && + node.name.escapedText === 'create' + ) { + return ts.factory.updateMethodDeclaration( + node, + node.modifiers, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + ts.factory.createBlock([ + ts.factory.createReturnStatement( + ts.factory.createObjectLiteralExpression([ + ts.factory.createMethodDeclaration( + [], + undefined, + 'Program', + undefined, + [], + [ + ts.factory.createParameterDeclaration( + [], + undefined, + 'node', + undefined, + undefined, + undefined + ), + ], + undefined, + ts.factory.createBlock([ + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('context'), + 'report' + ), + [], + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + 'messageId', + ts.factory.createCallExpression( + ts.factory.createIdentifier(libMethodName), + [], + [] + ) + ), + ts.factory.createShorthandPropertyAssignment( + 'node' + ), + ]), + ] + ) + ), + ]) + ), + ]) + ), + ]) + ); + } + + return ts.visitEachChild(node, visit, context); + } + + /** + * Add lib import as a first line of the rule file. + * Needed for the access of getMessageId in the context report above. + * + * i.e. + * + * import { getMessageId } from "@myproj/mylib"; + * + */ + const importAdded = ts.factory.updateSourceFile(rootNode, [ + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(libMethodName) + ), + ]) + ), + ts.factory.createStringLiteral(libPath) + ), + ...rootNode.statements, + ]); + return ts.visitNode(importAdded, visit); + }; + + const result: ts.TransformationResult = + ts.transform(newRuleSourceFile, [transformer]); + const updatedSourceFile: ts.SourceFile = result.transformed[0]; + + return printer.printFile(updatedSourceFile); +} diff --git a/e2e/eslint/src/linter-module-boundaries.test.ts b/e2e/eslint/src/linter-module-boundaries.test.ts new file mode 100644 index 0000000000000..5aadf78a3a4a9 --- /dev/null +++ b/e2e/eslint/src/linter-module-boundaries.test.ts @@ -0,0 +1,97 @@ +import { readJson, runCLI, uniq, updateFile } from '@nx/e2e-utils'; +import { + setupLinterIntegratedTest, + cleanupLinterIntegratedTest, + LinterIntegratedTestContext, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Integrated', () => { + let context: LinterIntegratedTestContext; + + beforeAll(() => { + context = setupLinterIntegratedTest(); + }); + afterAll(() => cleanupLinterIntegratedTest()); + + describe('linting errors', () => { + it('lint plugin should ensure module boundaries', () => { + const { myapp, projScope } = context; + const myapp2 = uniq('myapp2'); + const lazylib = uniq('lazylib'); + const invalidtaglib = uniq('invalidtaglib'); + const validtaglib = uniq('validtaglib'); + + runCLI(`generate @nx/react:app apps/${myapp2} --linter eslint`); + runCLI(`generate @nx/react:lib libs/${lazylib} --linter eslint`); + runCLI( + `generate @nx/js:lib libs/${invalidtaglib} --linter eslint --tags=invalidtag` + ); + runCLI( + `generate @nx/js:lib libs/${validtaglib} --linter eslint --tags=validtag` + ); + + const eslint = readJson('.eslintrc.json'); + eslint.overrides[0].rules[ + '@nx/enforce-module-boundaries' + ][1].depConstraints = [ + { sourceTag: 'validtag', onlyDependOnLibsWithTags: ['validtag'] }, + ...eslint.overrides[0].rules['@nx/enforce-module-boundaries'][1] + .depConstraints, + ]; + updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2)); + + const tsConfig = readJson('tsconfig.base.json'); + + /** + * apps do not add themselves to the tsconfig file. + * + * Let's add it so that we can trigger the lint failure + */ + tsConfig.compilerOptions.paths[`@${projScope}/${myapp2}`] = [ + `apps/${myapp2}/src/main.ts`, + ]; + + tsConfig.compilerOptions.paths[`@secondScope/${lazylib}`] = + tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`]; + delete tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`]; + updateFile('tsconfig.base.json', JSON.stringify(tsConfig, null, 2)); + + updateFile( + `apps/${myapp}/src/main.ts`, + ` + import '../../../libs/${context.mylib}'; + import '@secondScope/${lazylib}'; + import '@${projScope}/${myapp2}'; + import '@${projScope}/${invalidtaglib}'; + import '@${projScope}/${validtaglib}'; + + const s = {loadChildren: '@secondScope/${lazylib}'}; + ` + ); + + const out = runCLI(`lint ${myapp}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(out).toContain( + 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' + ); + expect(out).toContain('Imports of apps are forbidden'); + expect(out).toContain( + 'A project tagged with "validtag" can only depend on libs tagged with "validtag"' + ); + }, 1000000); + }); + }); +}); diff --git a/e2e/eslint/src/linter-root-angular.test.ts b/e2e/eslint/src/linter-root-angular.test.ts new file mode 100644 index 0000000000000..e29d5aedb8009 --- /dev/null +++ b/e2e/eslint/src/linter-root-angular.test.ts @@ -0,0 +1,105 @@ +import { checkFilesExist, readJson, runCLI, uniq } from '@nx/e2e-utils'; +import { + setupLinterRootProjectsTest, + cleanupLinterRootProjectsTest, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Root projects migration', () => { + beforeEach(() => setupLinterRootProjectsTest()); + afterEach(() => cleanupLinterRootProjectsTest()); + + function verifySuccessfulStandaloneSetup(myapp: string) { + expect( + runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow(); + + const rootEslint = readJson('.eslintrc.json'); + const e2eEslint = readJson('e2e/.eslintrc.json'); + + // should directly refer to nx plugin + expect(rootEslint.plugins).toEqual(['@nx']); + expect(e2eEslint.plugins).toEqual(['@nx']); + } + + function verifySuccessfulMigratedSetup(myapp: string, mylib: string) { + expect( + runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint ${mylib}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow(); + + const rootEslint = readJson('.eslintrc.base.json'); + const appEslint = readJson('.eslintrc.json'); + const e2eEslint = readJson('e2e/.eslintrc.json'); + const libEslint = readJson(`libs/${mylib}/.eslintrc.json`); + + // should directly refer to nx plugin + expect(rootEslint.plugins).toEqual(['@nx']); + expect(appEslint.plugins).toBeUndefined(); + expect(e2eEslint.plugins).toBeUndefined(); + expect(libEslint.plugins).toBeUndefined(); + + // should extend base + expect(appEslint.extends.slice(-1)).toEqual(['./.eslintrc.base.json']); + expect(e2eEslint.extends.slice(-1)).toEqual(['../.eslintrc.base.json']); + expect(libEslint.extends.slice(-1)).toEqual([ + '../../.eslintrc.base.json', + ]); + } + + it('(Angular standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { + const myapp = uniq('myapp'); + const mylib = uniq('mylib'); + + runCLI( + `generate @nx/angular:app --name=${myapp} --directory="." --linter eslint --no-interactive` + ); + runCLI('reset', { env: { CI: 'false' } }); + verifySuccessfulStandaloneSetup(myapp); + + let appEslint = readJson('.eslintrc.json'); + let e2eEslint = readJson('e2e/.eslintrc.json'); + + // should have plugin extends + let appOverrides = JSON.stringify(appEslint.overrides); + expect(appOverrides).toContain('plugin:@nx/typescript'); + let e2eOverrides = JSON.stringify(e2eEslint.overrides); + expect(e2eOverrides).toContain('plugin:@nx/javascript'); + + runCLI( + `generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive` + ); + runCLI('reset', { env: { CI: 'false' } }); + verifySuccessfulMigratedSetup(myapp, mylib); + + appEslint = readJson(`.eslintrc.json`); + e2eEslint = readJson('e2e/.eslintrc.json'); + + // should have no plugin extends + appOverrides = JSON.stringify(appEslint.overrides); + expect(appOverrides).not.toContain('plugin:@nx/typescript'); + e2eOverrides = JSON.stringify(e2eEslint.overrides); + expect(e2eOverrides).not.toContain('plugin:@nx/typescript'); + }); + }); +}); diff --git a/e2e/eslint/src/linter-root-node.test.ts b/e2e/eslint/src/linter-root-node.test.ts new file mode 100644 index 0000000000000..936c232c097e2 --- /dev/null +++ b/e2e/eslint/src/linter-root-node.test.ts @@ -0,0 +1,110 @@ +import { checkFilesExist, readJson, runCLI, uniq } from '@nx/e2e-utils'; +import { + setupLinterRootProjectsTest, + cleanupLinterRootProjectsTest, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Root projects migration', () => { + beforeEach(() => setupLinterRootProjectsTest()); + afterEach(() => cleanupLinterRootProjectsTest()); + + function verifySuccessfulStandaloneSetup(myapp: string) { + expect( + runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow(); + + const rootEslint = readJson('.eslintrc.json'); + const e2eEslint = readJson('e2e/.eslintrc.json'); + + // should directly refer to nx plugin + expect(rootEslint.plugins).toEqual(['@nx']); + expect(e2eEslint.plugins).toEqual(['@nx']); + } + + function verifySuccessfulMigratedSetup(myapp: string, mylib: string) { + expect( + runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint ${mylib}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow(); + + const rootEslint = readJson('.eslintrc.base.json'); + const appEslint = readJson('.eslintrc.json'); + const e2eEslint = readJson('e2e/.eslintrc.json'); + const libEslint = readJson(`libs/${mylib}/.eslintrc.json`); + + // should directly refer to nx plugin + expect(rootEslint.plugins).toEqual(['@nx']); + expect(appEslint.plugins).toBeUndefined(); + expect(e2eEslint.plugins).toBeUndefined(); + expect(libEslint.plugins).toBeUndefined(); + + // should extend base + expect(appEslint.extends.slice(-1)).toEqual(['./.eslintrc.base.json']); + expect(e2eEslint.extends.slice(-1)).toEqual(['../.eslintrc.base.json']); + expect(libEslint.extends.slice(-1)).toEqual([ + '../../.eslintrc.base.json', + ]); + } + + it('(Node standalone) should set root project config to app and e2e app and migrate when another lib is added', async () => { + const myapp = uniq('myapp'); + const mylib = uniq('mylib'); + + runCLI( + `generate @nx/node:app --name=${myapp} --linter=eslint --directory="." --unitTestRunner=jest --e2eTestRunner=jest --no-interactive` + ); + runCLI('reset', { env: { CI: 'false' } }); + verifySuccessfulStandaloneSetup(myapp); + + let appEslint = readJson('.eslintrc.json'); + let e2eEslint = readJson('e2e/.eslintrc.json'); + + // should have plugin extends + let appOverrides = JSON.stringify(appEslint.overrides); + expect(appOverrides).toContain('plugin:@nx/javascript'); + expect(appOverrides).toContain('plugin:@nx/typescript'); + let e2eOverrides = JSON.stringify(e2eEslint.overrides); + expect(e2eOverrides).toContain('plugin:@nx/javascript'); + expect(e2eOverrides).toContain('plugin:@nx/typescript'); + + runCLI( + `generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive` + ); + runCLI('reset', { env: { CI: 'false' } }); + verifySuccessfulMigratedSetup(myapp, mylib); + + appEslint = readJson(`.eslintrc.json`); + e2eEslint = readJson('e2e/.eslintrc.json'); + + // should have no plugin extends + // should have no plugin extends + appOverrides = JSON.stringify(appEslint.overrides); + expect(appOverrides).not.toContain('plugin:@nx/javascript'); + expect(appOverrides).not.toContain('plugin:@nx/typescript'); + e2eOverrides = JSON.stringify(e2eEslint.overrides); + expect(e2eOverrides).not.toContain('plugin:@nx/javascript'); + expect(e2eOverrides).not.toContain('plugin:@nx/typescript'); + }); + }); +}); diff --git a/e2e/eslint/src/linter-root-react.test.ts b/e2e/eslint/src/linter-root-react.test.ts new file mode 100644 index 0000000000000..b20227f3b86af --- /dev/null +++ b/e2e/eslint/src/linter-root-react.test.ts @@ -0,0 +1,108 @@ +import { checkFilesExist, readJson, runCLI, uniq } from '@nx/e2e-utils'; +import { + setupLinterRootProjectsTest, + cleanupLinterRootProjectsTest, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Root projects migration', () => { + beforeEach(() => setupLinterRootProjectsTest()); + afterEach(() => cleanupLinterRootProjectsTest()); + + function verifySuccessfulStandaloneSetup(myapp: string) { + expect( + runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow(); + + const rootEslint = readJson('.eslintrc.json'); + const e2eEslint = readJson('e2e/.eslintrc.json'); + + // should directly refer to nx plugin + expect(rootEslint.plugins).toEqual(['@nx']); + expect(e2eEslint.plugins).toEqual(['@nx']); + } + + function verifySuccessfulMigratedSetup(myapp: string, mylib: string) { + expect( + runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect( + runCLI(`lint ${mylib}`, { silenceError: true, env: { CI: 'false' } }) + ).toContain('Successfully ran target lint'); + expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow(); + + const rootEslint = readJson('.eslintrc.base.json'); + const appEslint = readJson('.eslintrc.json'); + const e2eEslint = readJson('e2e/.eslintrc.json'); + const libEslint = readJson(`libs/${mylib}/.eslintrc.json`); + + // should directly refer to nx plugin + expect(rootEslint.plugins).toEqual(['@nx']); + expect(appEslint.plugins).toBeUndefined(); + expect(e2eEslint.plugins).toBeUndefined(); + expect(libEslint.plugins).toBeUndefined(); + + // should extend base + expect(appEslint.extends.slice(-1)).toEqual(['./.eslintrc.base.json']); + expect(e2eEslint.extends.slice(-1)).toEqual(['../.eslintrc.base.json']); + expect(libEslint.extends.slice(-1)).toEqual([ + '../../.eslintrc.base.json', + ]); + } + + it('(React standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { + const myapp = uniq('myapp'); + const mylib = uniq('mylib'); + + runCLI( + `generate @nx/react:app --name=${myapp} --unitTestRunner=jest --linter eslint --directory="."` + ); + runCLI('reset', { env: { CI: 'false' } }); + verifySuccessfulStandaloneSetup(myapp); + + let appEslint = readJson('.eslintrc.json'); + let e2eEslint = readJson('e2e/.eslintrc.json'); + + // should have plugin extends + let appOverrides = JSON.stringify(appEslint.overrides); + expect(appOverrides).toContain('plugin:@nx/javascript'); + expect(appOverrides).toContain('plugin:@nx/typescript'); + let e2eOverrides = JSON.stringify(e2eEslint.overrides); + expect(e2eOverrides).toContain('plugin:@nx/javascript'); + + runCLI( + `generate @nx/js:lib libs/${mylib} --unitTestRunner=jest --linter eslint` + ); + runCLI('reset', { env: { CI: 'false' } }); + verifySuccessfulMigratedSetup(myapp, mylib); + + appEslint = readJson(`.eslintrc.json`); + e2eEslint = readJson('e2e/.eslintrc.json'); + + // should have no plugin extends + appOverrides = JSON.stringify(appEslint.overrides); + expect(appOverrides).not.toContain('plugin:@nx/javascript'); + expect(appOverrides).not.toContain('plugin:@nx/typescript'); + e2eOverrides = JSON.stringify(e2eEslint.overrides); + expect(e2eOverrides).not.toContain('plugin:@nx/javascript'); + expect(e2eOverrides).not.toContain('plugin:@nx/typescript'); + }); + }); +}); diff --git a/e2e/eslint/src/linter-setup.ts b/e2e/eslint/src/linter-setup.ts new file mode 100644 index 0000000000000..d8a867530adc9 --- /dev/null +++ b/e2e/eslint/src/linter-setup.ts @@ -0,0 +1,39 @@ +import { cleanupProject, newProject, runCLI, uniq } from '@nx/e2e-utils'; + +export interface LinterIntegratedTestContext { + myapp: string; + mylib: string; + projScope: string; +} + +export function setupLinterIntegratedTest(): LinterIntegratedTestContext { + const myapp = uniq('myapp'); + const mylib = uniq('mylib'); + + const projScope = newProject({ + packages: ['@nx/react', '@nx/js', '@nx/eslint'], + }); + runCLI( + `generate @nx/react:app apps/${myapp} --tags=validtag --linter eslint --unitTestRunner vitest` + ); + runCLI(`generate @nx/js:lib libs/${mylib} --linter eslint`); + + return { myapp, mylib, projScope }; +} + +export function cleanupLinterIntegratedTest() { + cleanupProject(); +} + +export interface LinterRootProjectsTestContext {} + +export function setupLinterRootProjectsTest(): LinterRootProjectsTestContext { + newProject({ + packages: ['@nx/react', '@nx/js', '@nx/angular', '@nx/node'], + }); + return {}; +} + +export function cleanupLinterRootProjectsTest() { + cleanupProject(); +} diff --git a/e2e/eslint/src/linter-workspace-boundaries.test.ts b/e2e/eslint/src/linter-workspace-boundaries.test.ts new file mode 100644 index 0000000000000..afaa3938dc350 --- /dev/null +++ b/e2e/eslint/src/linter-workspace-boundaries.test.ts @@ -0,0 +1,229 @@ +import { createFile, readFile, runCLI, uniq } from '@nx/e2e-utils'; +import { + setupLinterIntegratedTest, + cleanupLinterIntegratedTest, + LinterIntegratedTestContext, +} from './linter-setup'; + +describe('Linter', () => { + let originalEslintUseFlatConfigVal: string | undefined; + beforeAll(() => { + // Opt into legacy .eslintrc config format for these tests + originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + }); + afterAll(() => { + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + describe('Integrated', () => { + let context: LinterIntegratedTestContext; + + beforeAll(() => { + context = setupLinterIntegratedTest(); + }); + afterAll(() => cleanupLinterIntegratedTest()); + + describe('workspace boundary rules', () => { + const libA = uniq('tslib-a'); + const libB = uniq('tslib-b'); + const libC = uniq('tslib-c'); + + beforeAll(() => { + const { projScope } = context; + // make these libs non-buildable to avoid dep-checks triggering lint errors + runCLI( + `generate @nx/js:lib libs/${libA} --bundler=none --linter eslint` + ); + runCLI( + `generate @nx/js:lib libs/${libB} --bundler=none --linter eslint` + ); + runCLI( + `generate @nx/js:lib libs/${libC} --bundler=none --linter eslint` + ); + + /** + * create tslib-a structure + */ + createFile( + `libs/${libA}/src/lib/tslib-a.ts`, + ` + export function libASayHi(): string { + return 'hi there'; + } + + export function libASayHello(): string { + return 'Hi from tslib-a'; + } + ` + ); + + createFile( + `libs/${libA}/src/lib/some-non-exported-function.ts`, + ` + export function someNonPublicLibFunction() { + return 'this function is exported, but not via the libs barrel file'; + } + + export function someSelectivelyExportedFn() { + return 'this fn is exported selectively in the barrel file'; + } + ` + ); + + createFile( + `libs/${libA}/src/index.ts`, + ` + export * from './lib/tslib-a'; + + export { someSelectivelyExportedFn } from './lib/some-non-exported-function'; + ` + ); + + /** + * create tslib-b structure + */ + createFile( + `libs/${libB}/src/index.ts`, + ` + export * from './lib/tslib-b'; + ` + ); + + createFile( + `libs/${libB}/src/lib/tslib-b.ts`, + ` + import { libASayHi } from 'libs/${libA}/src/lib/tslib-a'; + import { libASayHello } from '../../../${libA}/src/lib/tslib-a'; + // import { someNonPublicLibFunction } from '../../../${libA}/src/lib/some-non-exported-function'; + import { someSelectivelyExportedFn } from '../../../${libA}/src/lib/some-non-exported-function'; + + export function tslibB(): string { + // someNonPublicLibFunction(); + someSelectivelyExportedFn(); + libASayHi(); + libASayHello(); + return 'hi there'; + } + ` + ); + + /** + * create tslib-c structure + */ + + createFile( + `libs/${libC}/src/index.ts`, + ` + export * from './lib/tslib-c'; + export * from './lib/constant'; + + ` + ); + + createFile( + `libs/${libC}/src/lib/constant.ts`, + ` + export const SOME_CONSTANT = 'some constant value'; + export const someFunc1 = () => 'hi'; + export function someFunc2() { + return 'hi2'; + } + ` + ); + + createFile( + `libs/${libC}/src/lib/tslib-c-another.ts`, + ` + import { tslibC, SOME_CONSTANT, someFunc1, someFunc2 } from '@${projScope}/${libC}'; + + export function someStuff() { + someFunc1(); + someFunc2(); + tslibC(); + console.log(SOME_CONSTANT); + return 'hi'; + } + + ` + ); + + createFile( + `libs/${libC}/src/lib/tslib-c.ts`, + ` + import { someFunc1, someFunc2, SOME_CONSTANT } from '@${projScope}/${libC}'; + + export function tslibC(): string { + someFunc1(); + someFunc2(); + console.log(SOME_CONSTANT); + return 'tslib-c'; + } + + ` + ); + }); + + it('should fix noSelfCircularDependencies', () => { + const stdout = runCLI(`lint ${libC}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(stdout).toContain( + 'Projects should use relative imports to import from other files within the same project' + ); + + // fix them + const fixedStout = runCLI(`lint ${libC} --fix`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(fixedStout).toContain( + `Successfully ran target lint for project ${libC}` + ); + + const fileContent = readFile(`libs/${libC}/src/lib/tslib-c-another.ts`); + expect(fileContent).toContain(`import { tslibC } from './tslib-c';`); + expect(fileContent).toContain( + `import { SOME_CONSTANT, someFunc1, someFunc2 } from './constant';` + ); + + const fileContentTslibC = readFile(`libs/${libC}/src/lib/tslib-c.ts`); + expect(fileContentTslibC).toContain( + `import { someFunc1, someFunc2, SOME_CONSTANT } from './constant';` + ); + }); + + it('should fix noRelativeOrAbsoluteImportsAcrossLibraries', () => { + const { projScope } = context; + const stdout = runCLI(`lint ${libB}`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(stdout).toContain( + 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' + ); + + // fix them + const fixedStout = runCLI(`lint ${libB} --fix`, { + silenceError: true, + env: { CI: 'false' }, + }); + expect(fixedStout).toContain( + `Successfully ran target lint for project ${libB}` + ); + + const fileContent = readFile(`libs/${libB}/src/lib/tslib-b.ts`); + expect(fileContent).toContain( + `import { libASayHello } from '@${projScope}/${libA}';` + ); + expect(fileContent).toContain( + `import { libASayHi } from '@${projScope}/${libA}';` + ); + expect(fileContent).toContain( + `import { someSelectivelyExportedFn } from '@${projScope}/${libA}';` + ); + }); + }); + }); +}); diff --git a/e2e/eslint/src/linter.test.ts b/e2e/eslint/src/linter.test.ts deleted file mode 100644 index 514af67622001..0000000000000 --- a/e2e/eslint/src/linter.test.ts +++ /dev/null @@ -1,994 +0,0 @@ -import * as path from 'path'; -import { - checkFilesDoNotExist, - checkFilesExist, - cleanupProject, - createFile, - newProject, - readFile, - readJson, - runCLI, - uniq, - updateFile, - updateJson, -} from '@nx/e2e-utils'; -import * as ts from 'typescript'; - -describe('Linter', () => { - let originalEslintUseFlatConfigVal: string | undefined; - beforeAll(() => { - // Opt into legacy .eslintrc config format for these tests - originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; - process.env.ESLINT_USE_FLAT_CONFIG = 'false'; - }); - afterAll(() => { - process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; - }); - - describe('Integrated', () => { - const myapp = uniq('myapp'); - const mylib = uniq('mylib'); - - let projScope; - - beforeAll(() => { - projScope = newProject({ - packages: ['@nx/react', '@nx/js', '@nx/eslint'], - }); - runCLI( - `generate @nx/react:app apps/${myapp} --tags=validtag --linter eslint --unitTestRunner vitest` - ); - runCLI(`generate @nx/js:lib libs/${mylib} --linter eslint`); - }); - afterAll(() => cleanupProject()); - - describe('linting errors', () => { - let defaultEslintrc; - - beforeAll(() => { - updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`); - defaultEslintrc = readJson('.eslintrc.json'); - }); - afterEach(() => { - updateFile('.eslintrc.json', JSON.stringify(defaultEslintrc, null, 2)); - }); - - it('should check for linting errors', () => { - // create faulty file - updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`); - const eslintrc = readJson('.eslintrc.json'); - - // set the eslint rules to error - eslintrc.overrides.forEach((override) => { - if (override.files.includes('*.ts')) { - override.rules['no-console'] = 'error'; - } - }); - updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); - - let out = runCLI(`lint ${myapp}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain('Unexpected console statement'); - - eslintrc.overrides.forEach((override) => { - if (override.files.includes('*.ts')) { - override.rules['no-console'] = undefined; - } - }); - updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); - - // 3. linting should not error when all rules are followed - out = runCLI(`lint ${myapp}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain('Successfully ran target lint'); - }, 1000000); - - it('should cache eslint with --cache', () => { - function readCacheFile(cacheFile) { - const cacheInfo = readFile(cacheFile); - return process.platform === 'win32' - ? cacheInfo.replace(/\\\\/g, '\\') - : cacheInfo; - } - - // should generate a default cache file - let cachePath = path.join('apps', myapp, '.eslintcache'); - expect(() => checkFilesExist(cachePath)).toThrow(); - runCLI(`lint ${myapp} --cache`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(() => checkFilesExist(cachePath)).not.toThrow(); - expect(readCacheFile(cachePath)).toContain( - path.normalize(`${myapp}/src/app/app.spec.tsx`) - ); - - // should let you specify a cache file location - cachePath = path.join('apps', myapp, 'my-cache'); - expect(() => checkFilesExist(cachePath)).toThrow(); - runCLI(`lint ${myapp} --cache --cache-location="my-cache"`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(() => checkFilesExist(cachePath)).not.toThrow(); - expect(readCacheFile(cachePath)).toContain( - path.normalize(`${myapp}/src/app/app.spec.tsx`) - ); - }); - - it('linting should generate an output file with a specific format', () => { - const eslintrc = readJson('.eslintrc.json'); - eslintrc.overrides.forEach((override) => { - if (override.files.includes('*.ts')) { - override.rules['no-console'] = 'error'; - } - }); - updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); - - const outputFile = 'a/b/c/lint-output.json'; - const outputFilePath = path.join('apps', myapp, outputFile); - expect(() => { - checkFilesExist(outputFilePath); - }).toThrow(); - const stdout = runCLI( - `lint ${myapp} --output-file="${outputFile}" --format=json`, - { - silenceError: true, - env: { CI: 'false' }, - } - ); - expect(stdout).not.toContain('Unexpected console statement'); - expect(() => checkFilesExist(outputFilePath)).not.toThrow(); - const outputContents = JSON.parse(readFile(outputFilePath)); - const outputForApp: any = Object.values(outputContents).filter( - (result: any) => - result.filePath.includes(path.normalize(`${myapp}/src/main.ts`)) - )[0]; - expect(outputForApp.errorCount).toBe(1); - expect(outputForApp.messages[0].ruleId).toBe('no-console'); - expect(outputForApp.messages[0].message).toBe( - 'Unexpected console statement.' - ); - }, 1000000); - - it('should support creating, testing and using workspace lint rules', () => { - const messageId = 'e2eMessageId'; - const libMethodName = 'getMessageId'; - - // add custom function - updateFile( - `libs/${mylib}/src/lib/${mylib}.ts`, - `export const ${libMethodName} = (): '${messageId}' => '${messageId}';` - ); - - // Generate a new rule (should also scaffold the required workspace project and tests) - const newRuleName = 'e2e-test-rule-name'; - runCLI(`generate @nx/eslint:workspace-rule ${newRuleName}`); - - // TODO(@AgentEnder): This reset gets rid of a lockfile changed error... we should fix this in another way - runCLI(`reset`, { - env: { CI: 'false' }, - }); - - // Ensure that the unit tests for the new rule are runnable - expect(() => - runCLI(`test eslint-rules`, { - env: { CI: 'false' }, - }) - ).not.toThrow(); - - // Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used - const knownLintErrorMessage = 'e2e test known error message'; - const newRulePath = `tools/eslint-rules/rules/${newRuleName}.ts`; - const newRuleGeneratedContents = readFile(newRulePath); - const updatedRuleContents = updateGeneratedRuleImplementation( - newRulePath, - newRuleGeneratedContents, - knownLintErrorMessage, - messageId, - libMethodName, - `@${projScope}/${mylib}` - ); - updateFile(newRulePath, updatedRuleContents); - - const newRuleNameForUsage = `@nx/workspace-${newRuleName}`; - - // Add the new workspace rule to the lint config and run linting - const eslintrc = readJson('.eslintrc.json'); - eslintrc.overrides.forEach((override) => { - if (override.files.includes('*.ts')) { - override.rules[newRuleNameForUsage] = 'error'; - } - }); - updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); - - const lintOutput = runCLI(`lint ${myapp} --verbose`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(lintOutput).toContain(newRuleNameForUsage); - expect(lintOutput).toContain(knownLintErrorMessage); - }, 1000000); - - it('lint plugin should ensure module boundaries', () => { - const myapp2 = uniq('myapp2'); - const lazylib = uniq('lazylib'); - const invalidtaglib = uniq('invalidtaglib'); - const validtaglib = uniq('validtaglib'); - - runCLI(`generate @nx/react:app apps/${myapp2} --linter eslint`); - runCLI(`generate @nx/react:lib libs/${lazylib} --linter eslint`); - runCLI( - `generate @nx/js:lib libs/${invalidtaglib} --linter eslint --tags=invalidtag` - ); - runCLI( - `generate @nx/js:lib libs/${validtaglib} --linter eslint --tags=validtag` - ); - - const eslint = readJson('.eslintrc.json'); - eslint.overrides[0].rules[ - '@nx/enforce-module-boundaries' - ][1].depConstraints = [ - { sourceTag: 'validtag', onlyDependOnLibsWithTags: ['validtag'] }, - ...eslint.overrides[0].rules['@nx/enforce-module-boundaries'][1] - .depConstraints, - ]; - updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2)); - - const tsConfig = readJson('tsconfig.base.json'); - - /** - * apps do not add themselves to the tsconfig file. - * - * Let's add it so that we can trigger the lint failure - */ - tsConfig.compilerOptions.paths[`@${projScope}/${myapp2}`] = [ - `apps/${myapp2}/src/main.ts`, - ]; - - tsConfig.compilerOptions.paths[`@secondScope/${lazylib}`] = - tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`]; - delete tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`]; - updateFile('tsconfig.base.json', JSON.stringify(tsConfig, null, 2)); - - updateFile( - `apps/${myapp}/src/main.ts`, - ` - import '../../../libs/${mylib}'; - import '@secondScope/${lazylib}'; - import '@${projScope}/${myapp2}'; - import '@${projScope}/${invalidtaglib}'; - import '@${projScope}/${validtaglib}'; - - const s = {loadChildren: '@secondScope/${lazylib}'}; - ` - ); - - const out = runCLI(`lint ${myapp}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain( - 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' - ); - expect(out).toContain('Imports of apps are forbidden'); - expect(out).toContain( - 'A project tagged with "validtag" can only depend on libs tagged with "validtag"' - ); - }, 1000000); - }); - - describe('workspace boundary rules', () => { - const libA = uniq('tslib-a'); - const libB = uniq('tslib-b'); - const libC = uniq('tslib-c'); - - beforeAll(() => { - // make these libs non-buildable to avoid dep-checks triggering lint errors - runCLI( - `generate @nx/js:lib libs/${libA} --bundler=none --linter eslint` - ); - runCLI( - `generate @nx/js:lib libs/${libB} --bundler=none --linter eslint` - ); - runCLI( - `generate @nx/js:lib libs/${libC} --bundler=none --linter eslint` - ); - - /** - * create tslib-a structure - */ - createFile( - `libs/${libA}/src/lib/tslib-a.ts`, - ` - export function libASayHi(): string { - return 'hi there'; - } - - export function libASayHello(): string { - return 'Hi from tslib-a'; - } - ` - ); - - createFile( - `libs/${libA}/src/lib/some-non-exported-function.ts`, - ` - export function someNonPublicLibFunction() { - return 'this function is exported, but not via the libs barrel file'; - } - - export function someSelectivelyExportedFn() { - return 'this fn is exported selectively in the barrel file'; - } - ` - ); - - createFile( - `libs/${libA}/src/index.ts`, - ` - export * from './lib/tslib-a'; - - export { someSelectivelyExportedFn } from './lib/some-non-exported-function'; - ` - ); - - /** - * create tslib-b structure - */ - createFile( - `libs/${libB}/src/index.ts`, - ` - export * from './lib/tslib-b'; - ` - ); - - createFile( - `libs/${libB}/src/lib/tslib-b.ts`, - ` - import { libASayHi } from 'libs/${libA}/src/lib/tslib-a'; - import { libASayHello } from '../../../${libA}/src/lib/tslib-a'; - // import { someNonPublicLibFunction } from '../../../${libA}/src/lib/some-non-exported-function'; - import { someSelectivelyExportedFn } from '../../../${libA}/src/lib/some-non-exported-function'; - - export function tslibB(): string { - // someNonPublicLibFunction(); - someSelectivelyExportedFn(); - libASayHi(); - libASayHello(); - return 'hi there'; - } - ` - ); - - /** - * create tslib-c structure - */ - - createFile( - `libs/${libC}/src/index.ts`, - ` - export * from './lib/tslib-c'; - export * from './lib/constant'; - - ` - ); - - createFile( - `libs/${libC}/src/lib/constant.ts`, - ` - export const SOME_CONSTANT = 'some constant value'; - export const someFunc1 = () => 'hi'; - export function someFunc2() { - return 'hi2'; - } - ` - ); - - createFile( - `libs/${libC}/src/lib/tslib-c-another.ts`, - ` - import { tslibC, SOME_CONSTANT, someFunc1, someFunc2 } from '@${projScope}/${libC}'; - - export function someStuff() { - someFunc1(); - someFunc2(); - tslibC(); - console.log(SOME_CONSTANT); - return 'hi'; - } - - ` - ); - - createFile( - `libs/${libC}/src/lib/tslib-c.ts`, - ` - import { someFunc1, someFunc2, SOME_CONSTANT } from '@${projScope}/${libC}'; - - export function tslibC(): string { - someFunc1(); - someFunc2(); - console.log(SOME_CONSTANT); - return 'tslib-c'; - } - - ` - ); - }); - - it('should fix noSelfCircularDependencies', () => { - const stdout = runCLI(`lint ${libC}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(stdout).toContain( - 'Projects should use relative imports to import from other files within the same project' - ); - - // fix them - const fixedStout = runCLI(`lint ${libC} --fix`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(fixedStout).toContain( - `Successfully ran target lint for project ${libC}` - ); - - const fileContent = readFile(`libs/${libC}/src/lib/tslib-c-another.ts`); - expect(fileContent).toContain(`import { tslibC } from './tslib-c';`); - expect(fileContent).toContain( - `import { SOME_CONSTANT, someFunc1, someFunc2 } from './constant';` - ); - - const fileContentTslibC = readFile(`libs/${libC}/src/lib/tslib-c.ts`); - expect(fileContentTslibC).toContain( - `import { someFunc1, someFunc2, SOME_CONSTANT } from './constant';` - ); - }); - - it('should fix noRelativeOrAbsoluteImportsAcrossLibraries', () => { - const stdout = runCLI(`lint ${libB}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(stdout).toContain( - 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' - ); - - // fix them - const fixedStout = runCLI(`lint ${libB} --fix`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(fixedStout).toContain( - `Successfully ran target lint for project ${libB}` - ); - - const fileContent = readFile(`libs/${libB}/src/lib/tslib-b.ts`); - expect(fileContent).toContain( - `import { libASayHello } from '@${projScope}/${libA}';` - ); - expect(fileContent).toContain( - `import { libASayHi } from '@${projScope}/${libA}';` - ); - expect(fileContent).toContain( - `import { someSelectivelyExportedFn } from '@${projScope}/${libA}';` - ); - }); - }); - - describe('dependency checks', () => { - beforeAll(() => { - updateJson(`libs/${mylib}/.eslintrc.json`, (json) => { - if (!json.overrides.some((o) => o.rules?.['@nx/dependency-checks'])) { - json.overrides = [ - ...json.overrides, - { - files: ['*.json'], - parser: 'jsonc-eslint-parser', - rules: { - '@nx/dependency-checks': 'error', - }, - }, - ]; - } - return json; - }); - }); - - afterAll(() => { - // ensure the rule for dependency checks is removed - // so that it does not affect other tests - updateJson(`libs/${mylib}/.eslintrc.json`, (json) => { - json.overrides = json.overrides.filter( - (o) => !o.rules?.['@nx/dependency-checks'] - ); - return json; - }); - }); - - it('should report dependency check issues', () => { - const rootPackageJson = readJson('package.json'); - const nxVersion = rootPackageJson.devDependencies.nx; - const tslibVersion = - rootPackageJson.dependencies['tslib'] || - rootPackageJson.devDependencies['tslib']; - - let out = runCLI(`lint ${mylib}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain('Successfully ran target lint'); - - // make an explict dependency to nx - updateFile( - `libs/${mylib}/src/lib/${mylib}.ts`, - (content) => - `import { names } from '@nx/devkit';\n\n` + - content.replace(/=> .*;/, `=> names('${mylib}').className;`) - ); - // intentionally set an obsolete dependency - updateJson(`libs/${mylib}/package.json`, (json) => { - json.dependencies['@nx/js'] = nxVersion; - return json; - }); - - // output should now report missing dependency and obsolete dependency - out = runCLI(`lint ${mylib}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain('they are missing'); - expect(out).toContain('@nx/devkit'); - expect(out).toContain( - `The "@nx/js" package is not used by "${mylib}" project` - ); - - // should fix the missing and obsolete dependency issues - out = runCLI(`lint ${mylib} --fix`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain( - `Successfully ran target lint for project ${mylib}` - ); - const packageJson = readJson(`libs/${mylib}/package.json`); - expect(packageJson).toMatchObject({ - dependencies: { - '@nx/devkit': nxVersion, - tslib: tslibVersion, - }, - main: './src/index.js', - name: `@proj/${mylib}`, - private: true, - type: 'commonjs', - types: './src/index.d.ts', - version: '0.0.1', - }); - - // intentionally set the invalid version - updateJson(`libs/${mylib}/package.json`, (json) => { - json.dependencies['@nx/devkit'] = '100.0.0'; - return json; - }); - out = runCLI(`lint ${mylib}`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain( - 'version specifier does not contain the installed version of "@nx/devkit"' - ); - - // should fix the version mismatch issue - out = runCLI(`lint ${mylib} --fix`, { - silenceError: true, - env: { CI: 'false' }, - }); - expect(out).toContain( - `Successfully ran target lint for project ${mylib}` - ); - }); - }); - - describe('flat config', () => { - let envVar: string | undefined; - beforeAll(() => { - runCLI(`generate @nx/eslint:convert-to-flat-config`); - envVar = process.env.ESLINT_USE_FLAT_CONFIG; - // Now that we have converted the existing configs to flat config we need to clear the explicitly set env var to allow it to infer things from the root config file type - delete process.env.ESLINT_USE_FLAT_CONFIG; - }); - afterAll(() => { - process.env.ESLINT_USE_FLAT_CONFIG = envVar; - }); - - it('should generate new projects using flat config', () => { - const reactLib = uniq('react-lib'); - const jsLib = uniq('js-lib'); - - runCLI(`generate @nx/react:lib ${reactLib} --linter eslint`); - runCLI(`generate @nx/js:lib ${jsLib} --linter eslint`); - - checkFilesExist( - `${reactLib}/eslint.config.mjs`, - `${jsLib}/eslint.config.mjs` - ); - checkFilesDoNotExist( - `${reactLib}/.eslintrc.json`, - `${jsLib}/.eslintrc.json` - ); - - // validate that the new projects are linted successfully - expect(() => - runCLI(`lint ${reactLib}`, { - env: { CI: 'false' }, - }) - ).not.toThrow(); - expect(() => - runCLI(`lint ${jsLib}`, { - env: { CI: 'false' }, - }) - ).not.toThrow(); - }); - }); - }); - - describe('Root projects migration', () => { - beforeEach(() => - newProject({ - packages: ['@nx/react', '@nx/js', '@nx/angular', '@nx/node'], - }) - ); - afterEach(() => cleanupProject()); - - function verifySuccessfulStandaloneSetup(myapp: string) { - expect( - runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) - ).toContain('Successfully ran target lint'); - expect( - runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) - ).toContain('Successfully ran target lint'); - expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow(); - - const rootEslint = readJson('.eslintrc.json'); - const e2eEslint = readJson('e2e/.eslintrc.json'); - - // should directly refer to nx plugin - expect(rootEslint.plugins).toEqual(['@nx']); - expect(e2eEslint.plugins).toEqual(['@nx']); - } - - function verifySuccessfulMigratedSetup(myapp: string, mylib: string) { - expect( - runCLI(`lint ${myapp}`, { silenceError: true, env: { CI: 'false' } }) - ).toContain('Successfully ran target lint'); - expect( - runCLI(`lint e2e`, { silenceError: true, env: { CI: 'false' } }) - ).toContain('Successfully ran target lint'); - expect( - runCLI(`lint ${mylib}`, { silenceError: true, env: { CI: 'false' } }) - ).toContain('Successfully ran target lint'); - expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow(); - - const rootEslint = readJson('.eslintrc.base.json'); - const appEslint = readJson('.eslintrc.json'); - const e2eEslint = readJson('e2e/.eslintrc.json'); - const libEslint = readJson(`libs/${mylib}/.eslintrc.json`); - - // should directly refer to nx plugin - expect(rootEslint.plugins).toEqual(['@nx']); - expect(appEslint.plugins).toBeUndefined(); - expect(e2eEslint.plugins).toBeUndefined(); - expect(libEslint.plugins).toBeUndefined(); - - // should extend base - expect(appEslint.extends.slice(-1)).toEqual(['./.eslintrc.base.json']); - expect(e2eEslint.extends.slice(-1)).toEqual(['../.eslintrc.base.json']); - expect(libEslint.extends.slice(-1)).toEqual([ - '../../.eslintrc.base.json', - ]); - } - - it('(React standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { - const myapp = uniq('myapp'); - const mylib = uniq('mylib'); - - runCLI( - `generate @nx/react:app --name=${myapp} --unitTestRunner=jest --linter eslint --directory="."` - ); - runCLI('reset', { env: { CI: 'false' } }); - verifySuccessfulStandaloneSetup(myapp); - - let appEslint = readJson('.eslintrc.json'); - let e2eEslint = readJson('e2e/.eslintrc.json'); - - // should have plugin extends - let appOverrides = JSON.stringify(appEslint.overrides); - expect(appOverrides).toContain('plugin:@nx/javascript'); - expect(appOverrides).toContain('plugin:@nx/typescript'); - let e2eOverrides = JSON.stringify(e2eEslint.overrides); - expect(e2eOverrides).toContain('plugin:@nx/javascript'); - - runCLI( - `generate @nx/js:lib libs/${mylib} --unitTestRunner=jest --linter eslint` - ); - runCLI('reset', { env: { CI: 'false' } }); - verifySuccessfulMigratedSetup(myapp, mylib); - - appEslint = readJson(`.eslintrc.json`); - e2eEslint = readJson('e2e/.eslintrc.json'); - - // should have no plugin extends - appOverrides = JSON.stringify(appEslint.overrides); - expect(appOverrides).not.toContain('plugin:@nx/javascript'); - expect(appOverrides).not.toContain('plugin:@nx/typescript'); - e2eOverrides = JSON.stringify(e2eEslint.overrides); - expect(e2eOverrides).not.toContain('plugin:@nx/javascript'); - expect(e2eOverrides).not.toContain('plugin:@nx/typescript'); - }); - - it('(Angular standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { - const myapp = uniq('myapp'); - const mylib = uniq('mylib'); - - runCLI( - `generate @nx/angular:app --name=${myapp} --directory="." --linter eslint --no-interactive` - ); - runCLI('reset', { env: { CI: 'false' } }); - verifySuccessfulStandaloneSetup(myapp); - - let appEslint = readJson('.eslintrc.json'); - let e2eEslint = readJson('e2e/.eslintrc.json'); - - // should have plugin extends - let appOverrides = JSON.stringify(appEslint.overrides); - expect(appOverrides).toContain('plugin:@nx/typescript'); - let e2eOverrides = JSON.stringify(e2eEslint.overrides); - expect(e2eOverrides).toContain('plugin:@nx/javascript'); - - runCLI( - `generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive` - ); - runCLI('reset', { env: { CI: 'false' } }); - verifySuccessfulMigratedSetup(myapp, mylib); - - appEslint = readJson(`.eslintrc.json`); - e2eEslint = readJson('e2e/.eslintrc.json'); - - // should have no plugin extends - appOverrides = JSON.stringify(appEslint.overrides); - expect(appOverrides).not.toContain('plugin:@nx/typescript'); - e2eOverrides = JSON.stringify(e2eEslint.overrides); - expect(e2eOverrides).not.toContain('plugin:@nx/typescript'); - }); - - it('(Node standalone) should set root project config to app and e2e app and migrate when another lib is added', async () => { - const myapp = uniq('myapp'); - const mylib = uniq('mylib'); - - runCLI( - `generate @nx/node:app --name=${myapp} --linter=eslint --directory="." --unitTestRunner=jest --e2eTestRunner=jest --no-interactive` - ); - runCLI('reset', { env: { CI: 'false' } }); - verifySuccessfulStandaloneSetup(myapp); - - let appEslint = readJson('.eslintrc.json'); - let e2eEslint = readJson('e2e/.eslintrc.json'); - - // should have plugin extends - let appOverrides = JSON.stringify(appEslint.overrides); - expect(appOverrides).toContain('plugin:@nx/javascript'); - expect(appOverrides).toContain('plugin:@nx/typescript'); - let e2eOverrides = JSON.stringify(e2eEslint.overrides); - expect(e2eOverrides).toContain('plugin:@nx/javascript'); - expect(e2eOverrides).toContain('plugin:@nx/typescript'); - - runCLI( - `generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive` - ); - runCLI('reset', { env: { CI: 'false' } }); - verifySuccessfulMigratedSetup(myapp, mylib); - - appEslint = readJson(`.eslintrc.json`); - e2eEslint = readJson('e2e/.eslintrc.json'); - - // should have no plugin extends - // should have no plugin extends - appOverrides = JSON.stringify(appEslint.overrides); - expect(appOverrides).not.toContain('plugin:@nx/javascript'); - expect(appOverrides).not.toContain('plugin:@nx/typescript'); - e2eOverrides = JSON.stringify(e2eEslint.overrides); - expect(e2eOverrides).not.toContain('plugin:@nx/javascript'); - expect(e2eOverrides).not.toContain('plugin:@nx/typescript'); - }); - }); -}); - -/** - * Update the generated rule implementation to produce a known lint error from all files. - * - * It is important that we do this surgically via AST transformations, otherwise we will - * drift further and further away from the original generated code and therefore make our - * e2e test less accurate and less valuable. - */ -function updateGeneratedRuleImplementation( - newRulePath: string, - newRuleGeneratedContents: string, - knownLintErrorMessage: string, - messageId, - libMethodName: string, - libPath: string -): string { - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); - const newRuleSourceFile = ts.createSourceFile( - newRulePath, - newRuleGeneratedContents, - ts.ScriptTarget.Latest, - true - ); - - const transformer = ( - context: ts.TransformationContext - ) => - ((rootNode: T) => { - function visit(node: ts.Node): ts.Node { - /** - * Add an ESLint messageId which will show the knownLintErrorMessage - * - * i.e. - * - * messages: { - * e2eMessageId: knownLintErrorMessage - * } - */ - if ( - ts.isPropertyAssignment(node) && - ts.isIdentifier(node.name) && - node.name.escapedText === 'messages' - ) { - return ts.factory.updatePropertyAssignment( - node, - node.name, - ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - messageId, - ts.factory.createStringLiteral(knownLintErrorMessage) - ), - ]) - ); - } - - /** - * Update the rule implementation to report the knownLintErrorMessage on every Program node - * - * During the debugging of the switch from ts-node to swc-node we found out - * that regular rules would work even without explicit path mapping registration, - * but rules that import runtime functionality from within the workspace - * would break the rule registration. - * - * Instead of having a static literal messageId we retreieved it via imported getMessageId method. - * - * i.e. - * - * create(context) { - * return { - * Program(node) { - * context.report({ - * messageId: getMessageId(), - * node, - * }); - * } - * } - * } - */ - if ( - ts.isMethodDeclaration(node) && - ts.isIdentifier(node.name) && - node.name.escapedText === 'create' - ) { - return ts.factory.updateMethodDeclaration( - node, - node.modifiers, - node.asteriskToken, - node.name, - node.questionToken, - node.typeParameters, - node.parameters, - node.type, - ts.factory.createBlock([ - ts.factory.createReturnStatement( - ts.factory.createObjectLiteralExpression([ - ts.factory.createMethodDeclaration( - [], - undefined, - 'Program', - undefined, - [], - [ - ts.factory.createParameterDeclaration( - [], - undefined, - 'node', - undefined, - undefined, - undefined - ), - ], - undefined, - ts.factory.createBlock([ - ts.factory.createExpressionStatement( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('context'), - 'report' - ), - [], - [ - ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - 'messageId', - ts.factory.createCallExpression( - ts.factory.createIdentifier(libMethodName), - [], - [] - ) - ), - ts.factory.createShorthandPropertyAssignment( - 'node' - ), - ]), - ] - ) - ), - ]) - ), - ]) - ), - ]) - ); - } - - return ts.visitEachChild(node, visit, context); - } - - /** - * Add lib import as a first line of the rule file. - * Needed for the access of getMessageId in the context report above. - * - * i.e. - * - * import { getMessageId } from "@myproj/mylib"; - * - */ - const importAdded = ts.factory.updateSourceFile(rootNode, [ - ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(libMethodName) - ), - ]) - ), - ts.factory.createStringLiteral(libPath) - ), - ...rootNode.statements, - ]); - return ts.visitNode(importAdded, visit); - }) as ts.Transformer; - - const result: ts.TransformationResult = - ts.transform(newRuleSourceFile, [transformer]); - const updatedSourceFile: ts.SourceFile = result.transformed[0]; - - return printer.printFile(updatedSourceFile); -} diff --git a/e2e/nx/src/affected-graph-affected-star.test.ts b/e2e/nx/src/affected-graph-affected-star.test.ts new file mode 100644 index 0000000000000..e82f164a0724e --- /dev/null +++ b/e2e/nx/src/affected-graph-affected-star.test.ts @@ -0,0 +1,162 @@ +import { readFile, runCLI, uniq, updateFile } from '@nx/e2e-utils'; +import type { NxJsonConfiguration } from '@nx/devkit'; +import { + setupAffectedGraphTest, + cleanupAffectedGraphTest, +} from './affected-graph-setup'; + +describe('Nx Affected and Graph Tests', () => { + let proj: string; + + beforeAll(() => { + const context = setupAffectedGraphTest(); + proj = context.proj; + }); + afterAll(() => cleanupAffectedGraphTest()); + + describe('affected:*', () => { + it('should print, build, and test affected apps', async () => { + process.env.CI = 'true'; + const myapp = uniq('myapp'); + const myapp2 = uniq('myapp2'); + const mylib = uniq('mylib'); + const mylib2 = uniq('mylib2'); + const mypublishablelib = uniq('mypublishablelib'); + runCLI(`generate @nx/web:app apps/${myapp} --unitTestRunner=vitest`); + runCLI(`generate @nx/web:app apps/${myapp2} --unitTestRunner=vitest`); + runCLI(`generate @nx/js:lib libs/${mylib}`); + runCLI(`generate @nx/js:lib libs/${mylib2}`); + runCLI( + `generate @nx/js:lib libs/${mypublishablelib} --publishable --importPath=@${proj}/${mypublishablelib} --tags=ui` + ); + + updateFile( + `apps/${myapp}/src/app/app.element.spec.ts`, + ` + import * as x from '@${proj}/${mylib}'; + describe('sample test', () => { + it('should test', () => { + expect(1).toEqual(1); + }); + }); + ` + ); + updateFile( + `libs/${mypublishablelib}/src/lib/${mypublishablelib}.spec.ts`, + ` + import * as x from '@${proj}/${mylib}'; + describe('sample test', () => { + it('should test', () => { + expect(1).toEqual(1); + }); + }); + ` + ); + + const affectedProjects = runCLI( + `show projects --affected --files="libs/${mylib}/src/index.ts"` + ); + expect(affectedProjects).toContain(myapp); + expect(affectedProjects).not.toContain(myapp2); + + let affectedLibs = runCLI( + `show projects --affected --files="libs/${mylib}/src/index.ts" --type lib` + ); + // type lib shows no apps + expect(affectedLibs).not.toContain(myapp); + expect(affectedLibs).not.toContain(myapp2); + expect(affectedLibs).toContain(mylib); + + const implicitlyAffectedApps = runCLI( + 'show projects --affected --files="tsconfig.base.json"' + ); + expect(implicitlyAffectedApps).toContain(myapp); + expect(implicitlyAffectedApps).toContain(myapp2); + + const noAffectedApps = runCLI( + 'show projects --affected projects --files="README.md"' + ); + expect(noAffectedApps).not.toContain(myapp); + expect(noAffectedApps).not.toContain(myapp2); + + affectedLibs = runCLI( + `show projects --affected --files="libs/${mylib}/src/index.ts"` + ); + expect(affectedLibs).toContain(mypublishablelib); + expect(affectedLibs).toContain(mylib); + expect(affectedLibs).not.toContain(mylib2); + + const implicitlyAffectedLibs = runCLI( + 'show projects --affected --files="tsconfig.base.json"' + ); + expect(implicitlyAffectedLibs).toContain(mypublishablelib); + expect(implicitlyAffectedLibs).toContain(mylib); + expect(implicitlyAffectedLibs).toContain(mylib2); + + const noAffectedLibsNonExistentFile = runCLI( + 'show projects --affected --files="tsconfig.json"' + ); + expect(noAffectedLibsNonExistentFile).not.toContain(mypublishablelib); + expect(noAffectedLibsNonExistentFile).not.toContain(mylib); + expect(noAffectedLibsNonExistentFile).not.toContain(mylib2); + + const noAffectedLibs = runCLI( + 'show projects --affected --files="README.md"' + ); + expect(noAffectedLibs).not.toContain(mypublishablelib); + expect(noAffectedLibs).not.toContain(mylib); + expect(noAffectedLibs).not.toContain(mylib2); + + // build + const build = runCLI( + `affected:build --files="libs/${mylib}/src/index.ts" --parallel` + ); + expect(build).toContain(`Running target build for 3 projects:`); + expect(build).toContain(`- ${myapp}`); + expect(build).toContain(`- ${mypublishablelib}`); + expect(build).not.toContain('is not registered with the build command'); + expect(build).toContain('Successfully ran target build'); + + const buildExcluded = runCLI( + `affected:build --files="libs/${mylib}/src/index.ts" --exclude=${myapp}` + ); + expect(buildExcluded).toContain(`Running target build for 2 projects:`); + expect(buildExcluded).toContain(`- ${mypublishablelib}`); + + const buildExcludedByTag = runCLI( + `affected:build --files="libs/${mylib}/src/index.ts" --exclude=tag:ui` + ); + expect(buildExcludedByTag).toContain( + `Running target build for 2 projects:` + ); + expect(buildExcludedByTag).not.toContain(`- ${mypublishablelib}`); + + // test + updateFile( + `apps/${myapp}/src/app/app.element.spec.ts`, + readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace( + '.toEqual(1)', + '.toEqual(2)' + ) + ); + + const failedTests = runCLI( + `affected:test --files="libs/${mylib}/src/index.ts"`, + { silenceError: true } + ); + expect(failedTests).toContain(mylib); + expect(failedTests).toContain(myapp); + expect(failedTests).toContain(mypublishablelib); + expect(failedTests).toContain(`Failed tasks:`); + + // Fix failing Unit Test + updateFile( + `apps/${myapp}/src/app/app.element.spec.ts`, + readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace( + '.toEqual(2)', + '.toEqual(1)' + ) + ); + }, 1000000); + }); +}); diff --git a/e2e/nx/src/affected-graph-graph-focus-exclude.test.ts b/e2e/nx/src/affected-graph-graph-focus-exclude.test.ts new file mode 100644 index 0000000000000..aad4a2f2bd45e --- /dev/null +++ b/e2e/nx/src/affected-graph-graph-focus-exclude.test.ts @@ -0,0 +1,138 @@ +import { + checkFilesExist, + readJson, + runCLI, + runCommand, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { + setupAffectedGraphTest, + cleanupAffectedGraphTest, +} from './affected-graph-setup'; + +describe('Nx Affected and Graph Tests', () => { + let proj: string; + + beforeAll(() => { + const context = setupAffectedGraphTest(); + proj = context.proj; + }); + afterAll(() => cleanupAffectedGraphTest()); + + describe('graph', () => { + let myapp: string; + let myapp2: string; + let myapp3: string; + let myappE2e: string; + let myapp2E2e: string; + let myapp3E2e: string; + let mylib: string; + let mylib2: string; + + beforeAll(() => { + myapp = uniq('myapp'); + myapp2 = uniq('myapp2'); + myapp3 = uniq('myapp3'); + myappE2e = `${myapp}-e2e`; + myapp2E2e = `${myapp2}-e2e`; + myapp3E2e = `${myapp3}-e2e`; + mylib = uniq('mylib'); + mylib2 = uniq('mylib2'); + + runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); + runCLI(`generate @nx/web:app ${myapp2} --directory=apps/${myapp2}`); + runCLI(`generate @nx/web:app ${myapp3} --directory=apps/${myapp3}`); + runCLI(`generate @nx/js:lib ${mylib} --directory=libs/${mylib}`); + runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); + + runCommand(`git init`); + runCommand(`git config user.email "test@test.com"`); + runCommand(`git config user.name "Test"`); + runCommand(`git config commit.gpgsign false`); + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b main` + ); + + updateFile( + `apps/${myapp}/src/main.ts`, + ` + import '@${proj}/${mylib}'; + + const s = {loadChildren: '@${proj}/${mylib2}'}; + ` + ); + + updateFile( + `apps/${myapp2}/src/app/app.element.spec.ts`, + `import '@${proj}/${mylib}';` + ); + + updateFile( + `libs/${mylib}/src/${mylib}.spec.ts`, + `import '@${proj}/${mylib2}';` + ); + }); + + it('graph should focus requested project', () => { + runCLI(`graph --focus=${myapp} --file=project-graph.json`); + + expect(() => checkFilesExist('project-graph.json')).not.toThrow(); + + const jsonFileContents = readJson('project-graph.json'); + const projectNames = Object.keys(jsonFileContents.graph.nodes); + + expect(projectNames).toContain(myapp); + expect(projectNames).toContain(mylib); + expect(projectNames).toContain(mylib2); + expect(projectNames).toContain(myappE2e); + + expect(projectNames).not.toContain(myapp2); + expect(projectNames).not.toContain(myapp3); + expect(projectNames).not.toContain(myapp2E2e); + expect(projectNames).not.toContain(myapp3E2e); + }, 1000000); + + it('graph should exclude requested projects', () => { + runCLI( + `graph --exclude=${myappE2e},${myapp2E2e},${myapp3E2e} --file=project-graph.json` + ); + + expect(() => checkFilesExist('project-graph.json')).not.toThrow(); + + const jsonFileContents = readJson('project-graph.json'); + const projectNames = Object.keys(jsonFileContents.graph.nodes); + + expect(projectNames).toContain(myapp); + expect(projectNames).toContain(mylib); + expect(projectNames).toContain(mylib2); + expect(projectNames).toContain(myapp2); + expect(projectNames).toContain(myapp3); + + expect(projectNames).not.toContain(myappE2e); + expect(projectNames).not.toContain(myapp2E2e); + expect(projectNames).not.toContain(myapp3E2e); + }, 1000000); + + it('graph should exclude requested projects that were included by a focus', () => { + runCLI( + `graph --focus=${myapp} --exclude=${myappE2e} --file=project-graph.json` + ); + + expect(() => checkFilesExist('project-graph.json')).not.toThrow(); + + const jsonFileContents = readJson('project-graph.json'); + const projectNames = Object.keys(jsonFileContents.graph.nodes); + + expect(projectNames).toContain(myapp); + expect(projectNames).toContain(mylib); + expect(projectNames).toContain(mylib2); + + expect(projectNames).not.toContain(myappE2e); + expect(projectNames).not.toContain(myapp2); + expect(projectNames).not.toContain(myapp3); + expect(projectNames).not.toContain(myapp2E2e); + expect(projectNames).not.toContain(myapp3E2e); + }, 1000000); + }); +}); diff --git a/e2e/nx/src/affected-graph-graph-html.test.ts b/e2e/nx/src/affected-graph-graph-html.test.ts new file mode 100644 index 0000000000000..e91cad92fcffe --- /dev/null +++ b/e2e/nx/src/affected-graph-graph-html.test.ts @@ -0,0 +1,116 @@ +import { + checkFilesExist, + readFile, + runCLI, + runCommand, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { + setupAffectedGraphTest, + cleanupAffectedGraphTest, +} from './affected-graph-setup'; + +describe('Nx Affected and Graph Tests', () => { + let proj: string; + + beforeAll(() => { + const context = setupAffectedGraphTest(); + proj = context.proj; + }); + afterAll(() => cleanupAffectedGraphTest()); + + describe('graph', () => { + let myapp: string; + let myapp2: string; + let myapp3: string; + let myappE2e: string; + let myapp2E2e: string; + let myapp3E2e: string; + let mylib: string; + let mylib2: string; + + beforeAll(() => { + myapp = uniq('myapp'); + myapp2 = uniq('myapp2'); + myapp3 = uniq('myapp3'); + myappE2e = `${myapp}-e2e`; + myapp2E2e = `${myapp2}-e2e`; + myapp3E2e = `${myapp3}-e2e`; + mylib = uniq('mylib'); + mylib2 = uniq('mylib2'); + + runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); + runCLI(`generate @nx/web:app ${myapp2} --directory=apps/${myapp2}`); + runCLI(`generate @nx/web:app ${myapp3} --directory=apps/${myapp3}`); + runCLI(`generate @nx/js:lib ${mylib} --directory=libs/${mylib}`); + runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); + + runCommand(`git init`); + runCommand(`git config user.email "test@test.com"`); + runCommand(`git config user.name "Test"`); + runCommand(`git config commit.gpgsign false`); + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b main` + ); + + updateFile( + `apps/${myapp}/src/main.ts`, + ` + import '@${proj}/${mylib}'; + + const s = {loadChildren: '@${proj}/${mylib2}'}; + ` + ); + + updateFile( + `apps/${myapp2}/src/app/app.element.spec.ts`, + `import '@${proj}/${mylib}';` + ); + + updateFile( + `libs/${mylib}/src/${mylib}.spec.ts`, + `import '@${proj}/${mylib2}';` + ); + }); + + it('graph should output a deployable static website in an html file accompanied by a folder with static assets', () => { + runCLI(`graph --file=project-graph.html`); + + expect(() => checkFilesExist('project-graph.html')).not.toThrow(); + expect(() => checkFilesExist('static/styles.css')).not.toThrow(); + expect(() => checkFilesExist('static/runtime.js')).not.toThrow(); + expect(() => checkFilesExist('static/main.js')).not.toThrow(); + expect(() => checkFilesExist('static/environment.js')).not.toThrow(); + + const environmentJs = readFile('static/environment.js'); + + expect(environmentJs).toContain('window.projectGraphResponse'); + expect(environmentJs).toMatch(/"affected":\[.*\]/); + }); + + // TODO(@AgentEnder): Please re-enable this when you fix the output + xit('graph should output valid json when stdout is specified', () => { + const result = runCLI(`affected -t build --graph stdout`); + let model; + expect(() => (model = JSON.parse(result))).not.toThrow(); + expect(model).toHaveProperty('graph'); + expect(model).toHaveProperty('tasks'); + }); + + it('should include affected projects in environment file', () => { + runCLI(`graph --affected --file=project-graph.html`); + + const environmentJs = readFile('static/environment.js'); + const affectedProjects = environmentJs + .match(/"affected":\[(.*?)\]/)[1] + ?.split(','); + + expect(affectedProjects).toContain(`"${myapp}"`); + expect(affectedProjects).toContain(`"${myappE2e}"`); + expect(affectedProjects).toContain(`"${myapp2}"`); + expect(affectedProjects).toContain(`"${myapp2E2e}"`); + expect(affectedProjects).toContain(`"${mylib}"`); + }); + }); +}); diff --git a/e2e/nx/src/affected-graph-graph-json.test.ts b/e2e/nx/src/affected-graph-graph-json.test.ts new file mode 100644 index 0000000000000..feae3ffe156a7 --- /dev/null +++ b/e2e/nx/src/affected-graph-graph-json.test.ts @@ -0,0 +1,153 @@ +import { + isNotWindows, + isWindows, + checkFilesExist, + fileExists, + readJson, + runCLI, + runCommand, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { + setupAffectedGraphTest, + cleanupAffectedGraphTest, +} from './affected-graph-setup'; + +describe('Nx Affected and Graph Tests', () => { + let proj: string; + + beforeAll(() => { + const context = setupAffectedGraphTest(); + proj = context.proj; + }); + afterAll(() => cleanupAffectedGraphTest()); + + describe('graph', () => { + let myapp: string; + let myapp2: string; + let myapp3: string; + let myappE2e: string; + let myapp2E2e: string; + let myapp3E2e: string; + let mylib: string; + let mylib2: string; + + beforeAll(() => { + myapp = uniq('myapp'); + myapp2 = uniq('myapp2'); + myapp3 = uniq('myapp3'); + myappE2e = `${myapp}-e2e`; + myapp2E2e = `${myapp2}-e2e`; + myapp3E2e = `${myapp3}-e2e`; + mylib = uniq('mylib'); + mylib2 = uniq('mylib2'); + + runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); + runCLI(`generate @nx/web:app ${myapp2} --directory=apps/${myapp2}`); + runCLI(`generate @nx/web:app ${myapp3} --directory=apps/${myapp3}`); + runCLI(`generate @nx/js:lib ${mylib} --directory=libs/${mylib}`); + runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); + + runCommand(`git init`); + runCommand(`git config user.email "test@test.com"`); + runCommand(`git config user.name "Test"`); + runCommand(`git config commit.gpgsign false`); + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b main` + ); + + updateFile( + `apps/${myapp}/src/main.ts`, + ` + import '@${proj}/${mylib}'; + + const s = {loadChildren: '@${proj}/${mylib2}'}; + ` + ); + + updateFile( + `apps/${myapp2}/src/app/app.element.spec.ts`, + `import '@${proj}/${mylib}';` + ); + + updateFile( + `libs/${mylib}/src/${mylib}.spec.ts`, + `import '@${proj}/${mylib2}';` + ); + }); + + it('graph should output json to file', () => { + runCLI(`graph --file=project-graph.json`); + + expect(() => checkFilesExist('project-graph.json')).not.toThrow(); + + const jsonFileContents = readJson('project-graph.json'); + + expect(jsonFileContents.graph.dependencies).toEqual( + expect.objectContaining({ + [myapp3E2e]: [ + { + source: myapp3E2e, + target: myapp3, + type: 'implicit', + }, + ], + [myapp2]: [ + { + source: myapp2, + target: mylib, + type: 'static', + }, + ], + [myapp2E2e]: [ + { + source: myapp2E2e, + target: myapp2, + type: 'implicit', + }, + ], + [mylib]: [ + { + source: mylib, + target: mylib2, + type: 'static', + }, + ], + [mylib2]: [], + [myapp]: [ + { + source: myapp, + target: mylib, + type: 'static', + }, + ], + [myappE2e]: [ + { + source: myappE2e, + target: myapp, + type: 'implicit', + }, + ], + [myapp3]: [], + }) + ); + }, 1000000); + + if (isNotWindows()) { + it('graph should output json to file by absolute path', () => { + runCLI(`graph --file=/tmp/project-graph.json`); + + expect(() => checkFilesExist('/tmp/project-graph.json')).not.toThrow(); + }, 1000000); + } + + if (isWindows()) { + it('graph should output json to file by absolute path in Windows', () => { + runCLI(`graph --file=C:\\tmp\\project-graph.json`); + + expect(fileExists('C:\\tmp\\project-graph.json')).toBeTruthy(); + }, 1000000); + } + }); +}); diff --git a/e2e/nx/src/affected-graph-setup.ts b/e2e/nx/src/affected-graph-setup.ts new file mode 100644 index 0000000000000..99714acba5f90 --- /dev/null +++ b/e2e/nx/src/affected-graph-setup.ts @@ -0,0 +1,14 @@ +import { cleanupProject, newProject } from '@nx/e2e-utils'; + +export interface AffectedGraphTestContext { + proj: string; +} + +export function setupAffectedGraphTest(): AffectedGraphTestContext { + const proj = newProject(); + return { proj }; +} + +export function cleanupAffectedGraphTest() { + cleanupProject(); +} diff --git a/e2e/nx/src/affected-graph-show-projects.test.ts b/e2e/nx/src/affected-graph-show-projects.test.ts new file mode 100644 index 0000000000000..efc895dbabd35 --- /dev/null +++ b/e2e/nx/src/affected-graph-show-projects.test.ts @@ -0,0 +1,94 @@ +import { readFile, runCLI, runCLIAsync, uniq, updateFile } from '@nx/e2e-utils'; +import { + setupAffectedGraphTest, + cleanupAffectedGraphTest, +} from './affected-graph-setup'; + +describe('show projects --affected', () => { + let proj: string; + + beforeAll(() => { + const context = setupAffectedGraphTest(); + proj = context.proj; + }); + afterAll(() => cleanupAffectedGraphTest()); + + it('should print information about affected projects', async () => { + const myapp = uniq('myapp-a'); + const myapp2 = uniq('myapp-b'); + const mylib = uniq('mylib'); + const mylib2 = uniq('mylib2'); + const mypublishablelib = uniq('mypublishablelib'); + + runCLI( + `generate @nx/web:app ${myapp} --directory=apps/${myapp} --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/web:app ${myapp2} --directory=apps/${myapp2} --unitTestRunner=vitest` + ); + runCLI(`generate @nx/js:lib ${mylib} --directory=libs/${mylib}`); + runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); + runCLI( + `generate @nx/js:lib ${mypublishablelib} --directory=libs/${mypublishablelib}` + ); + + const app1ElementSpec = readFile( + `apps/${myapp}/src/app/app.element.spec.ts` + ); + + updateFile( + `apps/${myapp}/src/app/app.element.spec.ts`, + ` + import "@${proj}/${mylib}"; + import "@${proj}/${mypublishablelib}"; + ${app1ElementSpec} + ` + ); + + const app2ElementSpec = readFile( + `apps/${myapp2}/src/app/app.element.spec.ts` + ); + + updateFile( + `apps/${myapp2}/src/app/app.element.spec.ts`, + ` + import "@${proj}/${mylib}"; + import "@${proj}/${mypublishablelib}"; + ${app2ElementSpec} + ` + ); + + const { stdout: resWithoutTarget } = await runCLIAsync( + `show projects --affected --files=apps/${myapp}/src/app/app.element.spec.ts` + ); + compareTwoArrays(resWithoutTarget.split('\n').filter(Boolean), [ + `${myapp}-e2e`, + myapp, + ]); + + const resWithTarget = JSON.parse( + ( + await runCLIAsync( + `affected -t test --files=apps/${myapp}/src/app/app.element.spec.ts --graph stdout`, + { silent: true } + ) + ).stdout.trim() + ); + + expect(resWithTarget.tasks.tasks[`${myapp}:test`]).toMatchObject({ + id: `${myapp}:test`, + overrides: {}, + target: { + project: myapp, + target: 'test', + }, + outputs: [`coverage/apps/${myapp}`], + }); + }, 120000); + + function compareTwoArrays(a: string[], b: string[]) { + expect(a.sort((x, y) => x.localeCompare(y))).toEqual( + b.sort((x, y) => x.localeCompare(y)) + ); + } +}); diff --git a/e2e/nx/src/affected-graph-with-git.test.ts b/e2e/nx/src/affected-graph-with-git.test.ts new file mode 100644 index 0000000000000..b232a37be9955 --- /dev/null +++ b/e2e/nx/src/affected-graph-with-git.test.ts @@ -0,0 +1,150 @@ +import type { NxJsonConfiguration } from '@nx/devkit'; +import { + isNotWindows, + readFile, + readJson, + runCLI, + runCommand, + uniq, + updateFile, + removeFile, +} from '@nx/e2e-utils'; +import { join } from 'path'; +import { + setupAffectedGraphTest, + cleanupAffectedGraphTest, +} from './affected-graph-setup'; + +describe('Nx Affected and Graph Tests', () => { + beforeAll(() => setupAffectedGraphTest()); + afterAll(() => cleanupAffectedGraphTest()); + + describe('affected (with git)', () => { + let myapp; + let myapp2; + let mylib; + + beforeEach(() => { + myapp = uniq('myapp'); + myapp2 = uniq('myapp'); + mylib = uniq('mylib'); + const nxJson: NxJsonConfiguration = readJson('nx.json'); + + updateFile('nx.json', JSON.stringify(nxJson)); + runCommand(`git init`); + runCommand(`git config user.email "test@test.com"`); + runCommand(`git config user.name "Test"`); + runCommand(`git config commit.gpgsign false`); + try { + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b main` + ); + } catch (e) {} + }); + + function generateAll() { + runCLI( + `generate @nx/web:app apps/${myapp} --bundler=webpack --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/web:app apps/${myapp2} --bundler=webpack --unitTestRunner=vitest` + ); + runCLI(`generate @nx/js:lib libs/${mylib}`); + runCommand(`git add . && git commit -am "add all"`); + } + + it('should not affect other projects by generating a new project', () => { + // TODO: investigate why affected gives different results on windows + if (isNotWindows()) { + runCLI(`generate @nx/web:app apps/${myapp}`); + expect(runCLI('show projects --affected')).toContain(myapp); + runCommand(`git add . && git commit -am "add ${myapp}"`); + + runCLI(`generate @nx/web:app apps/${myapp2}`); + let output = runCLI('show projects --affected'); + expect(output).not.toContain(myapp); + expect(output).toContain(myapp2); + runCommand(`git add . && git commit -am "add ${myapp2}"`); + + runCLI(`generate @nx/js:lib libs/${mylib}`); + output = runCLI('show projects --affected'); + expect(output).not.toContain(myapp); + expect(output).not.toContain(myapp2); + expect(output).toContain(mylib); + } + }, 1000000); + + it('should detect changes to projects based on tags changes', async () => { + // TODO: investigate why affected gives different results on windows + if (isNotWindows()) { + generateAll(); + updateFile(join('apps', myapp, 'project.json'), (content) => { + const data = JSON.parse(content); + data.tags = ['tag']; + return JSON.stringify(data, null, 2); + }); + const output = runCLI('show projects --affected'); + expect(output).toContain(myapp); + expect(output).not.toContain(myapp2); + expect(output).not.toContain(mylib); + } + }); + + it('should affect all projects by removing projects', async () => { + generateAll(); + const root = `libs/${mylib}`; + removeFile(root); + const output = runCLI('show projects --affected'); + expect(output).toContain(myapp); + expect(output).toContain(myapp2); + expect(output).not.toContain(mylib); + }); + + it('should detect changes to implicitly dependant projects', async () => { + generateAll(); + updateFile(join('apps', myapp, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = ['*', `!${myapp2}`]; + return JSON.stringify(data, null, 2); + }); + + runCommand('git commit -m "setup test"'); + updateFile(`libs/${mylib}/index.html`, ''); + + const output = runCLI('show projects --affected'); + + expect(output).toContain(myapp); + expect(output).not.toContain(myapp2); + expect(output).toContain(mylib); + + // Clear implicit deps to not interfere with other tests. + + updateFile(join('apps', myapp, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = []; + return JSON.stringify(data, null, 2); + }); + }); + + it('should handle file renames', () => { + generateAll(); + + // Move file + updateFile( + `apps/${myapp2}/src/index.html`, + readFile(`apps/${myapp}/src/index.html`) + ); + removeFile(`apps/${myapp}/src/index.html`); + + const affectedProjects = runCLI('show projects --affected --uncommitted'); + // .replace( + // /.*nx print-affected --uncommitted --select projects( --verbose)?\n/, + // '' + // ) + // .split(', '); + + expect(affectedProjects).toContain(myapp); + expect(affectedProjects).toContain(myapp2); + }); + }); +}); diff --git a/e2e/nx/src/affected-graph.test.ts b/e2e/nx/src/affected-graph.test.ts deleted file mode 100644 index a64df12f0edf1..0000000000000 --- a/e2e/nx/src/affected-graph.test.ts +++ /dev/null @@ -1,614 +0,0 @@ -import type { NxJsonConfiguration } from '@nx/devkit'; -import { - isNotWindows, - newProject, - readFile, - readJson, - cleanupProject, - runCLI, - runCLIAsync, - runCommand, - uniq, - updateFile, - checkFilesExist, - isWindows, - fileExists, - removeFile, -} from '@nx/e2e-utils'; -import { join } from 'path'; - -describe('Nx Affected and Graph Tests', () => { - let proj: string; - - beforeAll(() => (proj = newProject())); - afterAll(() => cleanupProject()); - - describe('affected:*', () => { - it('should print, build, and test affected apps', async () => { - process.env.CI = 'true'; - const myapp = uniq('myapp'); - const myapp2 = uniq('myapp2'); - const mylib = uniq('mylib'); - const mylib2 = uniq('mylib2'); - const mypublishablelib = uniq('mypublishablelib'); - runCLI(`generate @nx/web:app apps/${myapp} --unitTestRunner=vitest`); - runCLI(`generate @nx/web:app apps/${myapp2} --unitTestRunner=vitest`); - runCLI(`generate @nx/js:lib libs/${mylib}`); - runCLI(`generate @nx/js:lib libs/${mylib2}`); - runCLI( - `generate @nx/js:lib libs/${mypublishablelib} --publishable --importPath=@${proj}/${mypublishablelib} --tags=ui` - ); - - updateFile( - `apps/${myapp}/src/app/app.element.spec.ts`, - ` - import * as x from '@${proj}/${mylib}'; - describe('sample test', () => { - it('should test', () => { - expect(1).toEqual(1); - }); - }); - ` - ); - updateFile( - `libs/${mypublishablelib}/src/lib/${mypublishablelib}.spec.ts`, - ` - import * as x from '@${proj}/${mylib}'; - describe('sample test', () => { - it('should test', () => { - expect(1).toEqual(1); - }); - }); - ` - ); - - const affectedProjects = runCLI( - `show projects --affected --files="libs/${mylib}/src/index.ts"` - ); - expect(affectedProjects).toContain(myapp); - expect(affectedProjects).not.toContain(myapp2); - - let affectedLibs = runCLI( - `show projects --affected --files="libs/${mylib}/src/index.ts" --type lib` - ); - // type lib shows no apps - expect(affectedLibs).not.toContain(myapp); - expect(affectedLibs).not.toContain(myapp2); - expect(affectedLibs).toContain(mylib); - - const implicitlyAffectedApps = runCLI( - 'show projects --affected --files="tsconfig.base.json"' - ); - expect(implicitlyAffectedApps).toContain(myapp); - expect(implicitlyAffectedApps).toContain(myapp2); - - const noAffectedApps = runCLI( - 'show projects --affected projects --files="README.md"' - ); - expect(noAffectedApps).not.toContain(myapp); - expect(noAffectedApps).not.toContain(myapp2); - - affectedLibs = runCLI( - `show projects --affected --files="libs/${mylib}/src/index.ts"` - ); - expect(affectedLibs).toContain(mypublishablelib); - expect(affectedLibs).toContain(mylib); - expect(affectedLibs).not.toContain(mylib2); - - const implicitlyAffectedLibs = runCLI( - 'show projects --affected --files="tsconfig.base.json"' - ); - expect(implicitlyAffectedLibs).toContain(mypublishablelib); - expect(implicitlyAffectedLibs).toContain(mylib); - expect(implicitlyAffectedLibs).toContain(mylib2); - - const noAffectedLibsNonExistentFile = runCLI( - 'show projects --affected --files="tsconfig.json"' - ); - expect(noAffectedLibsNonExistentFile).not.toContain(mypublishablelib); - expect(noAffectedLibsNonExistentFile).not.toContain(mylib); - expect(noAffectedLibsNonExistentFile).not.toContain(mylib2); - - const noAffectedLibs = runCLI( - 'show projects --affected --files="README.md"' - ); - expect(noAffectedLibs).not.toContain(mypublishablelib); - expect(noAffectedLibs).not.toContain(mylib); - expect(noAffectedLibs).not.toContain(mylib2); - - // build - const build = runCLI( - `affected:build --files="libs/${mylib}/src/index.ts" --parallel` - ); - expect(build).toContain(`Running target build for 3 projects:`); - expect(build).toContain(`- ${myapp}`); - expect(build).toContain(`- ${mypublishablelib}`); - expect(build).not.toContain('is not registered with the build command'); - expect(build).toContain('Successfully ran target build'); - - const buildExcluded = runCLI( - `affected:build --files="libs/${mylib}/src/index.ts" --exclude=${myapp}` - ); - expect(buildExcluded).toContain(`Running target build for 2 projects:`); - expect(buildExcluded).toContain(`- ${mypublishablelib}`); - - const buildExcludedByTag = runCLI( - `affected:build --files="libs/${mylib}/src/index.ts" --exclude=tag:ui` - ); - expect(buildExcludedByTag).toContain( - `Running target build for 2 projects:` - ); - expect(buildExcludedByTag).not.toContain(`- ${mypublishablelib}`); - - // test - updateFile( - `apps/${myapp}/src/app/app.element.spec.ts`, - readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace( - '.toEqual(1)', - '.toEqual(2)' - ) - ); - - const failedTests = runCLI( - `affected:test --files="libs/${mylib}/src/index.ts"`, - { silenceError: true } - ); - expect(failedTests).toContain(mylib); - expect(failedTests).toContain(myapp); - expect(failedTests).toContain(mypublishablelib); - expect(failedTests).toContain(`Failed tasks:`); - - // Fix failing Unit Test - updateFile( - `apps/${myapp}/src/app/app.element.spec.ts`, - readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace( - '.toEqual(2)', - '.toEqual(1)' - ) - ); - }, 1000000); - }); - - describe('affected (with git)', () => { - let myapp; - let myapp2; - let mylib; - - beforeEach(() => { - myapp = uniq('myapp'); - myapp2 = uniq('myapp'); - mylib = uniq('mylib'); - const nxJson: NxJsonConfiguration = readJson('nx.json'); - - updateFile('nx.json', JSON.stringify(nxJson)); - runCommand(`git init`); - runCommand(`git config user.email "test@test.com"`); - runCommand(`git config user.name "Test"`); - runCommand(`git config commit.gpgsign false`); - try { - runCommand( - `git add . && git commit -am "initial commit" && git checkout -b main` - ); - } catch (e) {} - }); - - function generateAll() { - runCLI( - `generate @nx/web:app apps/${myapp} --bundler=webpack --unitTestRunner=vitest` - ); - runCLI( - `generate @nx/web:app apps/${myapp2} --bundler=webpack --unitTestRunner=vitest` - ); - runCLI(`generate @nx/js:lib libs/${mylib}`); - runCommand(`git add . && git commit -am "add all"`); - } - - it('should not affect other projects by generating a new project', () => { - // TODO: investigate why affected gives different results on windows - if (isNotWindows()) { - runCLI(`generate @nx/web:app apps/${myapp}`); - expect(runCLI('show projects --affected')).toContain(myapp); - runCommand(`git add . && git commit -am "add ${myapp}"`); - - runCLI(`generate @nx/web:app apps/${myapp2}`); - let output = runCLI('show projects --affected'); - expect(output).not.toContain(myapp); - expect(output).toContain(myapp2); - runCommand(`git add . && git commit -am "add ${myapp2}"`); - - runCLI(`generate @nx/js:lib libs/${mylib}`); - output = runCLI('show projects --affected'); - expect(output).not.toContain(myapp); - expect(output).not.toContain(myapp2); - expect(output).toContain(mylib); - } - }, 1000000); - - it('should detect changes to projects based on tags changes', async () => { - // TODO: investigate why affected gives different results on windows - if (isNotWindows()) { - generateAll(); - updateFile(join('apps', myapp, 'project.json'), (content) => { - const data = JSON.parse(content); - data.tags = ['tag']; - return JSON.stringify(data, null, 2); - }); - const output = runCLI('show projects --affected'); - expect(output).toContain(myapp); - expect(output).not.toContain(myapp2); - expect(output).not.toContain(mylib); - } - }); - - it('should affect all projects by removing projects', async () => { - generateAll(); - const root = `libs/${mylib}`; - removeFile(root); - const output = runCLI('show projects --affected'); - expect(output).toContain(myapp); - expect(output).toContain(myapp2); - expect(output).not.toContain(mylib); - }); - - it('should detect changes to implicitly dependant projects', async () => { - generateAll(); - updateFile(join('apps', myapp, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = ['*', `!${myapp2}`]; - return JSON.stringify(data, null, 2); - }); - - runCommand('git commit -m "setup test"'); - updateFile(`libs/${mylib}/index.html`, ''); - - const output = runCLI('show projects --affected'); - - expect(output).toContain(myapp); - expect(output).not.toContain(myapp2); - expect(output).toContain(mylib); - - // Clear implicit deps to not interfere with other tests. - - updateFile(join('apps', myapp, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = []; - return JSON.stringify(data, null, 2); - }); - }); - - it('should handle file renames', () => { - generateAll(); - - // Move file - updateFile( - `apps/${myapp2}/src/index.html`, - readFile(`apps/${myapp}/src/index.html`) - ); - removeFile(`apps/${myapp}/src/index.html`); - - const affectedProjects = runCLI('show projects --affected --uncommitted'); - // .replace( - // /.*nx print-affected --uncommitted --select projects( --verbose)?\n/, - // '' - // ) - // .split(', '); - - expect(affectedProjects).toContain(myapp); - expect(affectedProjects).toContain(myapp2); - }); - }); - - describe('graph', () => { - let myapp: string; - let myapp2: string; - let myapp3: string; - let myappE2e: string; - let myapp2E2e: string; - let myapp3E2e: string; - let mylib: string; - let mylib2: string; - - beforeAll(() => { - myapp = uniq('myapp'); - myapp2 = uniq('myapp2'); - myapp3 = uniq('myapp3'); - myappE2e = `${myapp}-e2e`; - myapp2E2e = `${myapp2}-e2e`; - myapp3E2e = `${myapp3}-e2e`; - mylib = uniq('mylib'); - mylib2 = uniq('mylib2'); - - runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); - runCLI(`generate @nx/web:app ${myapp2} --directory=apps/${myapp2}`); - runCLI(`generate @nx/web:app ${myapp3} --directory=apps/${myapp3}`); - runCLI(`generate @nx/js:lib ${mylib} --directory=libs/${mylib}`); - runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); - - runCommand(`git init`); - runCommand(`git config user.email "test@test.com"`); - runCommand(`git config user.name "Test"`); - runCommand(`git config commit.gpgsign false`); - runCommand( - `git add . && git commit -am "initial commit" && git checkout -b main` - ); - - updateFile( - `apps/${myapp}/src/main.ts`, - ` - import '@${proj}/${mylib}'; - - const s = {loadChildren: '@${proj}/${mylib2}'}; - ` - ); - - updateFile( - `apps/${myapp2}/src/app/app.element.spec.ts`, - `import '@${proj}/${mylib}';` - ); - - updateFile( - `libs/${mylib}/src/${mylib}.spec.ts`, - `import '@${proj}/${mylib2}';` - ); - }); - - it('graph should output json to file', () => { - runCLI(`graph --file=project-graph.json`); - - expect(() => checkFilesExist('project-graph.json')).not.toThrow(); - - const jsonFileContents = readJson('project-graph.json'); - - expect(jsonFileContents.graph.dependencies).toEqual( - expect.objectContaining({ - [myapp3E2e]: [ - { - source: myapp3E2e, - target: myapp3, - type: 'implicit', - }, - ], - [myapp2]: [ - { - source: myapp2, - target: mylib, - type: 'static', - }, - ], - [myapp2E2e]: [ - { - source: myapp2E2e, - target: myapp2, - type: 'implicit', - }, - ], - [mylib]: [ - { - source: mylib, - target: mylib2, - type: 'static', - }, - ], - [mylib2]: [], - [myapp]: [ - { - source: myapp, - target: mylib, - type: 'static', - }, - ], - [myappE2e]: [ - { - source: myappE2e, - target: myapp, - type: 'implicit', - }, - ], - [myapp3]: [], - }) - ); - }, 1000000); - - if (isNotWindows()) { - it('graph should output json to file by absolute path', () => { - runCLI(`graph --file=/tmp/project-graph.json`); - - expect(() => checkFilesExist('/tmp/project-graph.json')).not.toThrow(); - }, 1000000); - } - - if (isWindows()) { - it('graph should output json to file by absolute path in Windows', () => { - runCLI(`graph --file=C:\\tmp\\project-graph.json`); - - expect(fileExists('C:\\tmp\\project-graph.json')).toBeTruthy(); - }, 1000000); - } - - it('graph should focus requested project', () => { - runCLI(`graph --focus=${myapp} --file=project-graph.json`); - - expect(() => checkFilesExist('project-graph.json')).not.toThrow(); - - const jsonFileContents = readJson('project-graph.json'); - const projectNames = Object.keys(jsonFileContents.graph.nodes); - - expect(projectNames).toContain(myapp); - expect(projectNames).toContain(mylib); - expect(projectNames).toContain(mylib2); - expect(projectNames).toContain(myappE2e); - - expect(projectNames).not.toContain(myapp2); - expect(projectNames).not.toContain(myapp3); - expect(projectNames).not.toContain(myapp2E2e); - expect(projectNames).not.toContain(myapp3E2e); - }, 1000000); - - it('graph should exclude requested projects', () => { - runCLI( - `graph --exclude=${myappE2e},${myapp2E2e},${myapp3E2e} --file=project-graph.json` - ); - - expect(() => checkFilesExist('project-graph.json')).not.toThrow(); - - const jsonFileContents = readJson('project-graph.json'); - const projectNames = Object.keys(jsonFileContents.graph.nodes); - - expect(projectNames).toContain(myapp); - expect(projectNames).toContain(mylib); - expect(projectNames).toContain(mylib2); - expect(projectNames).toContain(myapp2); - expect(projectNames).toContain(myapp3); - - expect(projectNames).not.toContain(myappE2e); - expect(projectNames).not.toContain(myapp2E2e); - expect(projectNames).not.toContain(myapp3E2e); - }, 1000000); - - it('graph should exclude requested projects that were included by a focus', () => { - runCLI( - `graph --focus=${myapp} --exclude=${myappE2e} --file=project-graph.json` - ); - - expect(() => checkFilesExist('project-graph.json')).not.toThrow(); - - const jsonFileContents = readJson('project-graph.json'); - const projectNames = Object.keys(jsonFileContents.graph.nodes); - - expect(projectNames).toContain(myapp); - expect(projectNames).toContain(mylib); - expect(projectNames).toContain(mylib2); - - expect(projectNames).not.toContain(myappE2e); - expect(projectNames).not.toContain(myapp2); - expect(projectNames).not.toContain(myapp3); - expect(projectNames).not.toContain(myapp2E2e); - expect(projectNames).not.toContain(myapp3E2e); - }, 1000000); - - it('graph should output a deployable static website in an html file accompanied by a folder with static assets', () => { - runCLI(`graph --file=project-graph.html`); - - expect(() => checkFilesExist('project-graph.html')).not.toThrow(); - expect(() => checkFilesExist('static/styles.css')).not.toThrow(); - expect(() => checkFilesExist('static/runtime.js')).not.toThrow(); - expect(() => checkFilesExist('static/main.js')).not.toThrow(); - expect(() => checkFilesExist('static/environment.js')).not.toThrow(); - - const environmentJs = readFile('static/environment.js'); - - expect(environmentJs).toContain('window.projectGraphResponse'); - expect(environmentJs).toMatch(/"affected":\[.*\]/); - }); - - // TODO(@AgentEnder): Please re-enable this when you fix the output - xit('graph should output valid json when stdout is specified', () => { - const result = runCLI(`affected -t build --graph stdout`); - let model; - expect(() => (model = JSON.parse(result))).not.toThrow(); - expect(model).toHaveProperty('graph'); - expect(model).toHaveProperty('tasks'); - }); - - it('should include affected projects in environment file', () => { - runCLI(`graph --affected --file=project-graph.html`); - - const environmentJs = readFile('static/environment.js'); - const affectedProjects = environmentJs - .match(/"affected":\[(.*?)\]/)[1] - ?.split(','); - - expect(affectedProjects).toContain(`"${myapp}"`); - expect(affectedProjects).toContain(`"${myappE2e}"`); - expect(affectedProjects).toContain(`"${myapp2}"`); - expect(affectedProjects).toContain(`"${myapp2E2e}"`); - expect(affectedProjects).toContain(`"${mylib}"`); - }); - }); -}); - -describe('show projects --affected', () => { - let proj: string; - - beforeAll(() => (proj = newProject())); - afterAll(() => cleanupProject()); - - it('should print information about affected projects', async () => { - const myapp = uniq('myapp-a'); - const myapp2 = uniq('myapp-b'); - const mylib = uniq('mylib'); - const mylib2 = uniq('mylib2'); - const mypublishablelib = uniq('mypublishablelib'); - - runCLI( - `generate @nx/web:app ${myapp} --directory=apps/${myapp} --unitTestRunner=vitest` - ); - runCLI( - `generate @nx/web:app ${myapp2} --directory=apps/${myapp2} --unitTestRunner=vitest` - ); - runCLI(`generate @nx/js:lib ${mylib} --directory=libs/${mylib}`); - runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); - runCLI( - `generate @nx/js:lib ${mypublishablelib} --directory=libs/${mypublishablelib}` - ); - - const app1ElementSpec = readFile( - `apps/${myapp}/src/app/app.element.spec.ts` - ); - - updateFile( - `apps/${myapp}/src/app/app.element.spec.ts`, - ` - import "@${proj}/${mylib}"; - import "@${proj}/${mypublishablelib}"; - ${app1ElementSpec} - ` - ); - - const app2ElementSpec = readFile( - `apps/${myapp2}/src/app/app.element.spec.ts` - ); - - updateFile( - `apps/${myapp2}/src/app/app.element.spec.ts`, - ` - import "@${proj}/${mylib}"; - import "@${proj}/${mypublishablelib}"; - ${app2ElementSpec} - ` - ); - - const { stdout: resWithoutTarget } = await runCLIAsync( - `show projects --affected --files=apps/${myapp}/src/app/app.element.spec.ts` - ); - compareTwoArrays(resWithoutTarget.split('\n').filter(Boolean), [ - `${myapp}-e2e`, - myapp, - ]); - - const resWithTarget = JSON.parse( - ( - await runCLIAsync( - `affected -t test --files=apps/${myapp}/src/app/app.element.spec.ts --graph stdout`, - { silent: true } - ) - ).stdout.trim() - ); - - expect(resWithTarget.tasks.tasks[`${myapp}:test`]).toMatchObject({ - id: `${myapp}:test`, - overrides: {}, - target: { - project: myapp, - target: 'test', - }, - outputs: [`coverage/apps/${myapp}`], - }); - }, 120000); - - function compareTwoArrays(a: string[], b: string[]) { - expect(a.sort((x, y) => x.localeCompare(y))).toEqual( - b.sort((x, y) => x.localeCompare(y)) - ); - } -}); From 3b3b7b558fd05a6a3afd8052f2dc8d687ebde1d9 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Thu, 16 Oct 2025 13:15:04 +0100 Subject: [PATCH 02/11] chore(repo): fix failing tests --- e2e/eslint/src/linter-linting-errors.test.ts | 9 +++++---- e2e/nx/src/affected-graph-with-git.test.ts | 9 ++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/e2e/eslint/src/linter-linting-errors.test.ts b/e2e/eslint/src/linter-linting-errors.test.ts index 23440e26aaf4e..ef02dad740a8f 100644 --- a/e2e/eslint/src/linter-linting-errors.test.ts +++ b/e2e/eslint/src/linter-linting-errors.test.ts @@ -238,9 +238,10 @@ function updateGeneratedRuleImplementation( true ); - const transformer = - (context: ts.TransformationContext) => - (rootNode: T) => { + const transformer = ( + context: ts.TransformationContext + ) => + ((rootNode: T) => { function visit(node: ts.Node): ts.Node { /** * Add an ESLint messageId which will show the knownLintErrorMessage @@ -389,7 +390,7 @@ function updateGeneratedRuleImplementation( ...rootNode.statements, ]); return ts.visitNode(importAdded, visit); - }; + }) as ts.Transformer; const result: ts.TransformationResult = ts.transform(newRuleSourceFile, [transformer]); diff --git a/e2e/nx/src/affected-graph-with-git.test.ts b/e2e/nx/src/affected-graph-with-git.test.ts index b232a37be9955..2cef80ef0a3b5 100644 --- a/e2e/nx/src/affected-graph-with-git.test.ts +++ b/e2e/nx/src/affected-graph-with-git.test.ts @@ -31,15 +31,14 @@ describe('Nx Affected and Graph Tests', () => { const nxJson: NxJsonConfiguration = readJson('nx.json'); updateFile('nx.json', JSON.stringify(nxJson)); + runCommand(`rm -rf .git`); runCommand(`git init`); runCommand(`git config user.email "test@test.com"`); runCommand(`git config user.name "Test"`); runCommand(`git config commit.gpgsign false`); - try { - runCommand( - `git add . && git commit -am "initial commit" && git checkout -b main` - ); - } catch (e) {} + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b main` + ); }); function generateAll() { From a4c559fe7498a9660c9c0f933e57d6c330a13a1d Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Thu, 16 Oct 2025 14:55:38 +0100 Subject: [PATCH 03/11] chore(repo): undo ng-add split --- e2e/angular/src/ng-add-cypress-v10.test.ts | 100 ---- e2e/angular/src/ng-add-cypress-v9.test.ts | 98 ---- e2e/angular/src/ng-add-eslint.test.ts | 53 -- .../src/ng-add-generate-workspace.test.ts | 204 -------- .../src/ng-add-multiple-projects.test.ts | 90 ---- e2e/angular/src/ng-add-setup.ts | 59 --- e2e/angular/src/ng-add.test.ts | 486 ++++++++++++++++++ 7 files changed, 486 insertions(+), 604 deletions(-) delete mode 100644 e2e/angular/src/ng-add-cypress-v10.test.ts delete mode 100644 e2e/angular/src/ng-add-cypress-v9.test.ts delete mode 100644 e2e/angular/src/ng-add-eslint.test.ts delete mode 100644 e2e/angular/src/ng-add-generate-workspace.test.ts delete mode 100644 e2e/angular/src/ng-add-multiple-projects.test.ts delete mode 100644 e2e/angular/src/ng-add-setup.ts create mode 100644 e2e/angular/src/ng-add.test.ts diff --git a/e2e/angular/src/ng-add-cypress-v10.test.ts b/e2e/angular/src/ng-add-cypress-v10.test.ts deleted file mode 100644 index 4f8800d20e306..0000000000000 --- a/e2e/angular/src/ng-add-cypress-v10.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - checkFilesDoNotExist, - checkFilesExist, - packageInstall, - readJson, - runCLI, - runNgAdd, -} from '@nx/e2e-utils'; -import { - setupNgAddTest, - cleanupNgAddTest, - NgAddTestContext, -} from './ng-add-setup'; - -function addCypress10(project: string) { - runNgAdd('@cypress/schematic', '--e2e', 'latest'); - // pin latest version of Cypress that's supported by Nx to avoid flakiness - // when a new major version is released - packageInstall('cypress', null, '^14.2.1'); -} - -describe('convert Angular CLI workspace to an Nx workspace', () => { - let context: NgAddTestContext; - - beforeEach(() => { - context = setupNgAddTest(); - }); - - afterEach(() => { - cleanupNgAddTest(); - }); - - it('should handle a workspace with cypress v10', () => { - const { project } = context; - addCypress10(project); - - runCLI('g @nx/angular:ng-add --skip-install'); - - const e2eProject = `${project}-e2e`; - //check e2e project files - checkFilesDoNotExist( - 'cypress.config.ts', - 'cypress/tsconfig.json', - 'cypress/e2e/spec.cy.ts', - 'cypress/fixtures/example.json', - 'cypress/support/commands.ts', - 'cypress/support/e2e.ts' - ); - checkFilesExist( - `apps/${e2eProject}/cypress.config.ts`, - `apps/${e2eProject}/tsconfig.json`, - `apps/${e2eProject}/src/e2e/spec.cy.ts`, - `apps/${e2eProject}/src/fixtures/example.json`, - `apps/${e2eProject}/src/support/commands.ts`, - `apps/${e2eProject}/src/support/e2e.ts` - ); - - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.targets['cypress-run']).toBeUndefined(); - expect(projectConfig.targets['cypress-open']).toBeUndefined(); - expect(projectConfig.targets.e2e).toBeUndefined(); - - // check e2e project config - const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); - expect(e2eProjectConfig.targets['cypress-run']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - cypressConfig: `apps/${e2eProject}/cypress.config.ts`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - expect(e2eProjectConfig.targets['cypress-open']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.config.ts`, - }, - }); - expect(e2eProjectConfig.targets.e2e).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.config.ts`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - }); -}); diff --git a/e2e/angular/src/ng-add-cypress-v9.test.ts b/e2e/angular/src/ng-add-cypress-v9.test.ts deleted file mode 100644 index 4d775ca5290a0..0000000000000 --- a/e2e/angular/src/ng-add-cypress-v9.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - checkFilesDoNotExist, - checkFilesExist, - packageInstall, - readJson, - runCLI, - runNgAdd, -} from '@nx/e2e-utils'; -import { - setupNgAddTest, - cleanupNgAddTest, - NgAddTestContext, -} from './ng-add-setup'; - -function addCypress9(project: string) { - runNgAdd('@cypress/schematic', '--e2e-update', '1.7.0'); - packageInstall('cypress', null, '^9.0.0'); -} - -describe('convert Angular CLI workspace to an Nx workspace', () => { - let context: NgAddTestContext; - - beforeEach(() => { - context = setupNgAddTest(); - }); - - afterEach(() => { - cleanupNgAddTest(); - }); - - it('should handle a workspace with cypress v9', () => { - const { project } = context; - addCypress9(project); - - runCLI('g @nx/angular:ng-add --skip-install'); - - const e2eProject = `${project}-e2e`; - //check e2e project files - checkFilesDoNotExist( - 'cypress.json', - 'cypress/tsconfig.json', - 'cypress/integration/spec.ts', - 'cypress/plugins/index.ts', - 'cypress/support/commands.ts', - 'cypress/support/index.ts' - ); - checkFilesExist( - `apps/${e2eProject}/cypress.json`, - `apps/${e2eProject}/tsconfig.json`, - `apps/${e2eProject}/src/integration/spec.ts`, - `apps/${e2eProject}/src/plugins/index.ts`, - `apps/${e2eProject}/src/support/commands.ts`, - `apps/${e2eProject}/src/support/index.ts` - ); - - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.targets['cypress-run']).toBeUndefined(); - expect(projectConfig.targets['cypress-open']).toBeUndefined(); - expect(projectConfig.targets.e2e).toBeUndefined(); - - // check e2e project config - const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); - expect(e2eProjectConfig.targets['cypress-run']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - cypressConfig: `apps/${e2eProject}/cypress.json`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - expect(e2eProjectConfig.targets['cypress-open']).toEqual({ - executor: '@nx/cypress:cypress', - options: { - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.json`, - }, - }); - expect(e2eProjectConfig.targets.e2e).toEqual({ - executor: '@nx/cypress:cypress', - options: { - devServerTarget: `${project}:serve`, - watch: true, - headless: false, - cypressConfig: `apps/${e2eProject}/cypress.json`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - }); -}); diff --git a/e2e/angular/src/ng-add-eslint.test.ts b/e2e/angular/src/ng-add-eslint.test.ts deleted file mode 100644 index 4a95a88a57fec..0000000000000 --- a/e2e/angular/src/ng-add-eslint.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { checkFilesExist, readJson, runCLI, runNgAdd } from '@nx/e2e-utils'; -import { - setupNgAddTest, - cleanupNgAddTest, - NgAddTestContext, -} from './ng-add-setup'; - -function addEsLint(project: string) { - runNgAdd('@angular-eslint/schematics', undefined, 'latest'); -} - -describe('convert Angular CLI workspace to an Nx workspace', () => { - let context: NgAddTestContext; - - beforeEach(() => { - context = setupNgAddTest(); - }); - - afterEach(() => { - cleanupNgAddTest(); - }); - - // TODO(leo): The current Verdaccio setup fails to resolve older versions - // of @nx/* packages, the @angular-eslint/builder package depends on an - // older version of @nx/devkit so we skip this test for now. - it.skip('should handle a workspace with ESLint', () => { - const { project } = context; - addEsLint(project); - - runCLI('g @nx/angular:ng-add'); - - checkFilesExist(`apps/${project}/.eslintrc.json`, `.eslintrc.json`); - - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.targets.lint).toStrictEqual({ - executor: '@nx/eslint:lint', - }); - - let output = runCLI(`lint ${project}`); - expect(output).toContain(`> nx run ${project}:lint`); - expect(output).toContain('All files pass linting'); - expect(output).toContain( - `Successfully ran target lint for project ${project}` - ); - - output = runCLI(`lint ${project}`); - expect(output).toContain(`> nx run ${project}:lint [local cache]`); - expect(output).toContain('All files pass linting'); - expect(output).toContain( - `Successfully ran target lint for project ${project}` - ); - }); -}); diff --git a/e2e/angular/src/ng-add-generate-workspace.test.ts b/e2e/angular/src/ng-add-generate-workspace.test.ts deleted file mode 100644 index f8cb19e2311e1..0000000000000 --- a/e2e/angular/src/ng-add-generate-workspace.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { - checkFilesDoNotExist, - checkFilesExist, - packageInstall, - readJson, - runCLI, - runNgAdd, - updateFile, -} from '@nx/e2e-utils'; -import { - setupNgAddTest, - cleanupNgAddTest, - addProtractor, - NgAddTestContext, -} from './ng-add-setup'; - -describe('convert Angular CLI workspace to an Nx workspace', () => { - let context: NgAddTestContext; - - beforeEach(() => { - context = setupNgAddTest(); - }); - - afterEach(() => { - cleanupNgAddTest(); - }); - - it('should generate a workspace', () => { - const { project } = context; - addProtractor(project); - - // update package.json - const packageJson = readJson('package.json'); - packageJson.description = 'some description'; - updateFile('package.json', JSON.stringify(packageJson, null, 2)); - - // update tsconfig.json - const tsConfig = readJson('tsconfig.json'); - tsConfig.compilerOptions.paths = { a: ['b'] }; - updateFile('tsconfig.json', JSON.stringify(tsConfig, null, 2)); - - // add an extra script file - updateFile('src/scripts.js', 'const x = 1;'); - - // update angular.json - const angularJson = readJson('angular.json'); - angularJson.projects[project].architect.build.options.scripts = - angularJson.projects[project].architect.test.options.scripts = [ - 'src/scripts.js', - ]; - angularJson.projects[project].architect.test.options.styles = [ - 'src/styles.css', - ]; - updateFile('angular.json', JSON.stringify(angularJson, null, 2)); - - // confirm that @nx dependencies do not exist yet - expect(packageJson.devDependencies['@nx/workspace']).not.toBeDefined(); - - // run ng add - runCLI('g @nx/angular:ng-add --default-base main'); - - // check that prettier config exits and that files have been moved - checkFilesExist( - '.vscode/extensions.json', - '.prettierrc', - `apps/${project}/src/main.ts`, - `apps/${project}/src/app/app.config.ts`, - `apps/${project}/src/app/app.ts`, - `apps/${project}/src/app/app.routes.ts` - ); - - // check the right VSCode extensions are recommended - expect(readJson('.vscode/extensions.json').recommendations).toEqual([ - 'angular.ng-template', - 'nrwl.angular-console', - 'dbaeumer.vscode-eslint', - 'esbenp.prettier-vscode', - ]); - - // check package.json - const updatedPackageJson = readJson('package.json'); - expect(updatedPackageJson.description).toEqual('some description'); - expect(updatedPackageJson.scripts).toEqual({ - ng: 'ng', - start: 'nx serve', - build: 'nx build', - watch: 'nx build --watch --configuration development', - test: 'nx test', - }); - expect(updatedPackageJson.devDependencies['@nx/workspace']).toBeDefined(); - expect(updatedPackageJson.devDependencies['@angular/cli']).toBeDefined(); - - // check nx.json - const nxJson = readJson('nx.json'); - expect(nxJson).toEqual({ - defaultBase: 'main', - namedInputs: { - default: ['{projectRoot}/**/*', 'sharedGlobals'], - production: [ - 'default', - '!{projectRoot}/tsconfig.spec.json', - '!{projectRoot}/**/*.spec.[jt]s', - '!{projectRoot}/karma.conf.js', - ], - sharedGlobals: [], - }, - targetDefaults: { - build: { - dependsOn: ['^build'], - inputs: ['production', '^production'], - cache: true, - }, - e2e: { - inputs: ['default', '^production'], - cache: true, - }, - test: { - inputs: ['default', '^production', '{workspaceRoot}/karma.conf.js'], - cache: true, - }, - }, - }); - - // check angular.json does not exist - checkFilesDoNotExist('angular.json'); - - // check project configuration - const projectConfig = readJson(`apps/${project}/project.json`); - expect(projectConfig.sourceRoot).toEqual(`apps/${project}/src`); - expect(projectConfig.targets.build).toStrictEqual({ - executor: '@angular/build:application', - outputs: ['{options.outputPath}'], - options: { - outputPath: `dist/${project}`, - browser: `apps/${project}/src/main.ts`, - polyfills: [`zone.js`], - tsConfig: `apps/${project}/tsconfig.app.json`, - assets: [{ glob: '**/*', input: `apps/${project}/public` }], - styles: [`apps/${project}/src/styles.css`], - scripts: [`apps/${project}/src/scripts.js`], - }, - configurations: { - production: { - budgets: [ - { - type: 'initial', - maximumWarning: '500kB', - maximumError: '1MB', - }, - { - type: 'anyComponentStyle', - maximumWarning: '4kB', - maximumError: '8kB', - }, - ], - outputHashing: 'all', - }, - development: { - optimization: false, - extractLicenses: false, - sourceMap: true, - }, - }, - defaultConfiguration: 'production', - }); - expect(projectConfig.targets.serve).toEqual({ - executor: '@angular/build:dev-server', - configurations: { - production: { buildTarget: `${project}:build:production` }, - development: { buildTarget: `${project}:build:development` }, - }, - defaultConfiguration: 'development', - }); - expect(projectConfig.targets.test).toStrictEqual({ - executor: '@angular/build:karma', - options: { - polyfills: [`zone.js`, `zone.js/testing`], - tsConfig: `apps/${project}/tsconfig.spec.json`, - assets: [{ glob: '**/*', input: `apps/${project}/public` }], - styles: [`apps/${project}/src/styles.css`], - scripts: [`apps/${project}/src/scripts.js`], - }, - }); - expect(projectConfig.targets.e2e).toBeUndefined(); - - // check e2e project config - const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); - expect(e2eProjectConfig.targets.e2e).toEqual({ - executor: '@angular-devkit/build-angular:protractor', - options: { - protractorConfig: `apps/${project}-e2e/protractor.conf.js`, - devServerTarget: `${project}:serve`, - }, - configurations: { - production: { - devServerTarget: `${project}:serve:production`, - }, - }, - }); - - runCLI(`build ${project} --configuration production --outputHashing none`); - checkFilesExist(`dist/${project}/browser/main.js`); - }); -}); diff --git a/e2e/angular/src/ng-add-multiple-projects.test.ts b/e2e/angular/src/ng-add-multiple-projects.test.ts deleted file mode 100644 index 404cc68ca2d2f..0000000000000 --- a/e2e/angular/src/ng-add-multiple-projects.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - checkFilesDoNotExist, - checkFilesExist, - runCLI, - runCommand, - uniq, -} from '@nx/e2e-utils'; -import { - setupNgAddTest, - cleanupNgAddTest, - NgAddTestContext, -} from './ng-add-setup'; - -describe('convert Angular CLI workspace to an Nx workspace', () => { - let context: NgAddTestContext; - - beforeEach(() => { - context = setupNgAddTest(); - }); - - afterEach(() => { - cleanupNgAddTest(); - }); - - it('should support a workspace with multiple projects', () => { - const { project } = context; - // add other projects - const app1 = uniq('app1'); - const lib1 = uniq('lib1'); - runCommand(`ng g @schematics/angular:application ${app1} --no-interactive`); - runCommand(`ng g @schematics/angular:library ${lib1} --no-interactive`); - - runCLI('g @nx/angular:ng-add'); - - // check angular.json does not exist - checkFilesDoNotExist('angular.json'); - - // check building project - let output = runCLI(`build ${project} --outputHashing none`); - expect(output).toContain( - `> nx run ${project}:build:production --outputHashing none` - ); - expect(output).toContain( - `Successfully ran target build for project ${project}` - ); - checkFilesExist(`dist/${project}/browser/main.js`); - - output = runCLI(`build ${project} --outputHashing none`); - expect(output).toContain( - `> nx run ${project}:build:production --outputHashing none [local cache]` - ); - expect(output).toContain( - `Successfully ran target build for project ${project}` - ); - - // check building app1 - output = runCLI(`build ${app1} --outputHashing none`); - expect(output).toContain( - `> nx run ${app1}:build:production --outputHashing none` - ); - expect(output).toContain( - `Successfully ran target build for project ${app1}` - ); - checkFilesExist(`dist/${app1}/browser/main.js`); - - output = runCLI(`build ${app1} --outputHashing none`); - expect(output).toContain( - `> nx run ${app1}:build:production --outputHashing none [local cache]` - ); - expect(output).toContain( - `Successfully ran target build for project ${app1}` - ); - - // check building lib1 - output = runCLI(`build ${lib1}`); - expect(output).toContain(`> nx run ${lib1}:build:production`); - expect(output).toContain( - `Successfully ran target build for project ${lib1}` - ); - checkFilesExist(`dist/${lib1}/package.json`); - - output = runCLI(`build ${lib1}`); - expect(output).toContain( - `> nx run ${lib1}:build:production [local cache]` - ); - expect(output).toContain( - `Successfully ran target build for project ${lib1}` - ); - }); -}); diff --git a/e2e/angular/src/ng-add-setup.ts b/e2e/angular/src/ng-add-setup.ts deleted file mode 100644 index 93ab92a0c84d0..0000000000000 --- a/e2e/angular/src/ng-add-setup.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - cleanupProject, - getSelectedPackageManager, - packageInstall, - readJson, - runNgNew, - uniq, - updateFile, -} from '@nx/e2e-utils'; -import { PackageManager } from 'nx/src/utils/package-manager'; - -export interface NgAddTestContext { - project: string; - packageManager: PackageManager; -} - -export function setupNgAddTest(): NgAddTestContext { - let packageManager = getSelectedPackageManager(); - // TODO: solve issues with pnpm and remove this fallback - packageManager = packageManager === 'pnpm' ? 'yarn' : packageManager; - const project = runNgNew(packageManager); - packageInstall('nx', null, 'latest'); - packageInstall('@nx/angular', null, 'latest'); - - return { project, packageManager }; -} - -export function cleanupNgAddTest() { - cleanupProject(); -} - -export function addProtractor(project: string) { - updateFile('e2e/protractor.conf.js', 'exports.config = {};'); - updateFile( - 'e2e/tsconfig.json', - JSON.stringify({ extends: '../tsconfig.json' }, null, 2) - ); - updateFile( - 'e2e/src/app.e2e-spec.ts', - `describe('app', () => { - it('should pass', () => { - expect(true).toBe(true); - }); - });` - ); - - const angularJson = readJson('angular.json'); - angularJson.projects[project].architect.e2e = { - builder: '@angular-devkit/build-angular:protractor', - options: { - protractorConfig: 'e2e/protractor.conf.js', - devServerTarget: `${project}:serve`, - }, - configurations: { - production: { devServerTarget: `${project}:serve:production` }, - }, - }; - updateFile('angular.json', JSON.stringify(angularJson, null, 2)); -} diff --git a/e2e/angular/src/ng-add.test.ts b/e2e/angular/src/ng-add.test.ts new file mode 100644 index 0000000000000..c867764c875d7 --- /dev/null +++ b/e2e/angular/src/ng-add.test.ts @@ -0,0 +1,486 @@ +import { + checkFilesDoNotExist, + checkFilesExist, + cleanupProject, + getSelectedPackageManager, + packageInstall, + readJson, + runCLI, + runCommand, + runNgAdd, + runNgNew, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { PackageManager } from 'nx/src/utils/package-manager'; + +describe('convert Angular CLI workspace to an Nx workspace', () => { + let project: string; + let packageManager: PackageManager; + + // utility to manually add protractor since it's not generated + // in the latest Angular CLI versions, but older projects updated + // to latest versions might still have it + function addProtractor() { + updateFile('e2e/protractor.conf.js', 'exports.config = {};'); + updateFile( + 'e2e/tsconfig.json', + JSON.stringify({ extends: '../tsconfig.json' }, null, 2) + ); + updateFile( + 'e2e/src/app.e2e-spec.ts', + `describe('app', () => { + it('should pass', () => { + expect(true).toBe(true); + }); + });` + ); + + const angularJson = readJson('angular.json'); + angularJson.projects[project].architect.e2e = { + builder: '@angular-devkit/build-angular:protractor', + options: { + protractorConfig: 'e2e/protractor.conf.js', + devServerTarget: `${project}:serve`, + }, + configurations: { + production: { devServerTarget: `${project}:serve:production` }, + }, + }; + updateFile('angular.json', JSON.stringify(angularJson, null, 2)); + } + + function addCypress9() { + runNgAdd('@cypress/schematic', '--e2e-update', '1.7.0'); + packageInstall('cypress', null, '^9.0.0'); + } + + function addCypress10() { + runNgAdd('@cypress/schematic', '--e2e', 'latest'); + // pin latest version of Cypress that's supported by Nx to avoid flakiness + // when a new major version is released + packageInstall('cypress', null, '^14.2.1'); + } + + function addEsLint() { + runNgAdd('@angular-eslint/schematics', undefined, 'latest'); + } + + beforeEach(() => { + packageManager = getSelectedPackageManager(); + // TODO: solve issues with pnpm and remove this fallback + packageManager = packageManager === 'pnpm' ? 'yarn' : packageManager; + project = runNgNew(packageManager); + packageInstall('nx', null, 'latest'); + packageInstall('@nx/angular', null, 'latest'); + }); + + afterEach(() => { + cleanupProject(); + }); + + it('should generate a workspace', () => { + addProtractor(); + + // update package.json + const packageJson = readJson('package.json'); + packageJson.description = 'some description'; + updateFile('package.json', JSON.stringify(packageJson, null, 2)); + + // update tsconfig.json + const tsConfig = readJson('tsconfig.json'); + tsConfig.compilerOptions.paths = { a: ['b'] }; + updateFile('tsconfig.json', JSON.stringify(tsConfig, null, 2)); + + // add an extra script file + updateFile('src/scripts.js', 'const x = 1;'); + + // update angular.json + const angularJson = readJson('angular.json'); + angularJson.projects[project].architect.build.options.scripts = + angularJson.projects[project].architect.test.options.scripts = [ + 'src/scripts.js', + ]; + angularJson.projects[project].architect.test.options.styles = [ + 'src/styles.css', + ]; + updateFile('angular.json', JSON.stringify(angularJson, null, 2)); + + // confirm that @nx dependencies do not exist yet + expect(packageJson.devDependencies['@nx/workspace']).not.toBeDefined(); + + // run ng add + runCLI('g @nx/angular:ng-add --default-base main'); + + // check that prettier config exits and that files have been moved + checkFilesExist( + '.vscode/extensions.json', + '.prettierrc', + `apps/${project}/src/main.ts`, + `apps/${project}/src/app/app.config.ts`, + `apps/${project}/src/app/app.ts`, + `apps/${project}/src/app/app.routes.ts` + ); + + // check the right VSCode extensions are recommended + expect(readJson('.vscode/extensions.json').recommendations).toEqual([ + 'angular.ng-template', + 'nrwl.angular-console', + 'dbaeumer.vscode-eslint', + 'esbenp.prettier-vscode', + ]); + + // check package.json + const updatedPackageJson = readJson('package.json'); + expect(updatedPackageJson.description).toEqual('some description'); + expect(updatedPackageJson.scripts).toEqual({ + ng: 'ng', + start: 'nx serve', + build: 'nx build', + watch: 'nx build --watch --configuration development', + test: 'nx test', + }); + expect(updatedPackageJson.devDependencies['@nx/workspace']).toBeDefined(); + expect(updatedPackageJson.devDependencies['@angular/cli']).toBeDefined(); + + // check nx.json + const nxJson = readJson('nx.json'); + expect(nxJson).toEqual({ + defaultBase: 'main', + namedInputs: { + default: ['{projectRoot}/**/*', 'sharedGlobals'], + production: [ + 'default', + '!{projectRoot}/tsconfig.spec.json', + '!{projectRoot}/**/*.spec.[jt]s', + '!{projectRoot}/karma.conf.js', + ], + sharedGlobals: [], + }, + targetDefaults: { + build: { + dependsOn: ['^build'], + inputs: ['production', '^production'], + cache: true, + }, + e2e: { + inputs: ['default', '^production'], + cache: true, + }, + test: { + inputs: ['default', '^production', '{workspaceRoot}/karma.conf.js'], + cache: true, + }, + }, + }); + + // check angular.json does not exist + checkFilesDoNotExist('angular.json'); + + // check project configuration + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.sourceRoot).toEqual(`apps/${project}/src`); + expect(projectConfig.targets.build).toStrictEqual({ + executor: '@angular/build:application', + outputs: ['{options.outputPath}'], + options: { + outputPath: `dist/${project}`, + browser: `apps/${project}/src/main.ts`, + polyfills: [`zone.js`], + tsConfig: `apps/${project}/tsconfig.app.json`, + assets: [{ glob: '**/*', input: `apps/${project}/public` }], + styles: [`apps/${project}/src/styles.css`], + scripts: [`apps/${project}/src/scripts.js`], + }, + configurations: { + production: { + budgets: [ + { + type: 'initial', + maximumWarning: '500kB', + maximumError: '1MB', + }, + { + type: 'anyComponentStyle', + maximumWarning: '4kB', + maximumError: '8kB', + }, + ], + outputHashing: 'all', + }, + development: { + optimization: false, + extractLicenses: false, + sourceMap: true, + }, + }, + defaultConfiguration: 'production', + }); + expect(projectConfig.targets.serve).toEqual({ + executor: '@angular/build:dev-server', + configurations: { + production: { buildTarget: `${project}:build:production` }, + development: { buildTarget: `${project}:build:development` }, + }, + defaultConfiguration: 'development', + }); + expect(projectConfig.targets.test).toStrictEqual({ + executor: '@angular/build:karma', + options: { + polyfills: [`zone.js`, `zone.js/testing`], + tsConfig: `apps/${project}/tsconfig.spec.json`, + assets: [{ glob: '**/*', input: `apps/${project}/public` }], + styles: [`apps/${project}/src/styles.css`], + scripts: [`apps/${project}/src/scripts.js`], + }, + }); + expect(projectConfig.targets.e2e).toBeUndefined(); + + // check e2e project config + const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); + expect(e2eProjectConfig.targets.e2e).toEqual({ + executor: '@angular-devkit/build-angular:protractor', + options: { + protractorConfig: `apps/${project}-e2e/protractor.conf.js`, + devServerTarget: `${project}:serve`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + + runCLI(`build ${project} --configuration production --outputHashing none`); + checkFilesExist(`dist/${project}/browser/main.js`); + }); + + it('should handle a workspace with cypress v9', () => { + addCypress9(); + + runCLI('g @nx/angular:ng-add --skip-install'); + + const e2eProject = `${project}-e2e`; + //check e2e project files + checkFilesDoNotExist( + 'cypress.json', + 'cypress/tsconfig.json', + 'cypress/integration/spec.ts', + 'cypress/plugins/index.ts', + 'cypress/support/commands.ts', + 'cypress/support/index.ts' + ); + checkFilesExist( + `apps/${e2eProject}/cypress.json`, + `apps/${e2eProject}/tsconfig.json`, + `apps/${e2eProject}/src/integration/spec.ts`, + `apps/${e2eProject}/src/plugins/index.ts`, + `apps/${e2eProject}/src/support/commands.ts`, + `apps/${e2eProject}/src/support/index.ts` + ); + + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.targets['cypress-run']).toBeUndefined(); + expect(projectConfig.targets['cypress-open']).toBeUndefined(); + expect(projectConfig.targets.e2e).toBeUndefined(); + + // check e2e project config + const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); + expect(e2eProjectConfig.targets['cypress-run']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + cypressConfig: `apps/${e2eProject}/cypress.json`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + expect(e2eProjectConfig.targets['cypress-open']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.json`, + }, + }); + expect(e2eProjectConfig.targets.e2e).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.json`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + }); + + it('should handle a workspace with cypress v10', () => { + addCypress10(); + + runCLI('g @nx/angular:ng-add --skip-install'); + + const e2eProject = `${project}-e2e`; + //check e2e project files + checkFilesDoNotExist( + 'cypress.config.ts', + 'cypress/tsconfig.json', + 'cypress/e2e/spec.cy.ts', + 'cypress/fixtures/example.json', + 'cypress/support/commands.ts', + 'cypress/support/e2e.ts' + ); + checkFilesExist( + `apps/${e2eProject}/cypress.config.ts`, + `apps/${e2eProject}/tsconfig.json`, + `apps/${e2eProject}/src/e2e/spec.cy.ts`, + `apps/${e2eProject}/src/fixtures/example.json`, + `apps/${e2eProject}/src/support/commands.ts`, + `apps/${e2eProject}/src/support/e2e.ts` + ); + + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.targets['cypress-run']).toBeUndefined(); + expect(projectConfig.targets['cypress-open']).toBeUndefined(); + expect(projectConfig.targets.e2e).toBeUndefined(); + + // check e2e project config + const e2eProjectConfig = readJson(`apps/${project}-e2e/project.json`); + expect(e2eProjectConfig.targets['cypress-run']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + cypressConfig: `apps/${e2eProject}/cypress.config.ts`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + expect(e2eProjectConfig.targets['cypress-open']).toEqual({ + executor: '@nx/cypress:cypress', + options: { + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.config.ts`, + }, + }); + expect(e2eProjectConfig.targets.e2e).toEqual({ + executor: '@nx/cypress:cypress', + options: { + devServerTarget: `${project}:serve`, + watch: true, + headless: false, + cypressConfig: `apps/${e2eProject}/cypress.config.ts`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }); + }); + + // TODO(leo): The current Verdaccio setup fails to resolve older versions + // of @nx/* packages, the @angular-eslint/builder package depends on an + // older version of @nx/devkit so we skip this test for now. + it.skip('should handle a workspace with ESLint', () => { + addEsLint(); + + runCLI('g @nx/angular:ng-add'); + + checkFilesExist(`apps/${project}/.eslintrc.json`, `.eslintrc.json`); + + const projectConfig = readJson(`apps/${project}/project.json`); + expect(projectConfig.targets.lint).toStrictEqual({ + executor: '@nx/eslint:lint', + }); + + let output = runCLI(`lint ${project}`); + expect(output).toContain(`> nx run ${project}:lint`); + expect(output).toContain('All files pass linting'); + expect(output).toContain( + `Successfully ran target lint for project ${project}` + ); + + output = runCLI(`lint ${project}`); + expect(output).toContain(`> nx run ${project}:lint [local cache]`); + expect(output).toContain('All files pass linting'); + expect(output).toContain( + `Successfully ran target lint for project ${project}` + ); + }); + + it('should support a workspace with multiple projects', () => { + // add other projects + const app1 = uniq('app1'); + const lib1 = uniq('lib1'); + runCommand(`ng g @schematics/angular:application ${app1} --no-interactive`); + runCommand(`ng g @schematics/angular:library ${lib1} --no-interactive`); + + runCLI('g @nx/angular:ng-add'); + + // check angular.json does not exist + checkFilesDoNotExist('angular.json'); + + // check building project + let output = runCLI(`build ${project} --outputHashing none`); + expect(output).toContain( + `> nx run ${project}:build:production --outputHashing none` + ); + expect(output).toContain( + `Successfully ran target build for project ${project}` + ); + checkFilesExist(`dist/${project}/browser/main.js`); + + output = runCLI(`build ${project} --outputHashing none`); + expect(output).toContain( + `> nx run ${project}:build:production --outputHashing none [local cache]` + ); + expect(output).toContain( + `Successfully ran target build for project ${project}` + ); + + // check building app1 + output = runCLI(`build ${app1} --outputHashing none`); + expect(output).toContain( + `> nx run ${app1}:build:production --outputHashing none` + ); + expect(output).toContain( + `Successfully ran target build for project ${app1}` + ); + checkFilesExist(`dist/${app1}/browser/main.js`); + + output = runCLI(`build ${app1} --outputHashing none`); + expect(output).toContain( + `> nx run ${app1}:build:production --outputHashing none [local cache]` + ); + expect(output).toContain( + `Successfully ran target build for project ${app1}` + ); + + // check building lib1 + output = runCLI(`build ${lib1}`); + expect(output).toContain(`> nx run ${lib1}:build:production`); + expect(output).toContain( + `Successfully ran target build for project ${lib1}` + ); + checkFilesExist(`dist/${lib1}/package.json`); + + output = runCLI(`build ${lib1}`); + expect(output).toContain( + `> nx run ${lib1}:build:production [local cache]` + ); + expect(output).toContain( + `Successfully ran target build for project ${lib1}` + ); + }); +}); From 8632f17d80e3107696a982da395e61e3c093e575 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Thu, 16 Oct 2025 15:52:25 +0100 Subject: [PATCH 04/11] chore(repo): fix failing tests --- e2e/angular/tsconfig.spec.json | 3 ++- e2e/cypress/tsconfig.spec.json | 3 ++- e2e/eslint/src/linter-dependency-checks.test.ts | 10 +++++----- e2e/eslint/tsconfig.spec.json | 3 ++- e2e/nx/tsconfig.spec.json | 3 ++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/e2e/angular/tsconfig.spec.json b/e2e/angular/tsconfig.spec.json index 3a6cc7d023dbc..9769430ed933c 100644 --- a/e2e/angular/tsconfig.spec.json +++ b/e2e/angular/tsconfig.spec.json @@ -14,6 +14,7 @@ "**/*.spec.jsx", "**/*.test.jsx", "**/*.d.ts", - "jest.config.ts" + "jest.config.ts", + "**/*-setup.ts" ] } diff --git a/e2e/cypress/tsconfig.spec.json b/e2e/cypress/tsconfig.spec.json index 3a6cc7d023dbc..9769430ed933c 100644 --- a/e2e/cypress/tsconfig.spec.json +++ b/e2e/cypress/tsconfig.spec.json @@ -14,6 +14,7 @@ "**/*.spec.jsx", "**/*.test.jsx", "**/*.d.ts", - "jest.config.ts" + "jest.config.ts", + "**/*-setup.ts" ] } diff --git a/e2e/eslint/src/linter-dependency-checks.test.ts b/e2e/eslint/src/linter-dependency-checks.test.ts index fb50ef9792435..2b4d78db5606c 100644 --- a/e2e/eslint/src/linter-dependency-checks.test.ts +++ b/e2e/eslint/src/linter-dependency-checks.test.ts @@ -64,7 +64,7 @@ describe('Linter', () => { rootPackageJson.dependencies['tslib'] || rootPackageJson.devDependencies['tslib']; - let out = runCLI(`lint ${mylib}`, { + let out = runCLI(`lint ${mylib} --skip-nx-cache`, { silenceError: true, env: { CI: 'false' }, }); @@ -84,7 +84,7 @@ describe('Linter', () => { }); // output should now report missing dependency and obsolete dependency - out = runCLI(`lint ${mylib}`, { + out = runCLI(`lint ${mylib} --skip-nx-cache`, { silenceError: true, env: { CI: 'false' }, }); @@ -95,7 +95,7 @@ describe('Linter', () => { ); // should fix the missing and obsolete dependency issues - out = runCLI(`lint ${mylib} --fix`, { + out = runCLI(`lint ${mylib} --fix --skip-nx-cache`, { silenceError: true, env: { CI: 'false' }, }); @@ -121,7 +121,7 @@ describe('Linter', () => { json.dependencies['@nx/devkit'] = '100.0.0'; return json; }); - out = runCLI(`lint ${mylib}`, { + out = runCLI(`lint ${mylib} --skip-nx-cache`, { silenceError: true, env: { CI: 'false' }, }); @@ -130,7 +130,7 @@ describe('Linter', () => { ); // should fix the version mismatch issue - out = runCLI(`lint ${mylib} --fix`, { + out = runCLI(`lint ${mylib} --fix --skip-nx-cache`, { silenceError: true, env: { CI: 'false' }, }); diff --git a/e2e/eslint/tsconfig.spec.json b/e2e/eslint/tsconfig.spec.json index 3a6cc7d023dbc..9769430ed933c 100644 --- a/e2e/eslint/tsconfig.spec.json +++ b/e2e/eslint/tsconfig.spec.json @@ -14,6 +14,7 @@ "**/*.spec.jsx", "**/*.test.jsx", "**/*.d.ts", - "jest.config.ts" + "jest.config.ts", + "**/*-setup.ts" ] } diff --git a/e2e/nx/tsconfig.spec.json b/e2e/nx/tsconfig.spec.json index 3a6cc7d023dbc..9769430ed933c 100644 --- a/e2e/nx/tsconfig.spec.json +++ b/e2e/nx/tsconfig.spec.json @@ -14,6 +14,7 @@ "**/*.spec.jsx", "**/*.test.jsx", "**/*.d.ts", - "jest.config.ts" + "jest.config.ts", + "**/*-setup.ts" ] } From 5baba3df361572644ad935c207299902d9e243b7 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Thu, 16 Oct 2025 16:10:55 +0100 Subject: [PATCH 05/11] chore(repo): fix failing tests --- e2e/nx/src/affected-graph-with-git.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/e2e/nx/src/affected-graph-with-git.test.ts b/e2e/nx/src/affected-graph-with-git.test.ts index 2cef80ef0a3b5..46b2f4097c1f5 100644 --- a/e2e/nx/src/affected-graph-with-git.test.ts +++ b/e2e/nx/src/affected-graph-with-git.test.ts @@ -31,14 +31,18 @@ describe('Nx Affected and Graph Tests', () => { const nxJson: NxJsonConfiguration = readJson('nx.json'); updateFile('nx.json', JSON.stringify(nxJson)); - runCommand(`rm -rf .git`); + // clean up any projects from previous tests + runCommand(`rm -rf apps libs`); + runCommand(`mkdir -p apps libs`); runCommand(`git init`); runCommand(`git config user.email "test@test.com"`); runCommand(`git config user.name "Test"`); runCommand(`git config commit.gpgsign false`); - runCommand( - `git add . && git commit -am "initial commit" && git checkout -b main` - ); + try { + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b main` + ); + } catch (e) {} }); function generateAll() { @@ -107,7 +111,7 @@ describe('Nx Affected and Graph Tests', () => { return JSON.stringify(data, null, 2); }); - runCommand('git commit -m "setup test"'); + runCommand('git add . && git commit -m "setup test"'); updateFile(`libs/${mylib}/index.html`, ''); const output = runCLI('show projects --affected'); From 51869f22d0c238738490845c8504b928d875b42f Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Thu, 16 Oct 2025 17:08:44 +0100 Subject: [PATCH 06/11] chore(repo): fix failing tests --- e2e/angular/tsconfig.spec.json | 3 +-- e2e/nx/src/affected-graph-with-git.test.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e/angular/tsconfig.spec.json b/e2e/angular/tsconfig.spec.json index 9769430ed933c..3a6cc7d023dbc 100644 --- a/e2e/angular/tsconfig.spec.json +++ b/e2e/angular/tsconfig.spec.json @@ -14,7 +14,6 @@ "**/*.spec.jsx", "**/*.test.jsx", "**/*.d.ts", - "jest.config.ts", - "**/*-setup.ts" + "jest.config.ts" ] } diff --git a/e2e/nx/src/affected-graph-with-git.test.ts b/e2e/nx/src/affected-graph-with-git.test.ts index 46b2f4097c1f5..ebb7daff0a477 100644 --- a/e2e/nx/src/affected-graph-with-git.test.ts +++ b/e2e/nx/src/affected-graph-with-git.test.ts @@ -40,7 +40,7 @@ describe('Nx Affected and Graph Tests', () => { runCommand(`git config commit.gpgsign false`); try { runCommand( - `git add . && git commit -am "initial commit" && git checkout -b main` + `git add . && git commit -am "initial commit" && git checkout -B main` ); } catch (e) {} }); From a7b5713129974befcea4c621781ce1afc54b2098 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Thu, 16 Oct 2025 17:58:22 +0100 Subject: [PATCH 07/11] chore(repo): fix failing tests --- e2e/nx/src/affected-graph-with-git.test.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/e2e/nx/src/affected-graph-with-git.test.ts b/e2e/nx/src/affected-graph-with-git.test.ts index ebb7daff0a477..bab7b935d91f1 100644 --- a/e2e/nx/src/affected-graph-with-git.test.ts +++ b/e2e/nx/src/affected-graph-with-git.test.ts @@ -8,16 +8,14 @@ import { uniq, updateFile, removeFile, + newProject, + cleanupProject, } from '@nx/e2e-utils'; import { join } from 'path'; -import { - setupAffectedGraphTest, - cleanupAffectedGraphTest, -} from './affected-graph-setup'; describe('Nx Affected and Graph Tests', () => { - beforeAll(() => setupAffectedGraphTest()); - afterAll(() => cleanupAffectedGraphTest()); + beforeAll(() => newProject()); + afterAll(() => cleanupProject()); describe('affected (with git)', () => { let myapp; @@ -31,9 +29,6 @@ describe('Nx Affected and Graph Tests', () => { const nxJson: NxJsonConfiguration = readJson('nx.json'); updateFile('nx.json', JSON.stringify(nxJson)); - // clean up any projects from previous tests - runCommand(`rm -rf apps libs`); - runCommand(`mkdir -p apps libs`); runCommand(`git init`); runCommand(`git config user.email "test@test.com"`); runCommand(`git config user.name "Test"`); From 8c1461bd992368393b84779111d6d5d5b04cfc50 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Thu, 16 Oct 2025 18:18:46 +0100 Subject: [PATCH 08/11] chore(repo): combine git tests --- .../src/affected-graph-affected-star.test.ts | 136 +++++++++++++++- e2e/nx/src/affected-graph-with-git.test.ts | 148 ------------------ 2 files changed, 135 insertions(+), 149 deletions(-) delete mode 100644 e2e/nx/src/affected-graph-with-git.test.ts diff --git a/e2e/nx/src/affected-graph-affected-star.test.ts b/e2e/nx/src/affected-graph-affected-star.test.ts index e82f164a0724e..4d8597bab7c5d 100644 --- a/e2e/nx/src/affected-graph-affected-star.test.ts +++ b/e2e/nx/src/affected-graph-affected-star.test.ts @@ -1,4 +1,14 @@ -import { readFile, runCLI, uniq, updateFile } from '@nx/e2e-utils'; +import { + readFile, + runCLI, + uniq, + updateFile, + isNotWindows, + runCommand, + removeFile, + readJson, +} from '@nx/e2e-utils'; +import { join } from 'path'; import type { NxJsonConfiguration } from '@nx/devkit'; import { setupAffectedGraphTest, @@ -159,4 +169,128 @@ describe('Nx Affected and Graph Tests', () => { ); }, 1000000); }); + + describe('affected (with git)', () => { + let myapp; + let myapp2; + let mylib; + + beforeEach(() => { + myapp = uniq('myapp'); + myapp2 = uniq('myapp'); + mylib = uniq('mylib'); + const nxJson: NxJsonConfiguration = readJson('nx.json'); + + updateFile('nx.json', JSON.stringify(nxJson)); + runCommand(`git init`); + runCommand(`git config user.email "test@test.com"`); + runCommand(`git config user.name "Test"`); + runCommand(`git config commit.gpgsign false`); + try { + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b main` + ); + } catch (e) {} + }); + + function generateAll() { + runCLI( + `generate @nx/web:app apps/${myapp} --bundler=webpack --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/web:app apps/${myapp2} --bundler=webpack --unitTestRunner=vitest` + ); + runCLI(`generate @nx/js:lib libs/${mylib}`); + runCommand(`git add . && git commit -am "add all"`); + } + + it('should not affect other projects by generating a new project', () => { + // TODO: investigate why affected gives different results on windows + if (isNotWindows()) { + runCLI(`generate @nx/web:app apps/${myapp}`); + expect(runCLI('show projects --affected')).toContain(myapp); + runCommand(`git add . && git commit -am "add ${myapp}"`); + + runCLI(`generate @nx/web:app apps/${myapp2}`); + let output = runCLI('show projects --affected'); + expect(output).not.toContain(myapp); + expect(output).toContain(myapp2); + runCommand(`git add . && git commit -am "add ${myapp2}"`); + + runCLI(`generate @nx/js:lib libs/${mylib}`); + output = runCLI('show projects --affected'); + expect(output).not.toContain(myapp); + expect(output).not.toContain(myapp2); + expect(output).toContain(mylib); + } + }, 1000000); + + it('should detect changes to projects based on tags changes', async () => { + // TODO: investigate why affected gives different results on windows + if (isNotWindows()) { + generateAll(); + updateFile(join('apps', myapp, 'project.json'), (content) => { + const data = JSON.parse(content); + data.tags = ['tag']; + return JSON.stringify(data, null, 2); + }); + const output = runCLI('show projects --affected'); + expect(output).toContain(myapp); + expect(output).not.toContain(myapp2); + expect(output).not.toContain(mylib); + } + }); + + it('should affect all projects by removing projects', async () => { + generateAll(); + const root = `libs/${mylib}`; + removeFile(root); + const output = runCLI('show projects --affected'); + expect(output).toContain(myapp); + expect(output).toContain(myapp2); + expect(output).not.toContain(mylib); + }); + + it('should detect changes to implicitly dependant projects', async () => { + generateAll(); + updateFile(join('apps', myapp, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = ['*', `!${myapp2}`]; + return JSON.stringify(data, null, 2); + }); + + runCommand('git commit -m "setup test"'); + updateFile(`libs/${mylib}/index.html`, ''); + + const output = runCLI('show projects --affected'); + + expect(output).toContain(myapp); + expect(output).not.toContain(myapp2); + expect(output).toContain(mylib); + + // Clear implicit deps to not interfere with other tests. + + updateFile(join('apps', myapp, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = []; + return JSON.stringify(data, null, 2); + }); + }); + + it('should handle file renames', () => { + generateAll(); + + // Move file + updateFile( + `apps/${myapp2}/src/index.html`, + readFile(`apps/${myapp}/src/index.html`) + ); + removeFile(`apps/${myapp}/src/index.html`); + + const affectedProjects = runCLI('show projects --affected --uncommitted'); + + expect(affectedProjects).toContain(myapp); + expect(affectedProjects).toContain(myapp2); + }); + }); }); diff --git a/e2e/nx/src/affected-graph-with-git.test.ts b/e2e/nx/src/affected-graph-with-git.test.ts deleted file mode 100644 index bab7b935d91f1..0000000000000 --- a/e2e/nx/src/affected-graph-with-git.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { NxJsonConfiguration } from '@nx/devkit'; -import { - isNotWindows, - readFile, - readJson, - runCLI, - runCommand, - uniq, - updateFile, - removeFile, - newProject, - cleanupProject, -} from '@nx/e2e-utils'; -import { join } from 'path'; - -describe('Nx Affected and Graph Tests', () => { - beforeAll(() => newProject()); - afterAll(() => cleanupProject()); - - describe('affected (with git)', () => { - let myapp; - let myapp2; - let mylib; - - beforeEach(() => { - myapp = uniq('myapp'); - myapp2 = uniq('myapp'); - mylib = uniq('mylib'); - const nxJson: NxJsonConfiguration = readJson('nx.json'); - - updateFile('nx.json', JSON.stringify(nxJson)); - runCommand(`git init`); - runCommand(`git config user.email "test@test.com"`); - runCommand(`git config user.name "Test"`); - runCommand(`git config commit.gpgsign false`); - try { - runCommand( - `git add . && git commit -am "initial commit" && git checkout -B main` - ); - } catch (e) {} - }); - - function generateAll() { - runCLI( - `generate @nx/web:app apps/${myapp} --bundler=webpack --unitTestRunner=vitest` - ); - runCLI( - `generate @nx/web:app apps/${myapp2} --bundler=webpack --unitTestRunner=vitest` - ); - runCLI(`generate @nx/js:lib libs/${mylib}`); - runCommand(`git add . && git commit -am "add all"`); - } - - it('should not affect other projects by generating a new project', () => { - // TODO: investigate why affected gives different results on windows - if (isNotWindows()) { - runCLI(`generate @nx/web:app apps/${myapp}`); - expect(runCLI('show projects --affected')).toContain(myapp); - runCommand(`git add . && git commit -am "add ${myapp}"`); - - runCLI(`generate @nx/web:app apps/${myapp2}`); - let output = runCLI('show projects --affected'); - expect(output).not.toContain(myapp); - expect(output).toContain(myapp2); - runCommand(`git add . && git commit -am "add ${myapp2}"`); - - runCLI(`generate @nx/js:lib libs/${mylib}`); - output = runCLI('show projects --affected'); - expect(output).not.toContain(myapp); - expect(output).not.toContain(myapp2); - expect(output).toContain(mylib); - } - }, 1000000); - - it('should detect changes to projects based on tags changes', async () => { - // TODO: investigate why affected gives different results on windows - if (isNotWindows()) { - generateAll(); - updateFile(join('apps', myapp, 'project.json'), (content) => { - const data = JSON.parse(content); - data.tags = ['tag']; - return JSON.stringify(data, null, 2); - }); - const output = runCLI('show projects --affected'); - expect(output).toContain(myapp); - expect(output).not.toContain(myapp2); - expect(output).not.toContain(mylib); - } - }); - - it('should affect all projects by removing projects', async () => { - generateAll(); - const root = `libs/${mylib}`; - removeFile(root); - const output = runCLI('show projects --affected'); - expect(output).toContain(myapp); - expect(output).toContain(myapp2); - expect(output).not.toContain(mylib); - }); - - it('should detect changes to implicitly dependant projects', async () => { - generateAll(); - updateFile(join('apps', myapp, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = ['*', `!${myapp2}`]; - return JSON.stringify(data, null, 2); - }); - - runCommand('git add . && git commit -m "setup test"'); - updateFile(`libs/${mylib}/index.html`, ''); - - const output = runCLI('show projects --affected'); - - expect(output).toContain(myapp); - expect(output).not.toContain(myapp2); - expect(output).toContain(mylib); - - // Clear implicit deps to not interfere with other tests. - - updateFile(join('apps', myapp, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = []; - return JSON.stringify(data, null, 2); - }); - }); - - it('should handle file renames', () => { - generateAll(); - - // Move file - updateFile( - `apps/${myapp2}/src/index.html`, - readFile(`apps/${myapp}/src/index.html`) - ); - removeFile(`apps/${myapp}/src/index.html`); - - const affectedProjects = runCLI('show projects --affected --uncommitted'); - // .replace( - // /.*nx print-affected --uncommitted --select projects( --verbose)?\n/, - // '' - // ) - // .split(', '); - - expect(affectedProjects).toContain(myapp); - expect(affectedProjects).toContain(myapp2); - }); - }); -}); From ede11878701debc397172fab285b25fcaef22238 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Fri, 17 Oct 2025 13:56:41 +0100 Subject: [PATCH 09/11] chore(repo): bust cache --- nx.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nx.json b/nx.json index 0c6ca02ce7bf2..82f3fa185cac4 100644 --- a/nx.json +++ b/nx.json @@ -330,7 +330,7 @@ } ], "parallel": 1, - "bust": 3, + "bust": 4, "defaultBase": "master", "sync": { "applyChanges": true From cda692bbd9ad34b54c25a4999432ea33ea5bd498 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Fri, 17 Oct 2025 17:00:30 +0100 Subject: [PATCH 10/11] chore(repo): run conflicting tests serially --- .nx/workflows/dynamic-changesets.yaml | 10 ++++++++++ nx.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.nx/workflows/dynamic-changesets.yaml b/.nx/workflows/dynamic-changesets.yaml index 8580c122f8c9c..c4f0da65fe4a9 100644 --- a/.nx/workflows/dynamic-changesets.yaml +++ b/.nx/workflows/dynamic-changesets.yaml @@ -25,6 +25,9 @@ assignment-rules: - e2e-next - e2e-web - e2e-eslint + - e2e-remix + - e2e-cypress + - e2e-docker targets: - e2e-ci**react-package** - e2e-ci**react.test** @@ -36,6 +39,13 @@ assignment-rules: - e2e-ci**web** - e2e-ci**remix-ts-solution** - e2e-ci**linter** + - e2e-ci**module-federation/misc-rspack-interoperability** + - e2e-ci**module-federation/dynamic-federation.webpack** + - e2e-ci**docker** + - e2e-ci**module-federation/misc-rspack-interoperability** + - e2e-ci**cypress-legacy** + - e2e-ci**nx-remix** + - e2e-ci**cypress** run-on: - agent: linux-large parallelism: 1 diff --git a/nx.json b/nx.json index 82f3fa185cac4..f7ce4fdc832e9 100644 --- a/nx.json +++ b/nx.json @@ -330,7 +330,7 @@ } ], "parallel": 1, - "bust": 4, + "bust": 6, "defaultBase": "master", "sync": { "applyChanges": true From e40e2c36bfaa035b75caf53b3ea16ea9a9e4be53 Mon Sep 17 00:00:00 2001 From: Rares Matei Date: Fri, 17 Oct 2025 22:54:17 +0100 Subject: [PATCH 11/11] chore(repo): switch all e2e tests to run serially --- .nx/workflows/dynamic-changesets.yaml | 18 +----------------- nx.json | 2 +- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/.nx/workflows/dynamic-changesets.yaml b/.nx/workflows/dynamic-changesets.yaml index c4f0da65fe4a9..7e93d24e912e2 100644 --- a/.nx/workflows/dynamic-changesets.yaml +++ b/.nx/workflows/dynamic-changesets.yaml @@ -29,23 +29,7 @@ assignment-rules: - e2e-cypress - e2e-docker targets: - - e2e-ci**react-package** - - e2e-ci**react.test** - - e2e-ci**react-router-ts-solution** - - e2e-ci**next-e2e-and-snapshots** - - e2e-ci**next-generation** - - e2e-ci**next-ts-solutions** - - e2e-ci**next-webpack** - - e2e-ci**web** - - e2e-ci**remix-ts-solution** - - e2e-ci**linter** - - e2e-ci**module-federation/misc-rspack-interoperability** - - e2e-ci**module-federation/dynamic-federation.webpack** - - e2e-ci**docker** - - e2e-ci**module-federation/misc-rspack-interoperability** - - e2e-ci**cypress-legacy** - - e2e-ci**nx-remix** - - e2e-ci**cypress** + - e2e-ci** run-on: - agent: linux-large parallelism: 1 diff --git a/nx.json b/nx.json index f7ce4fdc832e9..f7f2298df0769 100644 --- a/nx.json +++ b/nx.json @@ -330,7 +330,7 @@ } ], "parallel": 1, - "bust": 6, + "bust": 12, "defaultBase": "master", "sync": { "applyChanges": true