diff --git a/.nx/workflows/dynamic-changesets.yaml b/.nx/workflows/dynamic-changesets.yaml index db7a21ebd283b..9e5270f287d3d 100644 --- a/.nx/workflows/dynamic-changesets.yaml +++ b/.nx/workflows/dynamic-changesets.yaml @@ -25,17 +25,11 @@ assignment-rules: - e2e-next - e2e-web - e2e-eslint + - e2e-remix + - 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** run-on: - agent: linux-large parallelism: 1 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..826fed99c0dcb --- /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(3000)).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/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 new file mode 100644 index 0000000000000..2b4d78db5606c --- /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} --skip-nx-cache`, { + 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} --skip-nx-cache`, { + 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 --skip-nx-cache`, { + 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} --skip-nx-cache`, { + 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 --skip-nx-cache`, { + 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..ef02dad740a8f --- /dev/null +++ b/e2e/eslint/src/linter-linting-errors.test.ts @@ -0,0 +1,400 @@ +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); + }) 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/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/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/src/affected-graph-affected-star.test.ts b/e2e/nx/src/affected-graph-affected-star.test.ts new file mode 100644 index 0000000000000..4d8597bab7c5d --- /dev/null +++ b/e2e/nx/src/affected-graph-affected-star.test.ts @@ -0,0 +1,296 @@ +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, + 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); + }); + + 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-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..f3924eb467738 --- /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({ packages: ['@nx/js', '@nx/web'] }); + 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.test.ts b/e2e/nx/src/affected-graph.test.ts deleted file mode 100644 index 0f328b8c04184..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({ packages: ['@nx/js', '@nx/web'] }))); - 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({ packages: ['@nx/js', '@nx/web'] }))); - 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)) - ); - } -}); 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" ] } diff --git a/nx.json b/nx.json index bfd68988f1be7..ecd1fdf6abf7e 100644 --- a/nx.json +++ b/nx.json @@ -333,7 +333,7 @@ } ], "parallel": 1, - "bust": 3, + "bust": 12, "defaultBase": "master", "sync": { "applyChanges": true