import { NxJson } from '@nrwl/workspace'; import { ensureProject, forEachCli, listFiles, newProject, readFile, readJson, rmDist, runCLI, runCommand, uniq, updateFile, workspaceConfigName, } from './utils'; let originalCIValue: any; forEachCli((cliName) => { const cliCommand = cliName === 'angular' ? 'ng' : 'nx'; /** * Setting CI=true makes it simpler to configure assertions around output, as there * won't be any colors. */ beforeAll(() => { originalCIValue = process.env.CI; process.env.CI = 'true'; }); afterAll(() => { process.env.CI = originalCIValue; }); describe('run-one', () => { it('should build specific project', () => { ensureProject(); const myapp = uniq('myapp'); const mylib1 = uniq('mylib1'); const mylib2 = uniq('mylib1'); runCLI(`generate @nrwl/react:app ${myapp}`); runCLI(`generate @nrwl/react:lib ${mylib1} --publishable`); runCLI(`generate @nrwl/react:lib ${mylib2} --publishable`); updateFile( `apps/${myapp}/src/main.ts`, ` import "@proj/${mylib1}"; import "@proj/${mylib2}"; ` ); const buildWithDeps = runCLI(`build ${myapp} --with-deps --prod`); expect(buildWithDeps).toContain(`Running target "build" succeeded`); expect(buildWithDeps).toContain( `${cliCommand} run ${myapp}:build:production` ); expect(buildWithDeps).toContain(`${cliCommand} run ${mylib1}:build`); expect(buildWithDeps).toContain(`${cliCommand} run ${mylib2}:build`); const testsWithDeps = runCLI(`test ${myapp} --with-deps`); expect(testsWithDeps).toContain( `NX Running target test for project ${myapp} and its 2 deps` ); expect(testsWithDeps).toContain(myapp); expect(testsWithDeps).toContain(mylib1); expect(testsWithDeps).toContain(mylib2); const testsWithoutDeps = runCLI(`test ${myapp}`); expect(testsWithoutDeps).not.toContain(mylib1); }, 1000000); }); describe('run-many', () => { it('should build specific and all projects', () => { newProject(); const appA = uniq('appa-rand'); const libA = uniq('liba-rand'); const libB = uniq('libb-rand'); const libC = uniq('libc-rand'); const libD = uniq('libd-rand'); runCLI(`generate @nrwl/angular:app ${appA}`); runCLI(`generate @nrwl/angular:lib ${libA} --publishable --defaults`); runCLI(`generate @nrwl/angular:lib ${libB} --publishable --defaults`); runCLI(`generate @nrwl/angular:lib ${libC} --publishable --defaults`); runCLI(`generate @nrwl/angular:lib ${libD} --defaults`); // libA depends on libC updateFile( `libs/${libA}/src/lib/${libA}.module.spec.ts`, ` import '@proj/${libC}'; describe('sample test', () => { it('should test', () => { expect(1).toEqual(1); }); }); ` ); // testing run many starting' const buildParallel = runCLI( `run-many --target=build --projects="${libC},${libB}"` ); expect(buildParallel).toContain(`Running target build for projects:`); expect(buildParallel).not.toContain(`- ${libA}`); expect(buildParallel).toContain(`- ${libB}`); expect(buildParallel).toContain(`- ${libC}`); expect(buildParallel).not.toContain(`- ${libD}`); expect(buildParallel).toContain('Running target "build" succeeded'); // testing run many --all starting const buildAllParallel = runCLI(`run-many --target=build --all`); expect(buildAllParallel).toContain(`Running target build for projects:`); expect(buildAllParallel).toContain(`- ${libA}`); expect(buildAllParallel).toContain(`- ${libB}`); expect(buildAllParallel).toContain(`- ${libC}`); expect(buildAllParallel).not.toContain(`- ${libD}`); expect(buildAllParallel).toContain('Running target "build" succeeded'); // testing run many --with-deps const buildWithDeps = runCLI( `run-many --target=build --projects="${libA}" --with-deps` ); expect(buildWithDeps).toContain(`Running target build for projects:`); expect(buildWithDeps).toContain(`- ${libA}`); expect(buildWithDeps).toContain(`- ${libC}`); expect(buildWithDeps).not.toContain(`- ${libB}`); expect(buildWithDeps).not.toContain(`- ${libD}`); expect(buildWithDeps).toContain('Running target "build" succeeded'); // testing run many --configuration const buildConfig = runCLI( `run-many --target=build --projects="${appA},${libA}" --prod` ); expect(buildConfig).toContain(`Running target build for projects:`); expect(buildConfig).toContain(`run ${appA}:build:production`); expect(buildConfig).toContain(`run ${libA}:build:production`); expect(buildConfig).toContain('Running target "build" succeeded'); }, 1000000); }); describe('affected:*', () => { it('should print, build, and test affected apps', () => { ensureProject(); const myapp = uniq('myapp'); const myapp2 = uniq('myapp2'); const mylib = uniq('mylib'); const mylib2 = uniq('mylib2'); const mypublishablelib = uniq('mypublishablelib'); runCLI(`generate @nrwl/angular:app ${myapp}`); runCLI(`generate @nrwl/angular:app ${myapp2}`); runCLI(`generate @nrwl/angular:lib ${mylib}`); runCLI(`generate @nrwl/angular:lib ${mylib2}`); runCLI(`generate @nrwl/angular:lib ${mypublishablelib} --publishable`); updateFile( `apps/${myapp}/src/app/app.component.spec.ts`, ` import '@proj/${mylib}'; describe('sample test', () => { it('should test', () => { expect(1).toEqual(1); }); }); ` ); updateFile( `libs/${mypublishablelib}/src/lib/${mypublishablelib}.module.spec.ts`, ` import '@proj/${mylib}'; describe('sample test', () => { it('should test', () => { expect(1).toEqual(1); }); }); ` ); expect( runCommand( `npm run affected:apps -- --files="libs/${mylib}/src/index.ts" --plain` ).split('\n')[4] ).toEqual(myapp); const affectedApps = runCommand( `npm run affected:apps -- --files="libs/${mylib}/src/index.ts"` ); expect(affectedApps).toContain(myapp); expect(affectedApps).not.toContain(myapp2); expect(affectedApps).not.toContain(`${myapp}-e2e`); const implicitlyAffectedApps = runCommand( 'npm run affected:apps -- --files="tsconfig.json"' ); expect(implicitlyAffectedApps).toContain(myapp); expect(implicitlyAffectedApps).toContain(myapp2); const noAffectedApps = runCommand( 'npm run affected:apps -- --files="README.md"' ); expect(noAffectedApps).not.toContain(myapp); expect(noAffectedApps).not.toContain(myapp2); expect( runCommand( `npm run affected:libs -- --files="libs/${mylib}/src/index.ts" --plain` ).split('\n')[4] ).toEqual(`${mylib} ${mypublishablelib}`); const affectedLibs = runCommand( `npm run affected:libs -- --files="libs/${mylib}/src/index.ts"` ); expect(affectedLibs).toContain(mypublishablelib); expect(affectedLibs).toContain(mylib); expect(affectedLibs).not.toContain(mylib2); const implicitlyAffectedLibs = runCommand( 'npm run affected:libs -- --files="tsconfig.json"' ); expect(implicitlyAffectedLibs).toContain(mypublishablelib); expect(implicitlyAffectedLibs).toContain(mylib); expect(implicitlyAffectedLibs).toContain(mylib2); const noAffectedLibs = runCommand( 'npm run affected:libs -- --files="README.md"' ); expect(noAffectedLibs).not.toContain(mypublishablelib); expect(noAffectedLibs).not.toContain(mylib); expect(noAffectedLibs).not.toContain(mylib2); // build const build = runCommand( `npm run affected:build -- --files="libs/${mylib}/src/index.ts" --parallel` ); expect(build).toContain(`Running target build for projects:`); expect(build).toContain(`- ${myapp}`); expect(build).toContain(`- ${mypublishablelib}`); expect(build).not.toContain('is not registered with the build command'); expect(build).toContain('Running target "build" succeeded'); const buildExcluded = runCommand( `npm run affected:build -- --files="libs/${mylib}/src/index.ts" --exclude ${myapp}` ); expect(buildExcluded).toContain(`Running target build for projects:`); expect(buildExcluded).toContain(`- ${mypublishablelib}`); // test updateFile( `apps/${myapp}/src/app/app.component.spec.ts`, readFile(`apps/${myapp}/src/app/app.component.spec.ts`).replace( '.toEqual(1)', '.toEqual(2)' ) ); const failedTests = runCommand( `npm run affected:test -- --files="libs/${mylib}/src/index.ts"` ); expect(failedTests).toContain(`Running target test for projects:`); expect(failedTests).toContain(`- ${mylib}`); expect(failedTests).toContain(`- ${myapp}`); expect(failedTests).toContain(`- ${mypublishablelib}`); expect(failedTests).toContain(`Failed projects:`); expect(failedTests).toContain( 'You can isolate the above projects by passing: --only-failed' ); expect(readJson('dist/.nx-results')).toEqual({ command: 'test', results: { [myapp]: false, [mylib]: true, [mypublishablelib]: true, }, }); // Fix failing Unit Test updateFile( `apps/${myapp}/src/app/app.component.spec.ts`, readFile(`apps/${myapp}/src/app/app.component.spec.ts`).replace( '.toEqual(2)', '.toEqual(1)' ) ); const isolatedTests = runCommand( `npm run affected:test -- --files="libs/${mylib}/src/index.ts" --only-failed` ); expect(isolatedTests).toContain(`Running target test for projects`); expect(isolatedTests).toContain(`- ${myapp}`); const interpolatedTests = runCommand( `npm run affected -- --target test --files="libs/${mylib}/src/index.ts" -- --jest-config {project.root}/jest.config.js` ); expect(interpolatedTests).toContain(`Running target \"test\" succeeded`); }, 1000000); }); describe('affected (with git)', () => { let myapp = uniq('myapp'); let myapp2 = uniq('myapp'); let mylib = uniq('mylib'); it('should not affect other projects by generating a new project', () => { ensureProject(); const nxJson: NxJson = readJson('nx.json'); delete nxJson.implicitDependencies; 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 add . && git commit -am "initial commit" && git checkout -b master` ); runCLI(`generate @nrwl/angular:app ${myapp}`); expect(runCommand('yarn affected:apps')).toContain(myapp); runCommand(`git add . && git commit -am "add ${myapp}"`); runCLI(`generate @nrwl/angular:app ${myapp2}`); expect(runCommand('yarn affected:apps')).not.toContain(myapp); expect(runCommand('yarn affected:apps')).toContain(myapp2); runCommand(`git add . && git commit -am "add ${myapp2}"`); runCLI(`generate @nrwl/angular:lib ${mylib}`); expect(runCommand('yarn affected:apps')).not.toContain(myapp); expect(runCommand('yarn affected:apps')).not.toContain(myapp2); expect(runCommand('yarn affected:libs')).toContain(mylib); runCommand(`git add . && git commit -am "add ${mylib}"`); }, 1000000); it('should detect changes to projects based on the nx.json', () => { const nxJson: NxJson = readJson('nx.json'); nxJson.projects[myapp].tags = ['tag']; updateFile('nx.json', JSON.stringify(nxJson)); expect(runCommand('yarn affected:apps')).toContain(myapp); expect(runCommand('yarn affected:apps')).not.toContain(myapp2); expect(runCommand('yarn affected:libs')).not.toContain(mylib); runCommand(`git add . && git commit -am "add tag to ${myapp}"`); }); it('should detect changes to projects based on the workspace.json', () => { const workspaceJson = readJson(workspaceConfigName()); workspaceJson.projects[myapp].prefix = 'my-app'; updateFile(workspaceConfigName(), JSON.stringify(workspaceJson)); expect(runCommand('yarn affected:apps')).toContain(myapp); expect(runCommand('yarn affected:apps')).not.toContain(myapp2); expect(runCommand('yarn affected:libs')).not.toContain(mylib); runCommand(`git add . && git commit -am "change prefix for ${myapp}"`); }); it('should affect all projects by removing projects', () => { const workspaceJson = readJson(workspaceConfigName()); delete workspaceJson.projects[mylib]; updateFile(workspaceConfigName(), JSON.stringify(workspaceJson)); const nxJson = readJson('nx.json'); delete nxJson.projects[mylib]; updateFile('nx.json', JSON.stringify(nxJson)); expect(runCommand('yarn affected:apps')).toContain(myapp); expect(runCommand('yarn affected:apps')).toContain(myapp2); expect(runCommand('yarn affected:libs')).not.toContain(mylib); runCommand(`git add . && git commit -am "remove ${mylib}"`); }); }); describe('print-affected', () => { it('should print information about affected projects', () => { newProject(); const myapp = uniq('myapp-a'); const myapp2 = uniq('myapp-b'); const mylib = uniq('mylib'); const mylib2 = uniq('mylib2'); const mypublishablelib = uniq('mypublishablelib'); runCLI(`generate @nrwl/react:app ${myapp}`); runCLI(`generate @nrwl/react:app ${myapp2}`); runCLI(`generate @nrwl/react:lib ${mylib}`); runCLI(`generate @nrwl/react:lib ${mylib2}`); runCLI(`generate @nrwl/react:lib ${mypublishablelib} --publishable`); updateFile( `apps/${myapp}/src/main.tsx`, ` import React from 'react'; import ReactDOM from 'react-dom'; import "@proj/${mylib}"; import "@proj/${mypublishablelib}"; import App from './app/app'; ReactDOM.render(, document.getElementById('root')); ` ); updateFile( `apps/${myapp2}/src/main.tsx`, ` import React from 'react'; import ReactDOM from 'react-dom'; import "@proj/${mylib}"; import "@proj/${mypublishablelib}"; import App from './app/app'; ReactDOM.render(, document.getElementById('root')); ` ); const resWithoutTarget = JSON.parse( runCommand( `npm run nx print-affected --silent -- --files=apps/${myapp}/src/main.tsx` ) ); expect(resWithoutTarget.tasks).toEqual([]); compareTwoArrays(resWithoutTarget.projects, [`${myapp}-e2e`, myapp]); const resWithTarget = JSON.parse( runCommand( `npm run nx print-affected --silent -- --files=apps/${myapp}/src/main.tsx --target=test` ).trim() ); expect(resWithTarget.tasks).toEqual([ { id: `${myapp}:test`, overrides: {}, target: { project: myapp, target: 'test', }, command: `npm run ${cliCommand} -- test ${myapp}`, outputs: [], }, ]); compareTwoArrays(resWithTarget.projects, [`${myapp}-e2e`, myapp]); const resWithDeps = JSON.parse( runCommand( `npm run nx print-affected --silent -- --files=apps/${myapp}/src/main.tsx --target=build --with-deps` ) ); expect(resWithDeps.tasks).toEqual([ { id: `${mypublishablelib}:build`, overrides: {}, target: { project: mypublishablelib, target: 'build', }, command: `npm run ${cliCommand} -- build ${mypublishablelib}`, outputs: [`dist/libs/${mypublishablelib}`], }, { id: `${myapp}:build`, overrides: {}, target: { project: myapp, target: 'build', }, command: `npm run ${cliCommand} -- build ${myapp}`, outputs: [`dist/apps/${myapp}`], }, ]); compareTwoArrays(resWithDeps.projects, [ mylib, mypublishablelib, myapp, `${myapp}-e2e`, ]); const resWithTargetWithSelect1 = runCommand( `npm run nx print-affected --silent -- --files=apps/${myapp}/src/main.tsx --target=test --select=projects` ).trim(); compareTwoSerializedArrays( resWithTargetWithSelect1, `${myapp}-e2e, ${myapp}` ); const resWithTargetWithSelect2 = runCommand( `npm run nx print-affected --silent -- --files=apps/${myapp}/src/main.tsx --target=test --select="tasks.target.project"` ).trim(); compareTwoSerializedArrays(resWithTargetWithSelect2, `${myapp}`); }, 120000); function compareTwoSerializedArrays(a: string, b: string) { compareTwoArrays( a.split(',').map((_) => _.trim()), b.split(',').map((_) => _.trim()) ); } function compareTwoArrays(a: string[], b: string[]) { expect(a.sort((x, y) => x.localeCompare(y))).toEqual( b.sort((x, y) => x.localeCompare(y)) ); } }); describe('cache', () => { it('should cache command execution', async () => { ensureProject(); const myapp1 = uniq('myapp1'); const myapp2 = uniq('myapp2'); runCLI(`generate @nrwl/web:app ${myapp1}`); runCLI(`generate @nrwl/web:app ${myapp2}`); const files = `--files="apps/${myapp1}/src/main.ts,apps/${myapp2}/src/main.ts"`; // run build with caching // -------------------------------------------- const outputThatPutsDataIntoCache = runCommand( `npm run affected:build -- ${files}` ); const filesApp1 = listFiles(`dist/apps/${myapp1}`); const filesApp2 = listFiles(`dist/apps/${myapp2}`); // now the data is in cache expect(outputThatPutsDataIntoCache).not.toContain( 'read the output from cache' ); rmDist(); const outputWithBothBuildTasksCached = runCommand( `npm run affected:build -- ${files}` ); expect(outputWithBothBuildTasksCached).toContain( 'read the output from cache' ); expectCached(outputWithBothBuildTasksCached, [myapp1, myapp2]); expect(listFiles(`dist/apps/${myapp1}`)).toEqual(filesApp1); expect(listFiles(`dist/apps/${myapp2}`)).toEqual(filesApp2); // run with skipping cache const outputWithBothBuildTasksCachedButSkipped = runCommand( `npm run affected:build -- ${files} --skip-nx-cache` ); expect(outputWithBothBuildTasksCachedButSkipped).not.toContain( `read the output from cache` ); // touch myapp1 // -------------------------------------------- updateFile(`apps/${myapp1}/src/main.ts`, (c) => { return `${c}\n//some comment`; }); const outputWithBuildApp2Cached = runCommand( `npm run affected:build -- ${files}` ); expect(outputWithBuildApp2Cached).toContain('read the output from cache'); expectCached(outputWithBuildApp2Cached, [myapp2]); // touch package.json // -------------------------------------------- updateFile(`package.json`, (c) => { const r = JSON.parse(c); r.description = 'different'; return JSON.stringify(r); }); const outputWithNoBuildCached = runCommand( `npm run affected:build -- ${files}` ); expect(outputWithNoBuildCached).not.toContain( 'read the output from cache' ); // build individual project with caching const individualBuildWithCache = runCommand( `npm run nx -- build ${myapp1}` ); expect(individualBuildWithCache).toContain('Cached Output'); // skip caching when building individual projects const individualBuildWithSkippedCache = runCommand( `npm run nx -- build ${myapp1} --skip-nx-cache` ); expect(individualBuildWithSkippedCache).not.toContain('Cached Output'); // run lint with caching // -------------------------------------------- const outputWithNoLintCached = runCommand( `npm run affected:lint -- ${files}` ); expect(outputWithNoLintCached).not.toContain( 'read the output from cache' ); const outputWithBothLintTasksCached = runCommand( `npm run affected:lint -- ${files}` ); expect(outputWithBothLintTasksCached).toContain( 'read the output from cache' ); expectCached(outputWithBothLintTasksCached, [ myapp1, myapp2, `${myapp1}-e2e`, `${myapp2}-e2e`, ]); // run without caching // -------------------------------------------- // disable caching // -------------------------------------------- updateFile('nx.json', (c) => { const nxJson = JSON.parse(c); nxJson.tasksRunnerOptions = { default: { options: { cacheableOperations: [], }, }, }; return JSON.stringify(nxJson, null, 2); }); const outputWithoutCachingEnabled1 = runCommand( `npm run affected:build -- ${files}` ); expect(outputWithoutCachingEnabled1).not.toContain( 'read the output from cache' ); const outputWithoutCachingEnabled2 = runCommand( `npm run affected:build -- ${files}` ); expect(outputWithoutCachingEnabled2).not.toContain( 'read the output from cache' ); }, 120000); function expectCached( actualOutput: string, expectedCachedProjects: string[] ) { const cachedProjects = []; const lines = actualOutput.split('\n'); lines.forEach((s, i) => { if (s.startsWith(`> ${cliCommand} run`)) { const projectName = s .split(`> ${cliCommand} run `)[1] .split(':')[0] .trim(); if (lines[i + 2].indexOf('Cached Output') > -1) { cachedProjects.push(projectName); } } }); cachedProjects.sort((a, b) => a.localeCompare(b)); expectedCachedProjects.sort((a, b) => a.localeCompare(b)); expect(cachedProjects).toEqual(expectedCachedProjects); } }); });