diff --git a/.nx/workflows/dynamic-changesets.yaml b/.nx/workflows/dynamic-changesets.yaml index 8580c122f8c9cd..62d7bb61e50a2a 100644 --- a/.nx/workflows/dynamic-changesets.yaml +++ b/.nx/workflows/dynamic-changesets.yaml @@ -21,21 +21,17 @@ assignment-rules: parallelism: 1 - projects: + - e2e-angular - e2e-react - e2e-next - e2e-web - e2e-eslint + - e2e-remix + - e2e-cypress + - e2e-docker + - e2e-js 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/nx/src/misc-cross-workspace.test.ts b/e2e/nx/src/misc-cross-workspace.test.ts new file mode 100644 index 00000000000000..b89ad65f6c183d --- /dev/null +++ b/e2e/nx/src/misc-cross-workspace.test.ts @@ -0,0 +1,50 @@ +import { + cleanupProject, + newProject, + runCLI, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { join } from 'path'; + +describe('cross-workspace implicit dependencies', () => { + beforeAll(() => + newProject({ + packages: ['@nx/js'], + }) + ); + + afterAll(() => cleanupProject()); + + it('should successfully build a project graph when cross-workspace implicit dependencies are present', () => { + const npmPackage = uniq('npm-package'); + runCLI(`generate @nx/workspace:npm-package ${npmPackage}`); + + function setImplicitDependencies(deps: string[]) { + updateFile(join(npmPackage, 'package.json'), (content) => { + const json = JSON.parse(content); + json.nx = { + ...json.nx, + implicitDependencies: deps, + }; + return JSON.stringify(json, null, 2); + }); + } + + // First set the implicit dependencies to an intentionally invalid value to prove the command fails during project graph construction + setImplicitDependencies(['this-project-does-not-exist']); + expect( + runCLI(`test ${npmPackage}`, { + silenceError: true, + }) + ).toContain('Failed to process project graph'); + + // Now set the implicit dependencies to a cross-workspace reference to prove that it is valid, despite not being resolvable in the current workspace + setImplicitDependencies(['nx-cloud:another-workspace']); + expect( + runCLI(`test ${npmPackage}`, { + silenceError: true, + }) + ).toContain('Successfully ran target test'); + }); +}); diff --git a/e2e/nx/src/misc-format.test.ts b/e2e/nx/src/misc-format.test.ts new file mode 100644 index 00000000000000..5e8aed6c465a4d --- /dev/null +++ b/e2e/nx/src/misc-format.test.ts @@ -0,0 +1,173 @@ +import { + cleanupProject, + isNotWindows, + runCLI, + runCLIAsync, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import * as path from 'path'; +import { setupMiscTests } from './misc-setup'; + +describe('Nx Commands - format', () => { + const myapp = uniq('myapp'); + const mylib = uniq('mylib'); + + beforeAll(() => { + setupMiscTests(); + runCLI(`generate @nx/web:app apps/${myapp}`); + runCLI(`generate @nx/js:lib libs/${mylib}`); + }); + + afterAll(() => cleanupProject()); + + beforeEach(() => { + updateFile( + `apps/${myapp}/src/main.ts`, + ` + const x = 1111; + ` + ); + + updateFile( + `apps/${myapp}/src/app/app.element.spec.ts`, + ` + const y = 1111; + ` + ); + + updateFile( + `apps/${myapp}/src/app/app.element.ts`, + ` + const z = 1111; + ` + ); + + updateFile( + `libs/${mylib}/index.ts`, + ` + const x = 1111; + ` + ); + updateFile( + `libs/${mylib}/src/${mylib}.spec.ts`, + ` + const y = 1111; + ` + ); + + updateFile( + `README.md`, + ` + my new readme; + ` + ); + }); + + it('should check libs and apps specific files', async () => { + if (isNotWindows()) { + const stdout = runCLI( + `format:check --files="libs/${mylib}/index.ts,package.json" --libs-and-apps`, + { silenceError: true } + ); + expect(stdout).toContain(path.normalize(`libs/${mylib}/index.ts`)); + expect(stdout).toContain( + path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) + ); + expect(stdout).not.toContain(path.normalize(`README.md`)); // It will be contained only in case of exception, that we fallback to all + } + }, 90000); + + it('should check specific project', async () => { + if (isNotWindows()) { + const stdout = runCLI(`format:check --projects=${myapp}`, { + silenceError: true, + }); + expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); + expect(stdout).toContain( + path.normalize(`apps/${myapp}/src/app/app.element.ts`) + ); + expect(stdout).toContain( + path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) + ); + expect(stdout).not.toContain(path.normalize(`libs/${mylib}/index.ts`)); + expect(stdout).not.toContain( + path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) + ); + expect(stdout).not.toContain(path.normalize(`README.md`)); + } + }, 90000); + + it('should check multiple projects', async () => { + if (isNotWindows()) { + const stdout = runCLI(`format:check --projects=${myapp},${mylib}`, { + silenceError: true, + }); + expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); + expect(stdout).toContain( + path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) + ); + expect(stdout).toContain( + path.normalize(`apps/${myapp}/src/app/app.element.ts`) + ); + expect(stdout).toContain(path.normalize(`libs/${mylib}/index.ts`)); + expect(stdout).toContain( + path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) + ); + expect(stdout).not.toContain(path.normalize(`README.md`)); + } + }, 90000); + + it('should check all', async () => { + if (isNotWindows()) { + const stdout = runCLI(`format:check --all`, { silenceError: true }); + expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); + expect(stdout).toContain( + path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) + ); + expect(stdout).toContain( + path.normalize(`apps/${myapp}/src/app/app.element.ts`) + ); + expect(stdout).toContain(path.normalize(`libs/${mylib}/index.ts`)); + expect(stdout).toContain( + path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) + ); + expect(stdout).toContain(path.normalize(`README.md`)); + } + }, 90000); + + it('should throw error if passing both projects and --all param', async () => { + if (isNotWindows()) { + const { stderr } = await runCLIAsync( + `format:check --projects=${myapp},${mylib} --all`, + { + silenceError: true, + } + ); + expect(stderr).toContain( + 'Arguments all and projects are mutually exclusive' + ); + } + }, 90000); + + it('should reformat the code', async () => { + if (isNotWindows()) { + runCLI( + `format:write --files="apps/${myapp}/src/app/app.element.spec.ts,apps/${myapp}/src/app/app.element.ts"` + ); + const stdout = runCLI('format:check --all', { silenceError: true }); + expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); + expect(stdout).not.toContain( + path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) + ); + expect(stdout).not.toContain( + path.normalize(`apps/${myapp}/src/app/app.element.ts`) + ); + + runCLI('format:write --all'); + expect(runCLI('format:check --all')).not.toContain( + path.normalize(`apps/${myapp}/src/main.ts`) + ); + } + }, 300000); +}); diff --git a/e2e/nx/src/misc-global-installation.test.ts b/e2e/nx/src/misc-global-installation.test.ts new file mode 100644 index 00000000000000..67ba74479ff272 --- /dev/null +++ b/e2e/nx/src/misc-global-installation.test.ts @@ -0,0 +1,144 @@ +import { + createNonNxProjectDirectory, + e2eCwd, + getPackageManagerCommand, + getPublishedVersion, + newProject, + readFile, + readJson, + runCommand, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { ensureDirSync, writeFileSync } from 'fs-extra'; +import * as path from 'path'; +import { major } from 'semver'; + +describe('global installation', () => { + // Additionally, installing Nx under e2eCwd like this still acts like a global install, + // but is easier to cleanup and doesn't mess with the users PC if running tests locally. + const globalsPath = path.join(e2eCwd, 'globals', 'node_modules', '.bin'); + + let oldPath: string; + + beforeAll(() => { + ensureDirSync(globalsPath); + writeFileSync( + path.join(path.dirname(path.dirname(globalsPath)), 'package.json'), + JSON.stringify( + { + dependencies: { + nx: getPublishedVersion(), + }, + }, + null, + 2 + ) + ); + + runCommand(getPackageManagerCommand().install, { + cwd: path.join(e2eCwd, 'globals'), + }); + + // Update process.path to have access to modules installed in e2ecwd/node_modules/.bin, + // this lets commands run things like `nx`. We put it at the beginning so they are found first. + oldPath = process.env.PATH; + process.env.PATH = globalsPath + path.delimiter + process.env.PATH; + }); + + afterAll(() => { + process.env.PATH = oldPath; + }); + + describe('inside nx directory', () => { + beforeAll(() => { + newProject(); + }); + + it('should invoke Nx commands from local repo', () => { + const nxJsContents = readFile('node_modules/nx/bin/nx.js'); + updateFile('node_modules/nx/bin/nx.js', `console.log('local install');`); + let output: string; + expect(() => { + output = runCommand(`nx show projects`); + }).not.toThrow(); + expect(output).toContain('local install'); + updateFile('node_modules/nx/bin/nx.js', nxJsContents); + }); + + it('should warn if local Nx has higher major version', () => { + const packageJsonContents = readFile('node_modules/nx/package.json'); + updateJson('node_modules/nx/package.json', (json) => { + json.version = `${major(getPublishedVersion()) + 2}.0.0`; + return json; + }); + let output: string; + expect(() => { + output = runCommand(`nx show projects`); + }).not.toThrow(); + expect(output).toContain(`It's time to update Nx`); + updateFile('node_modules/nx/package.json', packageJsonContents); + }); + + it('--version should display global installs version', () => { + const packageJsonContents = readFile('node_modules/nx/package.json'); + const localVersion = `${major(getPublishedVersion()) + 2}.0.0`; + updateJson('node_modules/nx/package.json', (json) => { + json.version = localVersion; + return json; + }); + let output: string; + expect(() => { + output = runCommand(`nx --version`); + }).not.toThrow(); + expect(output).toContain(`- Local: v${localVersion}`); + expect(output).toContain(`- Global: v${getPublishedVersion()}`); + updateFile('node_modules/nx/package.json', packageJsonContents); + }); + + it('report should display global installs version', () => { + const packageJsonContents = readFile('node_modules/nx/package.json'); + const localVersion = `${major(getPublishedVersion()) + 2}.0.0`; + updateJson('node_modules/nx/package.json', (json) => { + json.version = localVersion; + return json; + }); + let output: string; + expect(() => { + output = runCommand(`nx report`); + }).not.toThrow(); + expect(output).toEqual( + expect.stringMatching(new RegExp(`nx.*:.*${localVersion}`)) + ); + expect(output).toEqual( + expect.stringMatching( + new RegExp(`nx \\(global\\).*:.*${getPublishedVersion()}`) + ) + ); + updateFile('node_modules/nx/package.json', packageJsonContents); + }); + }); + + describe('non-nx directory', () => { + beforeAll(() => { + createNonNxProjectDirectory(); + }); + + it('--version should report global version and local not found', () => { + let output: string; + expect(() => { + output = runCommand(`nx --version`); + }).not.toThrow(); + expect(output).toContain(`- Local: Not found`); + expect(output).toContain(`- Global: v${getPublishedVersion()}`); + }); + + it('graph should work in npm workspaces repo', () => { + expect(() => { + runCommand(`nx graph --file graph.json`); + }).not.toThrow(); + const { graph } = readJson('graph.json'); + expect(graph).toHaveProperty('nodes'); + }); + }); +}); diff --git a/e2e/nx/src/misc-help.test.ts b/e2e/nx/src/misc-help.test.ts new file mode 100644 index 00000000000000..1fba984a5b0224 --- /dev/null +++ b/e2e/nx/src/misc-help.test.ts @@ -0,0 +1,14 @@ +import { cleanupProject, runCLI } from '@nx/e2e-utils'; +import { setupMiscTests } from './misc-setup'; + +describe('Nx Commands - help', () => { + beforeAll(() => setupMiscTests()); + + afterAll(() => cleanupProject()); + + it('should show help if no command provided', () => { + const output = runCLI('', { silenceError: true }); + expect(output).toContain('Smart Repos · Fast Builds'); + expect(output).toContain('Commands:'); + }); +}); diff --git a/e2e/nx/src/misc.test.ts b/e2e/nx/src/misc-migrate.test.ts similarity index 51% rename from e2e/nx/src/misc.test.ts rename to e2e/nx/src/misc-migrate.test.ts index bf98edb64b4822..8d387f5567dec7 100644 --- a/e2e/nx/src/misc.test.ts +++ b/e2e/nx/src/misc-migrate.test.ts @@ -1,454 +1,23 @@ -import type { NxJsonConfiguration, ProjectConfiguration } from '@nx/devkit'; +import type { NxJsonConfiguration } from '@nx/devkit'; import { - cleanupProject, - createNonNxProjectDirectory, - e2eCwd, - getPackageManagerCommand, getPublishedVersion, getSelectedPackageManager, isNotWindows, - killProcessAndPorts, newProject, readFile, readJson, removeFile, runCLI, - runCLIAsync, runCommand, - runCommandUntil, - tmpProjPath, uniq, updateFile, updateJson, } from '@nx/e2e-utils'; -import { renameSync, writeFileSync } from 'fs'; -import { ensureDirSync } from 'fs-extra'; -import * as path from 'path'; -import { major } from 'semver'; -import { join } from 'path'; - -describe('Nx Commands', () => { - beforeAll(() => - newProject({ - packages: ['@nx/web', '@nx/angular', '@nx/next'], - }) - ); - - afterAll(() => cleanupProject()); - - describe('show', () => { - it('should show the list of projects', async () => { - const app1 = uniq('myapp'); - const app2 = uniq('myapp'); - expect( - runCLI('show projects').replace(/.*nx show projects( --verbose)?\n/, '') - ).toEqual(''); - - runCLI(`generate @nx/web:app apps/${app1} --tags e2etag`); - runCLI(`generate @nx/web:app apps/${app2}`); - - const s = runCLI('show projects').split('\n'); - - expect(s.length).toEqual(5); - expect(s).toContain(app1); - expect(s).toContain(app2); - expect(s).toContain(`${app1}-e2e`); - expect(s).toContain(`${app2}-e2e`); - - const withTag = JSON.parse(runCLI('show projects -p tag:e2etag --json')); - expect(withTag).toEqual([app1]); - - const withTargets = JSON.parse( - runCLI('show projects --with-target e2e --json') - ); - expect(withTargets).toEqual( - expect.arrayContaining([`${app1}-e2e`, `${app2}-e2e`]) - ); - expect(withTargets.length).toEqual(2); - }); - - it('should show detailed project info', () => { - const app = uniq('myapp'); - runCLI( - `generate @nx/web:app apps/${app} --bundler=webpack --unitTestRunner=vitest --linter=eslint` - ); - const project: ProjectConfiguration = JSON.parse( - runCLI(`show project ${app} --json`) - ); - expect(project.targets.build).toBeDefined(); - expect(project.targets.lint).toBeDefined(); - }); - - it('should open project details view', async () => { - const app = uniq('myapp'); - runCLI(`generate @nx/web:app apps/${app}`); - let url: string; - let port: number; - const childProcess = await runCommandUntil( - `show project ${app} --web --open=false`, - (output) => { - console.log(output); - // output should contain 'Project graph started at http://127.0.0.1:{port}' - if (output.includes('Project graph started at http://')) { - const match = /https?:\/\/[\d.]+:(?\d+)/.exec(output); - if (match) { - port = parseInt(match.groups.port); - url = match[0]; - return true; - } - } - return false; - } - ); - // Check that url is alive - const response = await fetch(url); - expect(response.status).toEqual(200); - await killProcessAndPorts(childProcess.pid, port); - }, 700000); - - it('should find alternative port when default port is occupied', async () => { - const app = uniq('myapp'); - runCLI(`generate @nx/web:app apps/${app}`); - - const http = require('http'); - - // Create a server that occupies the default port 4211 - const blockingServer = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('blocking server'); - }); - - await new Promise((resolve) => { - blockingServer.listen(4211, '127.0.0.1', () => { - console.log('Blocking server started on port 4211'); - resolve(); - }); - }); - - let url: string; - let port: number; - let foundAlternativePort = false; - - try { - const childProcess = await runCommandUntil( - `show project ${app} --web --open=false`, - (output) => { - console.log(output); - // Should find alternative port and show message about port being in use - if (output.includes('Port 4211 was already in use, using port')) { - foundAlternativePort = true; - } - // output should contain 'Project graph started at http://127.0.0.1:{port}' - if (output.includes('Project graph started at http://')) { - const match = /https?:\/\/[\d.]+:(?\d+)/.exec(output); - if (match) { - port = parseInt(match.groups.port); - url = match[0]; - return true; - } - } - return false; - } - ); - - // Verify that an alternative port was found - expect(foundAlternativePort).toBe(true); - expect(port).not.toBe(4211); - expect(port).toBeGreaterThan(4211); - - // Check that url is alive - const response = await fetch(url); - expect(response.status).toEqual(200); - - await killProcessAndPorts(childProcess.pid, port); - } finally { - // Clean up the blocking server - blockingServer.close(); - } - }, 700000); - }); - - describe('report and list', () => { - it(`should report package versions`, async () => { - const reportOutput = runCLI('report'); - - expect(reportOutput).toEqual( - expect.stringMatching( - new RegExp(`\@nx\/workspace.*:.*${getPublishedVersion()}`) - ) - ); - expect(reportOutput).toContain('@nx/workspace'); - }, 120000); - - it(`should list plugins`, async () => { - let listOutput = runCLI('list'); - - expect(listOutput).toContain('NX Installed plugins'); - - // just check for some, not all - expect(listOutput).toContain('@nx/workspace'); - - // temporarily make it look like this isn't installed - // For pnpm, we need to rename the actual package in .pnpm directory, not just the symlink - const { readdirSync, statSync } = require('fs'); - const pnpmDir = tmpProjPath('node_modules/.pnpm'); - let renamedPnpmEntry = null; - - if (require('fs').existsSync(pnpmDir)) { - const entries = readdirSync(pnpmDir); - const nextEntries = entries.filter((entry) => - entry.includes('nx+next@') - ); - - // Rename all nx+next entries - const renamedEntries = []; - for (const entry of nextEntries) { - const tmpName = entry.replace('@nx+next@', 'tmp_nx_next_'); - renameSync( - tmpProjPath(`node_modules/.pnpm/${entry}`), - tmpProjPath(`node_modules/.pnpm/${tmpName}`) - ); - renamedEntries.push(entry); - } - renamedPnpmEntry = renamedEntries; - } - - // Also rename the symlink - if (require('fs').existsSync(tmpProjPath('node_modules/@nx/next'))) { - renameSync( - tmpProjPath('node_modules/@nx/next'), - tmpProjPath('node_modules/@nx/next_tmp') - ); - } - - listOutput = runCLI('list'); - expect(listOutput).toContain('NX Also available'); - - // look for specific plugin - listOutput = runCLI('list @nx/workspace'); - - expect(listOutput).toContain('Capabilities in @nx/workspace'); - - // check for schematics - expect(listOutput).toContain('workspace'); - expect(listOutput).toContain('library'); - - // check for builders - expect(listOutput).toContain('run-commands'); - - listOutput = runCLI('list @nx/angular'); - - expect(listOutput).toContain('Capabilities in @nx/angular'); - - expect(listOutput).toContain('library'); - expect(listOutput).toContain('component'); - - // check for builders - expect(listOutput).toContain('package'); - - // look for uninstalled core plugin - listOutput = runCLI('list @nx/next'); - - expect(listOutput).toContain('NX @nx/next is not currently installed'); - - // look for an unknown plugin - listOutput = runCLI('list @wibble/fish'); - - expect(listOutput).toContain( - 'NX @wibble/fish is not currently installed' - ); - - // put back the @nx/next module (or all the other e2e tests after this will fail) - if (renamedPnpmEntry && Array.isArray(renamedPnpmEntry)) { - for (const entry of renamedPnpmEntry) { - const tmpName = entry.replace('@nx+next@', 'tmp_nx_next_'); - renameSync( - tmpProjPath(`node_modules/.pnpm/${tmpName}`), - tmpProjPath(`node_modules/.pnpm/${entry}`) - ); - } - } - - if (require('fs').existsSync(tmpProjPath('node_modules/@nx/next_tmp'))) { - renameSync( - tmpProjPath('node_modules/@nx/next_tmp'), - tmpProjPath('node_modules/@nx/next') - ); - } - }, 120000); - }); - - describe('format', () => { - const myapp = uniq('myapp'); - const mylib = uniq('mylib'); - - beforeAll(async () => { - runCLI(`generate @nx/web:app apps/${myapp}`); - runCLI(`generate @nx/js:lib libs/${mylib}`); - }); - - beforeEach(() => { - updateFile( - `apps/${myapp}/src/main.ts`, - ` - const x = 1111; - ` - ); - - updateFile( - `apps/${myapp}/src/app/app.element.spec.ts`, - ` - const y = 1111; - ` - ); - - updateFile( - `apps/${myapp}/src/app/app.element.ts`, - ` - const z = 1111; - ` - ); - - updateFile( - `libs/${mylib}/index.ts`, - ` - const x = 1111; - ` - ); - updateFile( - `libs/${mylib}/src/${mylib}.spec.ts`, - ` - const y = 1111; - ` - ); - - updateFile( - `README.md`, - ` - my new readme; - ` - ); - }); - - it('should check libs and apps specific files', async () => { - if (isNotWindows()) { - const stdout = runCLI( - `format:check --files="libs/${mylib}/index.ts,package.json" --libs-and-apps`, - { silenceError: true } - ); - expect(stdout).toContain(path.normalize(`libs/${mylib}/index.ts`)); - expect(stdout).toContain( - path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) - ); - expect(stdout).not.toContain(path.normalize(`README.md`)); // It will be contained only in case of exception, that we fallback to all - } - }, 90000); - - it('should check specific project', async () => { - if (isNotWindows()) { - const stdout = runCLI(`format:check --projects=${myapp}`, { - silenceError: true, - }); - expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); - expect(stdout).toContain( - path.normalize(`apps/${myapp}/src/app/app.element.ts`) - ); - expect(stdout).toContain( - path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) - ); - expect(stdout).not.toContain(path.normalize(`libs/${mylib}/index.ts`)); - expect(stdout).not.toContain( - path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) - ); - expect(stdout).not.toContain(path.normalize(`README.md`)); - } - }, 90000); - - it('should check multiple projects', async () => { - if (isNotWindows()) { - const stdout = runCLI(`format:check --projects=${myapp},${mylib}`, { - silenceError: true, - }); - expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); - expect(stdout).toContain( - path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) - ); - expect(stdout).toContain( - path.normalize(`apps/${myapp}/src/app/app.element.ts`) - ); - expect(stdout).toContain(path.normalize(`libs/${mylib}/index.ts`)); - expect(stdout).toContain( - path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) - ); - expect(stdout).not.toContain(path.normalize(`README.md`)); - } - }, 90000); - - it('should check all', async () => { - if (isNotWindows()) { - const stdout = runCLI(`format:check --all`, { silenceError: true }); - expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); - expect(stdout).toContain( - path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) - ); - expect(stdout).toContain( - path.normalize(`apps/${myapp}/src/app/app.element.ts`) - ); - expect(stdout).toContain(path.normalize(`libs/${mylib}/index.ts`)); - expect(stdout).toContain( - path.normalize(`libs/${mylib}/src/${mylib}.spec.ts`) - ); - expect(stdout).toContain(path.normalize(`README.md`)); - } - }, 90000); - - it('should throw error if passing both projects and --all param', async () => { - if (isNotWindows()) { - const { stderr } = await runCLIAsync( - `format:check --projects=${myapp},${mylib} --all`, - { - silenceError: true, - } - ); - expect(stderr).toContain( - 'Arguments all and projects are mutually exclusive' - ); - } - }, 90000); - - it('should reformat the code', async () => { - if (isNotWindows()) { - runCLI( - `format:write --files="apps/${myapp}/src/app/app.element.spec.ts,apps/${myapp}/src/app/app.element.ts"` - ); - const stdout = runCLI('format:check --all', { silenceError: true }); - expect(stdout).toContain(path.normalize(`apps/${myapp}/src/main.ts`)); - expect(stdout).not.toContain( - path.normalize(`apps/${myapp}/src/app/app.element.spec.ts`) - ); - expect(stdout).not.toContain( - path.normalize(`apps/${myapp}/src/app/app.element.ts`) - ); - - runCLI('format:write --all'); - expect(runCLI('format:check --all')).not.toContain( - path.normalize(`apps/${myapp}/src/main.ts`) - ); - } - }, 300000); - }); - - it('should show help if no command provided', () => { - const output = runCLI('', { silenceError: true }); - expect(output).toContain('Smart Repos · Fast Builds'); - expect(output).toContain('Commands:'); - }); -}); // TODO(colum): Change the fetcher to allow incremental migrations over multiple versions, allowing for beforeAll describe('migrate', () => { beforeEach(() => { - newProject({ packages: [] }); + newProject(); updateFile( `./node_modules/migrate-parent-package/package.json`, @@ -1079,174 +648,3 @@ catalogs: }); } }); - -describe('global installation', () => { - // Additionally, installing Nx under e2eCwd like this still acts like a global install, - // but is easier to cleanup and doesn't mess with the users PC if running tests locally. - const globalsPath = path.join(e2eCwd, 'globals', 'node_modules', '.bin'); - - let oldPath: string; - - beforeAll(() => { - ensureDirSync(globalsPath); - writeFileSync( - path.join(path.dirname(path.dirname(globalsPath)), 'package.json'), - JSON.stringify( - { - dependencies: { - nx: getPublishedVersion(), - }, - }, - null, - 2 - ) - ); - - runCommand(getPackageManagerCommand().install, { - cwd: path.join(e2eCwd, 'globals'), - }); - - // Update process.path to have access to modules installed in e2ecwd/node_modules/.bin, - // this lets commands run things like `nx`. We put it at the beginning so they are found first. - oldPath = process.env.PATH; - process.env.PATH = globalsPath + path.delimiter + process.env.PATH; - }); - - afterAll(() => { - process.env.PATH = oldPath; - }); - - describe('inside nx directory', () => { - beforeAll(() => { - newProject({ packages: [] }); - }); - - it('should invoke Nx commands from local repo', () => { - const nxJsContents = readFile('node_modules/nx/bin/nx.js'); - updateFile('node_modules/nx/bin/nx.js', `console.log('local install');`); - let output: string; - expect(() => { - output = runCommand(`nx show projects`); - }).not.toThrow(); - expect(output).toContain('local install'); - updateFile('node_modules/nx/bin/nx.js', nxJsContents); - }); - - it('should warn if local Nx has higher major version', () => { - const packageJsonContents = readFile('node_modules/nx/package.json'); - updateJson('node_modules/nx/package.json', (json) => { - json.version = `${major(getPublishedVersion()) + 2}.0.0`; - return json; - }); - let output: string; - expect(() => { - output = runCommand(`nx show projects`); - }).not.toThrow(); - expect(output).toContain(`It's time to update Nx`); - updateFile('node_modules/nx/package.json', packageJsonContents); - }); - - it('--version should display global installs version', () => { - const packageJsonContents = readFile('node_modules/nx/package.json'); - const localVersion = `${major(getPublishedVersion()) + 2}.0.0`; - updateJson('node_modules/nx/package.json', (json) => { - json.version = localVersion; - return json; - }); - let output: string; - expect(() => { - output = runCommand(`nx --version`); - }).not.toThrow(); - expect(output).toContain(`- Local: v${localVersion}`); - expect(output).toContain(`- Global: v${getPublishedVersion()}`); - updateFile('node_modules/nx/package.json', packageJsonContents); - }); - - it('report should display global installs version', () => { - const packageJsonContents = readFile('node_modules/nx/package.json'); - const localVersion = `${major(getPublishedVersion()) + 2}.0.0`; - updateJson('node_modules/nx/package.json', (json) => { - json.version = localVersion; - return json; - }); - let output: string; - expect(() => { - output = runCommand(`nx report`); - }).not.toThrow(); - expect(output).toEqual( - expect.stringMatching(new RegExp(`nx.*:.*${localVersion}`)) - ); - expect(output).toEqual( - expect.stringMatching( - new RegExp(`nx \\(global\\).*:.*${getPublishedVersion()}`) - ) - ); - updateFile('node_modules/nx/package.json', packageJsonContents); - }); - }); - - describe('non-nx directory', () => { - beforeAll(() => { - createNonNxProjectDirectory(); - }); - - it('--version should report global version and local not found', () => { - let output: string; - expect(() => { - output = runCommand(`nx --version`); - }).not.toThrow(); - expect(output).toContain(`- Local: Not found`); - expect(output).toContain(`- Global: v${getPublishedVersion()}`); - }); - - it('graph should work in npm workspaces repo', () => { - expect(() => { - runCommand(`nx graph --file graph.json`); - }).not.toThrow(); - const { graph } = readJson('graph.json'); - expect(graph).toHaveProperty('nodes'); - }); - }); -}); - -describe('cross-workspace implicit dependencies', () => { - beforeAll(() => - newProject({ - packages: ['@nx/js'], - }) - ); - - afterAll(() => cleanupProject()); - - it('should successfully build a project graph when cross-workspace implicit dependencies are present', () => { - const npmPackage = uniq('npm-package'); - runCLI(`generate @nx/workspace:npm-package ${npmPackage}`); - - function setImplicitDependencies(deps: string[]) { - updateFile(join(npmPackage, 'package.json'), (content) => { - const json = JSON.parse(content); - json.nx = { - ...json.nx, - implicitDependencies: deps, - }; - return JSON.stringify(json, null, 2); - }); - } - - // First set the implicit dependencies to an intentionally invalid value to prove the command fails during project graph construction - setImplicitDependencies(['this-project-does-not-exist']); - expect( - runCLI(`test ${npmPackage}`, { - silenceError: true, - }) - ).toContain('Failed to process project graph'); - - // Now set the implicit dependencies to a cross-workspace reference to prove that it is valid, despite not being resolvable in the current workspace - setImplicitDependencies(['nx-cloud:another-workspace']); - expect( - runCLI(`test ${npmPackage}`, { - silenceError: true, - }) - ).toContain('Successfully ran target test'); - }); -}); diff --git a/e2e/nx/src/misc-report-list.test.ts b/e2e/nx/src/misc-report-list.test.ts new file mode 100644 index 00000000000000..8c1b9abb0271c1 --- /dev/null +++ b/e2e/nx/src/misc-report-list.test.ts @@ -0,0 +1,121 @@ +import { + cleanupProject, + getPublishedVersion, + runCLI, + tmpProjPath, + uniq, +} from '@nx/e2e-utils'; +import { renameSync } from 'fs'; +import { setupMiscTests } from './misc-setup'; + +describe('Nx Commands - report and list', () => { + beforeAll(() => setupMiscTests()); + + afterAll(() => cleanupProject()); + + it(`should report package versions`, async () => { + const reportOutput = runCLI('report'); + + expect(reportOutput).toEqual( + expect.stringMatching( + new RegExp(`\@nx\/workspace.*:.*${getPublishedVersion()}`) + ) + ); + expect(reportOutput).toContain('@nx/workspace'); + }, 120000); + + it(`should list plugins`, async () => { + let listOutput = runCLI('list'); + + expect(listOutput).toContain('NX Installed plugins'); + + // just check for some, not all + expect(listOutput).toContain('@nx/workspace'); + + // temporarily make it look like this isn't installed + // For pnpm, we need to rename the actual package in .pnpm directory, not just the symlink + const { readdirSync, statSync } = require('fs'); + const pnpmDir = tmpProjPath('node_modules/.pnpm'); + let renamedPnpmEntry = null; + + if (require('fs').existsSync(pnpmDir)) { + const entries = readdirSync(pnpmDir); + const nextEntries = entries.filter((entry) => entry.includes('nx+next@')); + + // Rename all nx+next entries + const renamedEntries = []; + for (const entry of nextEntries) { + const tmpName = entry.replace('@nx+next@', 'tmp_nx_next_'); + renameSync( + tmpProjPath(`node_modules/.pnpm/${entry}`), + tmpProjPath(`node_modules/.pnpm/${tmpName}`) + ); + renamedEntries.push(entry); + } + renamedPnpmEntry = renamedEntries; + } + + // Also rename the symlink + if (require('fs').existsSync(tmpProjPath('node_modules/@nx/next'))) { + renameSync( + tmpProjPath('node_modules/@nx/next'), + tmpProjPath('node_modules/@nx/next_tmp') + ); + } + + listOutput = runCLI('list'); + expect(listOutput).toContain('NX Also available'); + + // look for specific plugin + listOutput = runCLI('list @nx/workspace'); + + expect(listOutput).toContain('Capabilities in @nx/workspace'); + + // check for schematics + expect(listOutput).toContain('workspace'); + expect(listOutput).toContain('library'); + + // check for builders + expect(listOutput).toContain('run-commands'); + + listOutput = runCLI('list @nx/angular'); + + expect(listOutput).toContain('Capabilities in @nx/angular'); + + expect(listOutput).toContain('library'); + expect(listOutput).toContain('component'); + + // check for builders + expect(listOutput).toContain('package'); + + // look for uninstalled core plugin + listOutput = runCLI('list @nx/next'); + + expect(listOutput).toContain('NX @nx/next is not currently installed'); + + // look for an unknown plugin + listOutput = runCLI('list @wibble/fish'); + + expect(listOutput).toContain( + 'NX @wibble/fish is not currently installed' + ); + + // put back the @nx/next module (or all the other e2e tests after this will fail) + if (renamedPnpmEntry && Array.isArray(renamedPnpmEntry)) { + for (const entry of renamedPnpmEntry) { + const tmpName = entry.replace('@nx+next@', 'tmp_nx_next_'); + renameSync( + tmpProjPath(`node_modules/.pnpm/${tmpName}`), + tmpProjPath(`node_modules/.pnpm/${entry}`) + ); + } + } + + if (require('fs').existsSync(tmpProjPath('node_modules/@nx/next_tmp'))) { + renameSync( + tmpProjPath('node_modules/@nx/next_tmp'), + tmpProjPath('node_modules/@nx/next') + ); + } + }, 120000); +}); diff --git a/e2e/nx/src/misc-setup.ts b/e2e/nx/src/misc-setup.ts new file mode 100644 index 00000000000000..db57a1e23cc08f --- /dev/null +++ b/e2e/nx/src/misc-setup.ts @@ -0,0 +1,7 @@ +import { newProject } from '@nx/e2e-utils'; + +export function setupMiscTests() { + return newProject({ + packages: ['@nx/web', '@nx/angular', '@nx/next'], + }); +} diff --git a/e2e/nx/src/misc-show.test.ts b/e2e/nx/src/misc-show.test.ts new file mode 100644 index 00000000000000..d1e24fdaddcb0a --- /dev/null +++ b/e2e/nx/src/misc-show.test.ts @@ -0,0 +1,145 @@ +import type { ProjectConfiguration } from '@nx/devkit'; +import { + cleanupProject, + killProcessAndPorts, + runCLI, + runCommandUntil, + uniq, +} from '@nx/e2e-utils'; +import { setupMiscTests } from './misc-setup'; + +describe('Nx Commands - show', () => { + beforeAll(() => setupMiscTests()); + + afterAll(() => cleanupProject()); + + it('should show the list of projects', async () => { + const app1 = uniq('myapp'); + const app2 = uniq('myapp'); + expect( + runCLI('show projects').replace(/.*nx show projects( --verbose)?\n/, '') + ).toEqual(''); + + runCLI(`generate @nx/web:app apps/${app1} --tags e2etag`); + runCLI(`generate @nx/web:app apps/${app2}`); + + const s = runCLI('show projects').split('\n'); + + expect(s.length).toEqual(5); + expect(s).toContain(app1); + expect(s).toContain(app2); + expect(s).toContain(`${app1}-e2e`); + expect(s).toContain(`${app2}-e2e`); + + const withTag = JSON.parse(runCLI('show projects -p tag:e2etag --json')); + expect(withTag).toEqual([app1]); + + const withTargets = JSON.parse( + runCLI('show projects --with-target e2e --json') + ); + expect(withTargets).toEqual( + expect.arrayContaining([`${app1}-e2e`, `${app2}-e2e`]) + ); + expect(withTargets.length).toEqual(2); + }); + + it('should show detailed project info', () => { + const app = uniq('myapp'); + runCLI( + `generate @nx/web:app apps/${app} --bundler=webpack --unitTestRunner=vitest --linter=eslint` + ); + const project: ProjectConfiguration = JSON.parse( + runCLI(`show project ${app} --json`) + ); + expect(project.targets.build).toBeDefined(); + expect(project.targets.lint).toBeDefined(); + }); + + it('should open project details view', async () => { + const app = uniq('myapp'); + runCLI(`generate @nx/web:app apps/${app}`); + let url: string; + let port: number; + const childProcess = await runCommandUntil( + `show project ${app} --web --open=false`, + (output) => { + console.log(output); + // output should contain 'Project graph started at http://127.0.0.1:{port}' + if (output.includes('Project graph started at http://')) { + const match = /https?:\/\/[\d.]+:(?\d+)/.exec(output); + if (match) { + port = parseInt(match.groups.port); + url = match[0]; + return true; + } + } + return false; + } + ); + // Check that url is alive + const response = await fetch(url); + expect(response.status).toEqual(200); + await killProcessAndPorts(childProcess.pid, port); + }, 700000); + + it('should find alternative port when default port is occupied', async () => { + const app = uniq('myapp'); + runCLI(`generate @nx/web:app apps/${app}`); + + const http = require('http'); + + // Create a server that occupies the default port 4211 + const blockingServer = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('blocking server'); + }); + + await new Promise((resolve) => { + blockingServer.listen(4211, '127.0.0.1', () => { + console.log('Blocking server started on port 4211'); + resolve(); + }); + }); + + let url: string; + let port: number; + let foundAlternativePort = false; + + try { + const childProcess = await runCommandUntil( + `show project ${app} --web --open=false`, + (output) => { + console.log(output); + // Should find alternative port and show message about port being in use + if (output.includes('Port 4211 was already in use, using port')) { + foundAlternativePort = true; + } + // output should contain 'Project graph started at http://127.0.0.1:{port}' + if (output.includes('Project graph started at http://')) { + const match = /https?:\/\/[\d.]+:(?\d+)/.exec(output); + if (match) { + port = parseInt(match.groups.port); + url = match[0]; + return true; + } + } + return false; + } + ); + + // Verify that an alternative port was found + expect(foundAlternativePort).toBe(true); + expect(port).not.toBe(4211); + expect(port).toBeGreaterThan(4211); + + // Check that url is alive + const response = await fetch(url); + expect(response.status).toEqual(200); + + await killProcessAndPorts(childProcess.pid, port); + } finally { + // Clean up the blocking server + blockingServer.close(); + } + }, 700000); +}); diff --git a/e2e/nx/src/run-bail.test.ts b/e2e/nx/src/run-bail.test.ts new file mode 100644 index 00000000000000..efdefd557db017 --- /dev/null +++ b/e2e/nx/src/run-bail.test.ts @@ -0,0 +1,71 @@ +import { + cleanupProject, + readJson, + runCLI, + uniq, + updateJson, +} from '@nx/e2e-utils'; +import { setupRunTests } from './run-setup'; + +describe('Nx Bail', () => { + let proj: string; + beforeAll(() => (proj = setupRunTests())); + afterAll(() => cleanupProject()); + + // Ensures that nx.json is restored to its original state after each test + let existingNxJson; + beforeEach(() => { + existingNxJson = readJson('nx.json'); + }); + afterEach(() => { + updateJson('nx.json', () => existingNxJson); + }); + + it('should stop executing all tasks when one of the tasks fails', async () => { + const myapp1 = uniq('a'); + const myapp2 = uniq('b'); + runCLI(`generate @nx/web:app apps/${myapp1}`); + runCLI(`generate @nx/web:app apps/${myapp2}`); + updateJson(`apps/${myapp1}/project.json`, (c) => { + c.targets['error'] = { + command: 'echo boom1 && exit 1', + }; + return c; + }); + updateJson(`apps/${myapp2}/project.json`, (c) => { + c.targets['error'] = { + executor: 'nx:run-commands', + options: { + command: 'echo boom2 && exit 1', + }, + }; + return c; + }); + + let withoutBail = runCLI(`run-many --target=error --parallel=1`, { + silenceError: true, + }) + .split('\n') + .map((r) => r.trim()) + .filter((r) => r); + + withoutBail = withoutBail.slice(withoutBail.indexOf('Failed tasks:')); + expect(withoutBail).toContain(`- ${myapp1}:error`); + expect(withoutBail).toContain(`- ${myapp2}:error`); + + let withBail = runCLI(`run-many --target=error --parallel=1 --nx-bail`, { + silenceError: true, + }) + .split('\n') + .map((r) => r.trim()) + .filter((r) => r); + withBail = withBail.slice(withBail.indexOf('Failed tasks:')); + + if (withBail[1] === `- ${myapp1}:error`) { + expect(withBail).not.toContain(`- ${myapp2}:error`); + } else { + expect(withBail[1]).toEqual(`- ${myapp2}:error`); + expect(withBail).not.toContain(`- ${myapp1}:error`); + } + }); +}); diff --git a/e2e/nx/src/run-exec.test.ts b/e2e/nx/src/run-exec.test.ts new file mode 100644 index 00000000000000..133534dc6aec90 --- /dev/null +++ b/e2e/nx/src/run-exec.test.ts @@ -0,0 +1,199 @@ +import { + cleanupProject, + fileExists, + readJson, + removeFile, + runCLI, + runCommand, + tmpProjPath, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { PackageJson } from 'nx/src/utils/package-json'; +import * as path from 'path'; +import { setupRunTests } from './run-setup'; + +describe('exec', () => { + let proj: string; + let pkg: string; + let pkg2: string; + let pkgRoot: string; + let pkg2Root: string; + let originalRootPackageJson: PackageJson; + + beforeAll(() => { + proj = setupRunTests(); + originalRootPackageJson = readJson('package.json'); + pkg = uniq('package'); + pkg2 = uniq('package'); + pkgRoot = tmpProjPath(path.join('libs', pkg)); + pkg2Root = tmpProjPath(path.join('libs', pkg2)); + runCLI( + `generate @nx/js:lib ${pkg} --bundler=none --unitTestRunner=none --directory=libs/${pkg}` + ); + runCLI( + `generate @nx/js:lib ${pkg2} --bundler=none --unitTestRunner=none --directory=libs/${pkg2}` + ); + + updateJson('package.json', (v) => { + v.workspaces = ['libs/*']; + return v; + }); + + updateFile( + `libs/${pkg}/package.json`, + JSON.stringify({ + name: pkg, + version: '0.0.1', + scripts: { + build: 'nx exec -- echo HELLO', + 'build:option': 'nx exec -- echo HELLO WITH OPTION', + }, + nx: { + targets: { + build: { + cache: true, + }, + }, + }, + }) + ); + + updateFile( + `libs/${pkg2}/package.json`, + JSON.stringify({ + name: pkg2, + version: '0.0.1', + scripts: { + build: "nx exec -- echo '$NX_PROJECT_NAME'", + }, + }) + ); + + updateJson(`libs/${pkg2}/project.json`, (content) => { + content['implicitDependencies'] = [pkg]; + return content; + }); + }); + + afterAll(() => { + updateJson('package.json', () => originalRootPackageJson); + cleanupProject(); + }); + + // Ensures that nx.json is restored to its original state after each test + let existingNxJson; + beforeEach(() => { + existingNxJson = readJson('nx.json'); + }); + afterEach(() => { + updateJson('nx.json', () => existingNxJson); + }); + + it('should work for npm scripts', () => { + const output = runCommand('npm run build', { + cwd: pkgRoot, + }); + expect(output).toContain('HELLO'); + expect(output).toContain(`nx run ${pkg}:build`); + }); + + it('should run adhoc tasks in topological order', () => { + let output = runCLI('exec -- echo HELLO'); + expect(output).toContain('HELLO'); + + output = runCLI(`build ${pkg}`); + expect(output).toContain(pkg); + expect(output).not.toContain(pkg2); + + output = runCommand('npm run build', { + cwd: pkgRoot, + }); + expect(output).toContain(pkg); + expect(output).not.toContain(pkg2); + + output = runCLI(`exec -- echo '$NX_PROJECT_NAME'`).replace(/\s+/g, ' '); + expect(output).toContain(pkg); + expect(output).toContain(pkg2); + + output = runCLI("exec -- echo '$NX_PROJECT_ROOT_PATH'").replace( + /\s+/g, + ' ' + ); + expect(output).toContain(`${path.join('libs', pkg)}`); + expect(output).toContain(`${path.join('libs', pkg2)}`); + + output = runCLI(`exec --projects ${pkg} -- echo WORLD`); + expect(output).toContain('WORLD'); + + output = runCLI(`exec --projects ${pkg} -- echo '$NX_PROJECT_NAME'`); + expect(output).toContain(pkg); + expect(output).not.toContain(pkg2); + }); + + it('should work for npm scripts with delimiter', () => { + const output = runCommand('npm run build:option', { cwd: pkgRoot }); + expect(output).toContain('HELLO WITH OPTION'); + expect(output).toContain(`nx run ${pkg}:"build:option"`); + }); + + it('should pass overrides', () => { + const output = runCommand('npm run build WORLD', { + cwd: pkgRoot, + }); + expect(output).toContain('HELLO WORLD'); + }); + + describe('caching', () => { + it('should cache subsequent calls', () => { + runCommand('npm run build', { + cwd: pkgRoot, + }); + const output = runCommand('npm run build', { + cwd: pkgRoot, + }); + expect(output).toContain('Nx read the output from the cache'); + }); + + it('should read outputs', () => { + const nodeCommands = [ + "const fs = require('fs')", + "fs.mkdirSync('../../tmp/exec-outputs-test', {recursive: true})", + "fs.writeFileSync('../../tmp/exec-outputs-test/file.txt', 'Outputs')", + ]; + updateFile( + `libs/${pkg}/package.json`, + JSON.stringify({ + name: pkg, + version: '0.0.1', + scripts: { + build: `nx exec -- node -e "${nodeCommands.join(';')}"`, + }, + nx: { + targets: { + build: { + cache: true, + outputs: ['{workspaceRoot}/tmp/exec-outputs-test'], + }, + }, + }, + }) + ); + runCommand('npm run build', { + cwd: pkgRoot, + }); + expect( + fileExists(tmpProjPath('tmp/exec-outputs-test/file.txt')) + ).toBeTruthy(); + removeFile('tmp'); + const output = runCommand('npm run build', { + cwd: pkgRoot, + }); + expect(output).toContain('[local cache]'); + expect( + fileExists(tmpProjPath('tmp/exec-outputs-test/file.txt')) + ).toBeTruthy(); + }); + }); +}); diff --git a/e2e/nx/src/run-forwarding-params.test.ts b/e2e/nx/src/run-forwarding-params.test.ts new file mode 100644 index 00000000000000..a843b8eb36eddd --- /dev/null +++ b/e2e/nx/src/run-forwarding-params.test.ts @@ -0,0 +1,127 @@ +import { + cleanupProject, + readJson, + runCLI, + uniq, + updateJson, +} from '@nx/e2e-utils'; +import { setupRunTests } from './run-setup'; + +describe('Nx Running Tests - forwarding params', () => { + let proj: string; + beforeAll(() => (proj = setupRunTests())); + afterAll(() => cleanupProject()); + + // Ensures that nx.json is restored to its original state after each test + let existingNxJson; + beforeEach(() => { + existingNxJson = readJson('nx.json'); + }); + afterEach(() => { + updateJson('nx.json', () => existingNxJson); + }); + + describe('(forwarding params)', () => { + let proj = uniq('proj'); + beforeAll(() => { + runCLI(`generate @nx/js:lib libs/${proj}`); + updateJson(`libs/${proj}/project.json`, (c) => { + c.targets['echo'] = { + command: 'echo ECHO:', + }; + return c; + }); + }); + + it('should support running with simple names (i.e. matching on full segments)', () => { + const foo = uniq('foo'); + const bar = uniq('bar'); + const nested = uniq('nested'); + runCLI(`generate @nx/js:lib libs/${foo}`); + runCLI(`generate @nx/js:lib libs/${bar}`); + runCLI(`generate @nx/js:lib libs/nested/${nested}`); + updateJson(`libs/${foo}/project.json`, (c) => { + c.name = `@acme/${foo}`; + c.targets['echo'] = { command: 'echo TEST' }; + return c; + }); + updateJson(`libs/${bar}/project.json`, (c) => { + c.name = `@acme/${bar}`; + c.targets['echo'] = { command: 'echo TEST' }; + return c; + }); + updateJson(`libs/nested/${nested}/project.json`, (c) => { + c.name = `@acme/nested/${bar}`; // The last segment is a duplicate + c.targets['echo'] = { command: 'echo TEST' }; + return c; + }); + + // Full segments should match + expect(() => runCLI(`echo ${foo}`)).not.toThrow(); + + // Multiple matches should fail + expect(() => runCLI(`echo ${bar}`)).toThrow(); + + // Partial segments should not match (Note: project foo has numbers in the end that aren't matched fully) + expect(() => runCLI(`echo foo`)).toThrow(); + }); + + it.each([ + '--watch false', + '--watch=false', + '--arr=a,b,c', + '--arr=a --arr=b --arr=c', + 'a', + '--a.b=1', + '--a.b 1', + '-- a b c --a --a.b=1', + '--ignored -- a b c --a --a.b=1', + ])('should forward %s properly', (args) => { + const output = runCLI(`echo ${proj} ${args}`); + expect(output).toContain(`ECHO: ${args.replace(/^.*-- /, '')}`); + }); + + it.each([ + { + args: '--test="hello world" "abc def"', + result: '--test="hello world" "abc def"', + }, + { + args: `--test="hello world" 'abc def'`, + result: '--test="hello world" "abc def"', + }, + { + args: `--test="hello world" 'abcdef'`, + result: '--test="hello world" abcdef', + }, + { + args: `--test='hello world' 'abcdef'`, + result: '--test="hello world" abcdef', + }, + { + args: `"--test='hello world' 'abcdef'"`, + result: `--test='hello world' 'abcdef'`, + }, + ])('should forward %args properly with quotes', ({ args, result }) => { + const output = runCLI(`echo ${proj} ${args}`); + expect(output).toContain(`ECHO: ${result}`); + }); + + it.each([ + { + args: '-- a b c --a --a.b=1 --no-color --no-parallel', + result: 'ECHO: a b c --a --a.b=1', + }, + { + args: '-- a b c --a --a.b=1 --color --parallel', + result: 'ECHO: a b c --a --a.b=1', + }, + ])( + 'should not forward --color --parallel for $args', + ({ args, result }) => { + const output = runCLI(`echo ${proj} ${args}`); + expect(output).toContain(result); + } + ); + }); +}); diff --git a/e2e/nx/src/run-many.test.ts b/e2e/nx/src/run-many.test.ts new file mode 100644 index 00000000000000..3144f1540b5314 --- /dev/null +++ b/e2e/nx/src/run-many.test.ts @@ -0,0 +1,164 @@ +import { + cleanupProject, + newProject, + readJson, + runCLI, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { setupRunTests } from './run-setup'; + +describe('run-many', () => { + let proj: string; + beforeAll(() => (proj = setupRunTests())); + afterAll(() => cleanupProject()); + + // Ensures that nx.json is restored to its original state after each test + let existingNxJson; + beforeEach(() => { + existingNxJson = readJson('nx.json'); + }); + afterEach(() => { + updateJson('nx.json', () => existingNxJson); + }); + + it('should build specific and all projects', () => { + // This is required to ensure the numbers used in the assertions make sense for this test + const proj = 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 @nx/web:app ${appA} --directory=apps/${appA}`); + runCLI( + `generate @nx/js:lib ${libA} --bundler=tsc --defaults --directory=libs/${libA}` + ); + runCLI( + `generate @nx/js:lib ${libB} --bundler=tsc --defaults --tags=ui-a --directory=libs/${libB}` + ); + runCLI( + `generate @nx/js:lib ${libC} --bundler=tsc --defaults --tags=ui-b,shared --directory=libs/${libC}` + ); + runCLI( + `generate @nx/node:lib ${libD} --defaults --tags=api --directory=libs/${libD} --buildable=false` + ); + + // libA depends on libC + updateFile( + `libs/${libA}/src/lib/${libA}.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 2 projects:`); + expect(buildParallel).not.toContain(`- ${appA}`); + expect(buildParallel).not.toContain(`- ${libA}`); + expect(buildParallel).toContain(`- ${libB}`); + expect(buildParallel).toContain(`- ${libC}`); + expect(buildParallel).not.toContain(`- ${libD}`); + expect(buildParallel).toContain('Successfully ran target build'); + + // testing run many --all starting + const buildAllParallel = runCLI(`run-many --target=build`); + expect(buildAllParallel).toContain(`Running target build for 4 projects:`); + expect(buildAllParallel).toContain(`- ${appA}`); + expect(buildAllParallel).toContain(`- ${libA}`); + expect(buildAllParallel).toContain(`- ${libB}`); + expect(buildAllParallel).toContain(`- ${libC}`); + expect(buildAllParallel).not.toContain(`- ${libD}`); + expect(buildAllParallel).toContain('Successfully ran target build'); + + // testing run many by tags + const buildByTagParallel = runCLI( + `run-many --target=build --projects="tag:ui*"` + ); + expect(buildByTagParallel).toContain( + `Running target build for 2 projects:` + ); + expect(buildByTagParallel).not.toContain(`- ${appA}`); + expect(buildByTagParallel).not.toContain(`- ${libA}`); + expect(buildByTagParallel).toContain(`- ${libB}`); + expect(buildByTagParallel).toContain(`- ${libC}`); + expect(buildByTagParallel).not.toContain(`- ${libD}`); + expect(buildByTagParallel).toContain('Successfully ran target build'); + + // testing run many with exclude + const buildWithExcludeParallel = runCLI( + `run-many --target=build --exclude="${libD},tag:ui*"` + ); + expect(buildWithExcludeParallel).toContain( + `Running target build for 2 projects and 1 task they depend on:` + ); + expect(buildWithExcludeParallel).toContain(`- ${appA}`); + expect(buildWithExcludeParallel).toContain(`- ${libA}`); + expect(buildWithExcludeParallel).not.toContain(`- ${libB}`); + expect(buildWithExcludeParallel).toContain(`${libC}`); // should still include libC as dependency despite exclude + expect(buildWithExcludeParallel).not.toContain(`- ${libD}`); + expect(buildWithExcludeParallel).toContain('Successfully ran target build'); + + // testing run many when project depends on other projects + const buildWithDeps = runCLI( + `run-many --target=build --projects="${libA}"` + ); + expect(buildWithDeps).toContain( + `Running target build for project ${libA} and 1 task it depends on:` + ); + expect(buildWithDeps).not.toContain(`- ${appA}`); + expect(buildWithDeps).toContain(`- ${libA}`); + expect(buildWithDeps).toContain(`${libC}`); // build should include libC as dependency + expect(buildWithDeps).not.toContain(`- ${libB}`); + expect(buildWithDeps).not.toContain(`- ${libD}`); + expect(buildWithDeps).toContain('Successfully ran target build'); + + // testing run many --configuration + const buildConfig = runCLI( + `run-many --target=build --projects="${appA},${libA}" --prod` + ); + expect(buildConfig).toContain( + `Running target build for 2 projects and 1 task they depend on:` + ); + expect(buildConfig).toContain(`run ${appA}:build`); + expect(buildConfig).toContain(`run ${libA}:build`); + expect(buildConfig).toContain(`run ${libC}:build`); + expect(buildConfig).toContain('Successfully ran target build'); + + // testing run many with daemon disabled + const buildWithDaemon = runCLI(`run-many --target=build`, { + env: { NX_DAEMON: 'false' }, + }); + expect(buildWithDaemon).toContain(`Successfully ran target build`); + }, 1000000); + + it('should run multiple targets', () => { + const myapp1 = uniq('myapp'); + const myapp2 = uniq('myapp'); + runCLI( + `generate @nx/web:app ${myapp1} --directory=apps/${myapp1} --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/web:app ${myapp2} --directory=apps/${myapp2} --unitTestRunner=vitest` + ); + + let outputs = runCLI( + // Options with lists can be specified using multiple args or with a delimiter (comma or space). + `run-many -t build -t test -p ${myapp1} ${myapp2}` + ); + expect(outputs).toContain('Running targets build, test for 2 projects:'); + + outputs = runCLI(`run-many -t build test -p=${myapp1},${myapp2}`); + expect(outputs).toContain('Running targets build, test for 2 projects:'); + }); +}); diff --git a/e2e/nx/src/run-one.test.ts b/e2e/nx/src/run-one.test.ts new file mode 100644 index 00000000000000..fb61316f157e68 --- /dev/null +++ b/e2e/nx/src/run-one.test.ts @@ -0,0 +1,276 @@ +import { + cleanupProject, + readJson, + runCLI, + runCommand, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { setupRunTests } from './run-setup'; + +describe('run-one', () => { + let proj: string; + beforeAll(() => (proj = setupRunTests())); + afterAll(() => cleanupProject()); + + // Ensures that nx.json is restored to its original state after each test + let existingNxJson; + beforeEach(() => { + existingNxJson = readJson('nx.json'); + }); + afterEach(() => { + updateJson('nx.json', () => existingNxJson); + }); + + it('should build a specific project', () => { + const myapp = uniq('app'); + runCLI(`generate @nx/web:app apps/${myapp}`); + + runCLI(`build ${myapp}`); + }, 10000); + + it('should support project name positional arg non-consecutive to target', () => { + const myapp = uniq('app'); + runCLI(`generate @nx/web:app apps/${myapp}`); + + runCLI(`build --verbose ${myapp}`); + }, 10000); + + it('should run targets from package json', () => { + const myapp = uniq('app'); + const target = uniq('script'); + const expectedOutput = uniq('myEchoedString'); + const expectedEnvOutput = uniq('myEnvString'); + + runCLI(`generate @nx/web:app apps/${myapp}`); + updateFile( + `apps/${myapp}/package.json`, + JSON.stringify({ + name: myapp, + scripts: { + [target]: `echo ${expectedOutput} $ENV_VAR`, + }, + nx: { + targets: { + [target]: { + configurations: { + production: {}, + }, + }, + }, + }, + }) + ); + + updateFile(`apps/${myapp}/.env.production`, `ENV_VAR=${expectedEnvOutput}`); + + expect(runCLI(`${target} ${myapp}`)).toContain(expectedOutput); + expect(runCLI(`${target} ${myapp}`)).not.toContain(expectedEnvOutput); + expect(runCLI(`${target} ${myapp} --configuration production`)).toContain( + expectedEnvOutput + ); + }, 10000); + + it('should build a specific project with the daemon disabled', () => { + const myapp = uniq('app'); + runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); + + const buildWithDaemon = runCLI(`build ${myapp}`, { + env: { NX_DAEMON: 'false' }, + }); + + expect(buildWithDaemon).toContain('Successfully ran target build'); + + const buildAgain = runCLI(`build ${myapp}`, { + env: { NX_DAEMON: 'false' }, + }); + + expect(buildAgain).toContain('[local cache]'); + }, 10000); + + it('should build the project when within the project root', () => { + const myapp = uniq('app'); + runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); + + // Should work within the project directory + expect(runCommand(`cd apps/${myapp}/src && npx nx build`)).toContain( + `nx run ${myapp}:build` + ); + }, 10000); + + it('should default to "run" target when only project is specified and it has a run target', () => { + const myapp = uniq('app'); + runCLI(`generate @nx/web:app apps/${myapp}`); + + // Add a "run" target to the project + updateJson(`apps/${myapp}/project.json`, (c) => { + c.targets['run'] = { + command: 'echo Running the app', + }; + return c; + }); + + // Running with just the project name should default to the "run" target + const output = runCLI(`run ${myapp}`); + expect(output).toContain('Running the app'); + expect(output).toContain(`nx run ${myapp}:run`); + }); + + it('should still require target when project does not have a run target', () => { + const myapp = uniq('app'); + runCLI(`generate @nx/web:app apps/${myapp}`); + + // Project has no "run" target, so it should fail + const result = runCLI(`run ${myapp}`, { silenceError: true }); + expect(result).toContain('Both project and target have to be specified'); + }); + + describe('target defaults + executor specifications', () => { + it('should be able to run targets with unspecified executor given an appropriate targetDefaults entry', () => { + const target = uniq('target'); + const lib = uniq('lib'); + + updateJson('nx.json', (nxJson) => { + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults[target] = { + executor: 'nx:run-commands', + options: { + command: `echo Hello from ${target}`, + }, + }; + return nxJson; + }); + + updateFile( + `libs/${lib}/project.json`, + JSON.stringify({ + name: lib, + targets: { + [target]: {}, + }, + }) + ); + + expect(runCLI(`${target} ${lib} --verbose`)).toContain( + `Hello from ${target}` + ); + }); + + it('should be able to pull options from targetDefaults based on executor', () => { + const target = uniq('target'); + const lib = uniq('lib'); + + updateJson('nx.json', (nxJson) => { + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults[`nx:run-commands`] = { + options: { + command: `echo Hello from ${target}`, + }, + }; + return nxJson; + }); + + updateFile( + `libs/${lib}/project.json`, + JSON.stringify({ + name: lib, + targets: { + [target]: { + executor: 'nx:run-commands', + }, + }, + }) + ); + + expect(runCLI(`${target} ${lib} --verbose`)).toContain( + `Hello from ${target}` + ); + }); + }); + + describe('target dependencies', () => { + let myapp; + let mylib1; + let mylib2; + beforeAll(() => { + myapp = uniq('myapp'); + mylib1 = uniq('mylib1'); + mylib2 = uniq('mylib1'); + runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); + runCLI(`generate @nx/js:lib ${mylib1} --directory=libs/${mylib1}`); + runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); + + updateFile( + `apps/${myapp}/src/main.ts`, + ` + import "@${proj}/${mylib1}"; + import "@${proj}/${mylib2}"; + ` + ); + }); + + it('should be able to include deps using dependsOn', async () => { + const originalWorkspace = readJson(`apps/${myapp}/project.json`); + updateJson(`apps/${myapp}/project.json`, (config) => { + config.targets.prep = { + executor: 'nx:run-commands', + options: { + command: 'echo PREP', + }, + }; + config.targets.build = { + dependsOn: ['prep', '^build'], + }; + return config; + }); + + const output = runCLI(`build ${myapp}`); + expect(output).toContain( + `NX Running target build for project ${myapp} and 3 tasks it depends on` + ); + expect(output).toContain(myapp); + expect(output).toContain(mylib1); + expect(output).toContain(mylib2); + expect(output).toContain('PREP'); + + updateJson(`apps/${myapp}/project.json`, () => originalWorkspace); + }, 10000); + + it('should be able to include deps using target defaults defined at the root', async () => { + const nxJson = readJson('nx.json'); + updateJson(`apps/${myapp}/project.json`, (config) => { + config.targets.prep = { + command: 'echo PREP > one.txt', + }; + config.targets.outside = { + command: 'echo OUTSIDE', + }; + return config; + }); + + nxJson.targetDefaults = { + prep: { + outputs: ['{workspaceRoot}/one.txt'], + cache: true, + }, + outside: { + dependsOn: ['prep'], + cache: true, + }, + }; + updateFile('nx.json', JSON.stringify(nxJson)); + + const output = runCLI(`outside ${myapp}`); + expect(output).toContain( + `NX Running target outside for project ${myapp} and 1 task it depends on` + ); + + const { removeFile, checkFilesExist } = require('@nx/e2e-utils'); + removeFile(`one.txt`); + runCLI(`outside ${myapp}`); + + checkFilesExist(`one.txt`); + }, 10000); + }); +}); diff --git a/e2e/nx/src/run-setup.ts b/e2e/nx/src/run-setup.ts new file mode 100644 index 00000000000000..85a46d313de33c --- /dev/null +++ b/e2e/nx/src/run-setup.ts @@ -0,0 +1,5 @@ +import { newProject } from '@nx/e2e-utils'; + +export function setupRunTests() { + return newProject({ packages: ['@nx/js', '@nx/web', '@nx/node'] }); +} diff --git a/e2e/nx/src/run-targets.test.ts b/e2e/nx/src/run-targets.test.ts new file mode 100644 index 00000000000000..3a8e13f2902510 --- /dev/null +++ b/e2e/nx/src/run-targets.test.ts @@ -0,0 +1,211 @@ +import { + cleanupProject, + isWindows, + readJson, + runCLI, + runCLIAsync, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { PackageJson } from 'nx/src/utils/package-json'; +import { setupRunTests } from './run-setup'; + +describe('Nx Running Tests - running targets', () => { + let proj: string; + beforeAll(() => (proj = setupRunTests())); + afterAll(() => cleanupProject()); + + // Ensures that nx.json is restored to its original state after each test + let existingNxJson; + beforeEach(() => { + existingNxJson = readJson('nx.json'); + }); + afterEach(() => { + updateJson('nx.json', () => existingNxJson); + }); + + it('should execute long running tasks', () => { + const myapp = uniq('myapp'); + runCLI(`generate @nx/web:app apps/${myapp}`); + updateJson(`apps/${myapp}/project.json`, (c) => { + c.targets['counter'] = { + executor: '@nx/workspace:counter', + options: { + to: 2, + }, + }; + return c; + }); + + const success = runCLI(`counter ${myapp} --result=true`); + expect(success).toContain('0'); + expect(success).toContain('1'); + + expect(() => runCLI(`counter ${myapp} --result=false`)).toThrow(); + }); + + it('should run npm scripts', async () => { + const mylib = uniq('mylib'); + runCLI(`generate @nx/node:lib libs/${mylib}`); + + // Used to restore targets to lib after test + const original = readJson(`libs/${mylib}/project.json`); + updateJson(`libs/${mylib}/project.json`, (j) => { + delete j.targets; + return j; + }); + + updateFile( + `libs/${mylib}/package.json`, + JSON.stringify({ + name: 'mylib1', + version: '1.0.0', + scripts: { 'echo:dev': `echo ECHOED`, 'echo:fail': 'should not run' }, + nx: { + includedScripts: ['echo:dev'], + }, + }) + ); + + const { stdout } = await runCLIAsync( + `echo:dev ${mylib} -- positional --a=123 --no-b`, + { + silent: true, + } + ); + if (isWindows()) { + expect(stdout).toMatch(/ECHOED "positional" "--a=123" "--no-b"/); + } else { + expect(stdout).toMatch(/ECHOED positional --a=123 --no-b/); + } + + expect(runCLI(`echo:fail ${mylib}`, { silenceError: true })).toContain( + `Cannot find configuration for task ${mylib}:echo:fail` + ); + + updateJson(`libs/${mylib}/project.json`, (c) => original); + }, 1000000); + + describe('tokens support', () => { + let app: string; + + beforeAll(async () => { + app = uniq('myapp'); + runCLI(`generate @nx/web:app apps/${app}`); + }); + + it('should support using {projectRoot} in options blocks in project.json', async () => { + updateJson(`apps/${app}/project.json`, (c) => { + c.targets['echo'] = { + command: `node -e 'console.log("{projectRoot}")'`, + }; + return c; + }); + + const output = runCLI(`echo ${app}`); + expect(output).toContain(`apps/${app}`); + }); + + it('should support using {projectName} in options blocks in project.json', () => { + updateJson(`apps/${app}/project.json`, (c) => { + c.targets['echo'] = { + command: `node -e 'console.log("{projectName}")'`, + }; + return c; + }); + + const output = runCLI(`echo ${app}`); + expect(output).toContain(app); + }); + + it('should support using {projectRoot} in targetDefaults', async () => { + updateJson(`nx.json`, (json) => { + json.targetDefaults = { + echo: { + command: `node -e 'console.log("{projectRoot}")'`, + }, + }; + return json; + }); + updateJson(`apps/${app}/project.json`, (c) => { + c.targets['echo'] = {}; + return c; + }); + const output = runCLI(`echo ${app}`); + expect(output).toContain(`apps/${app}`); + }); + + it('should support using {projectName} in targetDefaults', () => { + updateJson(`nx.json`, (json) => { + json.targetDefaults = { + echo: { + command: `node -e 'console.log("{projectName}")'`, + }, + }; + return json; + }); + updateJson(`apps/${app}/project.json`, (c) => { + c.targets['echo'] = {}; + return c; + }); + const output = runCLI(`echo ${app}`); + expect(output).toContain(app); + }); + }); + + it('should pass env option to nx:run-commands executor', () => { + const mylib = uniq('mylib'); + runCLI(`generate @nx/js:lib libs/${mylib}`); + + updateJson(`libs/${mylib}/project.json`, (c) => { + c.targets['echo'] = { + executor: 'nx:run-commands', + options: { + command: 'node -e "console.log(process.env.ONE)"', + env: { + ONE: 'TWO', + }, + }, + }; + return c; + }); + + const output = runCLI(`echo ${mylib}`); + expect(output).toContain('TWO'); + }); + + it('should not run dependencies if --no-dependencies is passed', () => { + const mylib = uniq('mylib'); + runCLI(`generate @nx/js:lib libs/${mylib}`); + + updateJson(`libs/${mylib}/project.json`, (c) => { + c.targets['one'] = { + executor: 'nx:run-commands', + options: { + command: 'echo ONE', + }, + }; + c.targets['two'] = { + executor: 'nx:run-commands', + options: { + command: 'echo TWO', + }, + dependsOn: ['one'], + }; + c.targets['three'] = { + executor: 'nx:run-commands', + options: { + command: 'echo THREE', + }, + dependsOn: ['two'], + }; + return c; + }); + + const output = runCLI(`one ${mylib} --no-deps`); + expect(output).toContain('ONE'); + expect(output).not.toContain('TWO'); + expect(output).not.toContain('THREE'); + }); +}); diff --git a/e2e/nx/src/run.test.ts b/e2e/nx/src/run.test.ts deleted file mode 100644 index 7d3ececa81f7af..00000000000000 --- a/e2e/nx/src/run.test.ts +++ /dev/null @@ -1,947 +0,0 @@ -import { - checkFilesExist, - cleanupProject, - fileExists, - isWindows, - newProject, - readJson, - removeFile, - runCLI, - runCLIAsync, - runCommand, - tmpProjPath, - uniq, - updateFile, - updateJson, -} from '@nx/e2e-utils'; -import { PackageJson } from 'nx/src/utils/package-json'; -import * as path from 'path'; - -describe('Nx Running Tests', () => { - let proj: string; - beforeAll( - () => (proj = newProject({ packages: ['@nx/js', '@nx/web', '@nx/node'] })) - ); - afterAll(() => cleanupProject()); - - // Ensures that nx.json is restored to its original state after each test - let existingNxJson; - beforeEach(() => { - existingNxJson = readJson('nx.json'); - }); - afterEach(() => { - updateFile('nx.json', JSON.stringify(existingNxJson, null, 2)); - }); - - describe('running targets', () => { - describe('(forwarding params)', () => { - let proj = uniq('proj'); - beforeAll(() => { - runCLI(`generate @nx/js:lib libs/${proj}`); - updateJson(`libs/${proj}/project.json`, (c) => { - c.targets['echo'] = { - command: 'echo ECHO:', - }; - return c; - }); - }); - - it('should support running with simple names (i.e. matching on full segments)', () => { - const foo = uniq('foo'); - const bar = uniq('bar'); - const nested = uniq('nested'); - runCLI(`generate @nx/js:lib libs/${foo}`); - runCLI(`generate @nx/js:lib libs/${bar}`); - runCLI(`generate @nx/js:lib libs/nested/${nested}`); - updateJson(`libs/${foo}/project.json`, (c) => { - c.name = `@acme/${foo}`; - c.targets['echo'] = { command: 'echo TEST' }; - return c; - }); - updateJson(`libs/${bar}/project.json`, (c) => { - c.name = `@acme/${bar}`; - c.targets['echo'] = { command: 'echo TEST' }; - return c; - }); - updateJson(`libs/nested/${nested}/project.json`, (c) => { - c.name = `@acme/nested/${bar}`; // The last segment is a duplicate - c.targets['echo'] = { command: 'echo TEST' }; - return c; - }); - - // Full segments should match - expect(() => runCLI(`echo ${foo}`)).not.toThrow(); - - // Multiple matches should fail - expect(() => runCLI(`echo ${bar}`)).toThrow(); - - // Partial segments should not match (Note: project foo has numbers in the end that aren't matched fully) - expect(() => runCLI(`echo foo`)).toThrow(); - }); - - it.each([ - '--watch false', - '--watch=false', - '--arr=a,b,c', - '--arr=a --arr=b --arr=c', - 'a', - '--a.b=1', - '--a.b 1', - '-- a b c --a --a.b=1', - '--ignored -- a b c --a --a.b=1', - ])('should forward %s properly', (args) => { - const output = runCLI(`echo ${proj} ${args}`); - expect(output).toContain(`ECHO: ${args.replace(/^.*-- /, '')}`); - }); - - it.each([ - { - args: '--test="hello world" "abc def"', - result: '--test="hello world" "abc def"', - }, - { - args: `--test="hello world" 'abc def'`, - result: '--test="hello world" "abc def"', - }, - { - args: `--test="hello world" 'abcdef'`, - result: '--test="hello world" abcdef', - }, - { - args: `--test='hello world' 'abcdef'`, - result: '--test="hello world" abcdef', - }, - { - args: `"--test='hello world' 'abcdef'"`, - result: `--test='hello world' 'abcdef'`, - }, - ])('should forward %args properly with quotes', ({ args, result }) => { - const output = runCLI(`echo ${proj} ${args}`); - expect(output).toContain(`ECHO: ${result}`); - }); - - it.each([ - { - args: '-- a b c --a --a.b=1 --no-color --no-parallel', - result: 'ECHO: a b c --a --a.b=1', - }, - { - args: '-- a b c --a --a.b=1 --color --parallel', - result: 'ECHO: a b c --a --a.b=1', - }, - ])( - 'should not forward --color --parallel for $args', - ({ args, result }) => { - const output = runCLI(`echo ${proj} ${args}`); - expect(output).toContain(result); - } - ); - }); - - it('should execute long running tasks', () => { - const myapp = uniq('myapp'); - runCLI(`generate @nx/web:app apps/${myapp}`); - updateJson(`apps/${myapp}/project.json`, (c) => { - c.targets['counter'] = { - executor: '@nx/workspace:counter', - options: { - to: 2, - }, - }; - return c; - }); - - const success = runCLI(`counter ${myapp} --result=true`); - expect(success).toContain('0'); - expect(success).toContain('1'); - - expect(() => runCLI(`counter ${myapp} --result=false`)).toThrow(); - }); - - it('should run npm scripts', async () => { - const mylib = uniq('mylib'); - runCLI(`generate @nx/node:lib libs/${mylib}`); - - // Used to restore targets to lib after test - const original = readJson(`libs/${mylib}/project.json`); - updateJson(`libs/${mylib}/project.json`, (j) => { - delete j.targets; - return j; - }); - - updateFile( - `libs/${mylib}/package.json`, - JSON.stringify({ - name: 'mylib1', - version: '1.0.0', - scripts: { 'echo:dev': `echo ECHOED`, 'echo:fail': 'should not run' }, - nx: { - includedScripts: ['echo:dev'], - }, - }) - ); - - const { stdout } = await runCLIAsync( - `echo:dev ${mylib} -- positional --a=123 --no-b`, - { - silent: true, - } - ); - if (isWindows()) { - expect(stdout).toMatch(/ECHOED "positional" "--a=123" "--no-b"/); - } else { - expect(stdout).toMatch(/ECHOED positional --a=123 --no-b/); - } - - expect(runCLI(`echo:fail ${mylib}`, { silenceError: true })).toContain( - `Cannot find configuration for task ${mylib}:echo:fail` - ); - - updateJson(`libs/${mylib}/project.json`, (c) => original); - }, 1000000); - - describe('tokens support', () => { - let app: string; - - beforeAll(async () => { - app = uniq('myapp'); - runCLI(`generate @nx/web:app apps/${app}`); - }); - - it('should support using {projectRoot} in options blocks in project.json', async () => { - updateJson(`apps/${app}/project.json`, (c) => { - c.targets['echo'] = { - command: `node -e 'console.log("{projectRoot}")'`, - }; - return c; - }); - - const output = runCLI(`echo ${app}`); - expect(output).toContain(`apps/${app}`); - }); - - it('should support using {projectName} in options blocks in project.json', () => { - updateJson(`apps/${app}/project.json`, (c) => { - c.targets['echo'] = { - command: `node -e 'console.log("{projectName}")'`, - }; - return c; - }); - - const output = runCLI(`echo ${app}`); - expect(output).toContain(app); - }); - - it('should support using {projectRoot} in targetDefaults', async () => { - updateJson(`nx.json`, (json) => { - json.targetDefaults = { - echo: { - command: `node -e 'console.log("{projectRoot}")'`, - }, - }; - return json; - }); - updateJson(`apps/${app}/project.json`, (c) => { - c.targets['echo'] = {}; - return c; - }); - const output = runCLI(`echo ${app}`); - expect(output).toContain(`apps/${app}`); - }); - - it('should support using {projectName} in targetDefaults', () => { - updateJson(`nx.json`, (json) => { - json.targetDefaults = { - echo: { - command: `node -e 'console.log("{projectName}")'`, - }, - }; - return json; - }); - updateJson(`apps/${app}/project.json`, (c) => { - c.targets['echo'] = {}; - return c; - }); - const output = runCLI(`echo ${app}`); - expect(output).toContain(app); - }); - }); - - it('should pass env option to nx:run-commands executor', () => { - const mylib = uniq('mylib'); - runCLI(`generate @nx/js:lib libs/${mylib}`); - - updateJson(`libs/${mylib}/project.json`, (c) => { - c.targets['echo'] = { - executor: 'nx:run-commands', - options: { - command: 'node -e "console.log(process.env.ONE)"', - env: { - ONE: 'TWO', - }, - }, - }; - return c; - }); - - const output = runCLI(`echo ${mylib}`); - expect(output).toContain('TWO'); - }); - - it('should not run dependencies if --no-dependencies is passed', () => { - const mylib = uniq('mylib'); - runCLI(`generate @nx/js:lib libs/${mylib}`); - - updateJson(`libs/${mylib}/project.json`, (c) => { - c.targets['one'] = { - executor: 'nx:run-commands', - options: { - command: 'echo ONE', - }, - }; - c.targets['two'] = { - executor: 'nx:run-commands', - options: { - command: 'echo TWO', - }, - dependsOn: ['one'], - }; - c.targets['three'] = { - executor: 'nx:run-commands', - options: { - command: 'echo THREE', - }, - dependsOn: ['two'], - }; - return c; - }); - - const output = runCLI(`one ${mylib} --no-deps`); - expect(output).toContain('ONE'); - expect(output).not.toContain('TWO'); - expect(output).not.toContain('THREE'); - }); - }); - - describe('Nx Bail', () => { - it('should stop executing all tasks when one of the tasks fails', async () => { - const myapp1 = uniq('a'); - const myapp2 = uniq('b'); - runCLI(`generate @nx/web:app apps/${myapp1}`); - runCLI(`generate @nx/web:app apps/${myapp2}`); - updateJson(`apps/${myapp1}/project.json`, (c) => { - c.targets['error'] = { - command: 'echo boom1 && exit 1', - }; - return c; - }); - updateJson(`apps/${myapp2}/project.json`, (c) => { - c.targets['error'] = { - executor: 'nx:run-commands', - options: { - command: 'echo boom2 && exit 1', - }, - }; - return c; - }); - - let withoutBail = runCLI(`run-many --target=error --parallel=1`, { - silenceError: true, - }) - .split('\n') - .map((r) => r.trim()) - .filter((r) => r); - - withoutBail = withoutBail.slice(withoutBail.indexOf('Failed tasks:')); - expect(withoutBail).toContain(`- ${myapp1}:error`); - expect(withoutBail).toContain(`- ${myapp2}:error`); - - let withBail = runCLI(`run-many --target=error --parallel=1 --nx-bail`, { - silenceError: true, - }) - .split('\n') - .map((r) => r.trim()) - .filter((r) => r); - withBail = withBail.slice(withBail.indexOf('Failed tasks:')); - - if (withBail[1] === `- ${myapp1}:error`) { - expect(withBail).not.toContain(`- ${myapp2}:error`); - } else { - expect(withBail[1]).toEqual(`- ${myapp2}:error`); - expect(withBail).not.toContain(`- ${myapp1}:error`); - } - }); - }); - - describe('run-one', () => { - it('should build a specific project', () => { - const myapp = uniq('app'); - runCLI(`generate @nx/web:app apps/${myapp}`); - - runCLI(`build ${myapp}`); - }, 10000); - - it('should support project name positional arg non-consecutive to target', () => { - const myapp = uniq('app'); - runCLI(`generate @nx/web:app apps/${myapp}`); - - runCLI(`build --verbose ${myapp}`); - }, 10000); - - it('should run targets from package json', () => { - const myapp = uniq('app'); - const target = uniq('script'); - const expectedOutput = uniq('myEchoedString'); - const expectedEnvOutput = uniq('myEnvString'); - - runCLI(`generate @nx/web:app apps/${myapp}`); - updateFile( - `apps/${myapp}/package.json`, - JSON.stringify({ - name: myapp, - scripts: { - [target]: `echo ${expectedOutput} $ENV_VAR`, - }, - nx: { - targets: { - [target]: { - configurations: { - production: {}, - }, - }, - }, - }, - }) - ); - - updateFile( - `apps/${myapp}/.env.production`, - `ENV_VAR=${expectedEnvOutput}` - ); - - expect(runCLI(`${target} ${myapp}`)).toContain(expectedOutput); - expect(runCLI(`${target} ${myapp}`)).not.toContain(expectedEnvOutput); - expect(runCLI(`${target} ${myapp} --configuration production`)).toContain( - expectedEnvOutput - ); - }, 10000); - - it('should build a specific project with the daemon disabled', () => { - const myapp = uniq('app'); - runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); - - const buildWithDaemon = runCLI(`build ${myapp}`, { - env: { NX_DAEMON: 'false' }, - }); - - expect(buildWithDaemon).toContain('Successfully ran target build'); - - const buildAgain = runCLI(`build ${myapp}`, { - env: { NX_DAEMON: 'false' }, - }); - - expect(buildAgain).toContain('[local cache]'); - }, 10000); - - it('should build the project when within the project root', () => { - const myapp = uniq('app'); - runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); - - // Should work within the project directory - expect(runCommand(`cd apps/${myapp}/src && npx nx build`)).toContain( - `nx run ${myapp}:build` - ); - }, 10000); - - it('should default to "run" target when only project is specified and it has a run target', () => { - const myapp = uniq('app'); - runCLI(`generate @nx/web:app apps/${myapp}`); - - // Add a "run" target to the project - updateJson(`apps/${myapp}/project.json`, (c) => { - c.targets['run'] = { - command: 'echo Running the app', - }; - return c; - }); - - // Running with just the project name should default to the "run" target - const output = runCLI(`run ${myapp}`); - expect(output).toContain('Running the app'); - expect(output).toContain(`nx run ${myapp}:run`); - }); - - it('should still require target when project does not have a run target', () => { - const myapp = uniq('app'); - runCLI(`generate @nx/web:app apps/${myapp}`); - - // Project has no "run" target, so it should fail - const result = runCLI(`run ${myapp}`, { silenceError: true }); - expect(result).toContain('Both project and target have to be specified'); - }); - - describe('target defaults + executor specifications', () => { - it('should be able to run targets with unspecified executor given an appropriate targetDefaults entry', () => { - const target = uniq('target'); - const lib = uniq('lib'); - - updateJson('nx.json', (nxJson) => { - nxJson.targetDefaults ??= {}; - nxJson.targetDefaults[target] = { - executor: 'nx:run-commands', - options: { - command: `echo Hello from ${target}`, - }, - }; - return nxJson; - }); - - updateFile( - `libs/${lib}/project.json`, - JSON.stringify({ - name: lib, - targets: { - [target]: {}, - }, - }) - ); - - expect(runCLI(`${target} ${lib} --verbose`)).toContain( - `Hello from ${target}` - ); - }); - - it('should be able to pull options from targetDefaults based on executor', () => { - const target = uniq('target'); - const lib = uniq('lib'); - - updateJson('nx.json', (nxJson) => { - nxJson.targetDefaults ??= {}; - nxJson.targetDefaults[`nx:run-commands`] = { - options: { - command: `echo Hello from ${target}`, - }, - }; - return nxJson; - }); - - updateFile( - `libs/${lib}/project.json`, - JSON.stringify({ - name: lib, - targets: { - [target]: { - executor: 'nx:run-commands', - }, - }, - }) - ); - - expect(runCLI(`${target} ${lib} --verbose`)).toContain( - `Hello from ${target}` - ); - }); - }); - - describe('target dependencies', () => { - let myapp; - let mylib1; - let mylib2; - beforeAll(() => { - myapp = uniq('myapp'); - mylib1 = uniq('mylib1'); - mylib2 = uniq('mylib1'); - runCLI(`generate @nx/web:app ${myapp} --directory=apps/${myapp}`); - runCLI(`generate @nx/js:lib ${mylib1} --directory=libs/${mylib1}`); - runCLI(`generate @nx/js:lib ${mylib2} --directory=libs/${mylib2}`); - - updateFile( - `apps/${myapp}/src/main.ts`, - ` - import "@${proj}/${mylib1}"; - import "@${proj}/${mylib2}"; - ` - ); - }); - - it('should be able to include deps using dependsOn', async () => { - const originalWorkspace = readJson(`apps/${myapp}/project.json`); - updateJson(`apps/${myapp}/project.json`, (config) => { - config.targets.prep = { - executor: 'nx:run-commands', - options: { - command: 'echo PREP', - }, - }; - config.targets.build = { - dependsOn: ['prep', '^build'], - }; - return config; - }); - - const output = runCLI(`build ${myapp}`); - expect(output).toContain( - `NX Running target build for project ${myapp} and 3 tasks it depends on` - ); - expect(output).toContain(myapp); - expect(output).toContain(mylib1); - expect(output).toContain(mylib2); - expect(output).toContain('PREP'); - - updateJson(`apps/${myapp}/project.json`, () => originalWorkspace); - }, 10000); - - it('should be able to include deps using target defaults defined at the root', async () => { - const nxJson = readJson('nx.json'); - updateJson(`apps/${myapp}/project.json`, (config) => { - config.targets.prep = { - command: 'echo PREP > one.txt', - }; - config.targets.outside = { - command: 'echo OUTSIDE', - }; - return config; - }); - - nxJson.targetDefaults = { - prep: { - outputs: ['{workspaceRoot}/one.txt'], - cache: true, - }, - outside: { - dependsOn: ['prep'], - cache: true, - }, - }; - updateFile('nx.json', JSON.stringify(nxJson)); - - const output = runCLI(`outside ${myapp}`); - expect(output).toContain( - `NX Running target outside for project ${myapp} and 1 task it depends on` - ); - - removeFile(`one.txt`); - runCLI(`outside ${myapp}`); - - checkFilesExist(`one.txt`); - }, 10000); - }); - }); - - describe('run-many', () => { - it('should build specific and all projects', () => { - // This is required to ensure the numbers used in the assertions make sense for this test - const proj = 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 @nx/web:app ${appA} --directory=apps/${appA}`); - runCLI( - `generate @nx/js:lib ${libA} --bundler=tsc --defaults --directory=libs/${libA}` - ); - runCLI( - `generate @nx/js:lib ${libB} --bundler=tsc --defaults --tags=ui-a --directory=libs/${libB}` - ); - runCLI( - `generate @nx/js:lib ${libC} --bundler=tsc --defaults --tags=ui-b,shared --directory=libs/${libC}` - ); - runCLI( - `generate @nx/node:lib ${libD} --defaults --tags=api --directory=libs/${libD} --buildable=false` - ); - - // libA depends on libC - updateFile( - `libs/${libA}/src/lib/${libA}.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 2 projects:`); - expect(buildParallel).not.toContain(`- ${appA}`); - expect(buildParallel).not.toContain(`- ${libA}`); - expect(buildParallel).toContain(`- ${libB}`); - expect(buildParallel).toContain(`- ${libC}`); - expect(buildParallel).not.toContain(`- ${libD}`); - expect(buildParallel).toContain('Successfully ran target build'); - - // testing run many --all starting - const buildAllParallel = runCLI(`run-many --target=build`); - expect(buildAllParallel).toContain( - `Running target build for 4 projects:` - ); - expect(buildAllParallel).toContain(`- ${appA}`); - expect(buildAllParallel).toContain(`- ${libA}`); - expect(buildAllParallel).toContain(`- ${libB}`); - expect(buildAllParallel).toContain(`- ${libC}`); - expect(buildAllParallel).not.toContain(`- ${libD}`); - expect(buildAllParallel).toContain('Successfully ran target build'); - - // testing run many by tags - const buildByTagParallel = runCLI( - `run-many --target=build --projects="tag:ui*"` - ); - expect(buildByTagParallel).toContain( - `Running target build for 2 projects:` - ); - expect(buildByTagParallel).not.toContain(`- ${appA}`); - expect(buildByTagParallel).not.toContain(`- ${libA}`); - expect(buildByTagParallel).toContain(`- ${libB}`); - expect(buildByTagParallel).toContain(`- ${libC}`); - expect(buildByTagParallel).not.toContain(`- ${libD}`); - expect(buildByTagParallel).toContain('Successfully ran target build'); - - // testing run many with exclude - const buildWithExcludeParallel = runCLI( - `run-many --target=build --exclude="${libD},tag:ui*"` - ); - expect(buildWithExcludeParallel).toContain( - `Running target build for 2 projects and 1 task they depend on:` - ); - expect(buildWithExcludeParallel).toContain(`- ${appA}`); - expect(buildWithExcludeParallel).toContain(`- ${libA}`); - expect(buildWithExcludeParallel).not.toContain(`- ${libB}`); - expect(buildWithExcludeParallel).toContain(`${libC}`); // should still include libC as dependency despite exclude - expect(buildWithExcludeParallel).not.toContain(`- ${libD}`); - expect(buildWithExcludeParallel).toContain( - 'Successfully ran target build' - ); - - // testing run many when project depends on other projects - const buildWithDeps = runCLI( - `run-many --target=build --projects="${libA}"` - ); - expect(buildWithDeps).toContain( - `Running target build for project ${libA} and 1 task it depends on:` - ); - expect(buildWithDeps).not.toContain(`- ${appA}`); - expect(buildWithDeps).toContain(`- ${libA}`); - expect(buildWithDeps).toContain(`${libC}`); // build should include libC as dependency - expect(buildWithDeps).not.toContain(`- ${libB}`); - expect(buildWithDeps).not.toContain(`- ${libD}`); - expect(buildWithDeps).toContain('Successfully ran target build'); - - // testing run many --configuration - const buildConfig = runCLI( - `run-many --target=build --projects="${appA},${libA}" --prod` - ); - expect(buildConfig).toContain( - `Running target build for 2 projects and 1 task they depend on:` - ); - expect(buildConfig).toContain(`run ${appA}:build`); - expect(buildConfig).toContain(`run ${libA}:build`); - expect(buildConfig).toContain(`run ${libC}:build`); - expect(buildConfig).toContain('Successfully ran target build'); - - // testing run many with daemon disabled - const buildWithDaemon = runCLI(`run-many --target=build`, { - env: { NX_DAEMON: 'false' }, - }); - expect(buildWithDaemon).toContain(`Successfully ran target build`); - }, 1000000); - - it('should run multiple targets', () => { - const myapp1 = uniq('myapp'); - const myapp2 = uniq('myapp'); - runCLI( - `generate @nx/web:app ${myapp1} --directory=apps/${myapp1} --unitTestRunner=vitest` - ); - runCLI( - `generate @nx/web:app ${myapp2} --directory=apps/${myapp2} --unitTestRunner=vitest` - ); - - let outputs = runCLI( - // Options with lists can be specified using multiple args or with a delimiter (comma or space). - `run-many -t build -t test -p ${myapp1} ${myapp2}` - ); - expect(outputs).toContain('Running targets build, test for 2 projects:'); - - outputs = runCLI(`run-many -t build test -p=${myapp1},${myapp2}`); - expect(outputs).toContain('Running targets build, test for 2 projects:'); - }); - }); - - describe('exec', () => { - let pkg: string; - let pkg2: string; - let pkgRoot: string; - let pkg2Root: string; - let originalRootPackageJson: PackageJson; - - beforeAll(() => { - originalRootPackageJson = readJson('package.json'); - pkg = uniq('package'); - pkg2 = uniq('package'); - pkgRoot = tmpProjPath(path.join('libs', pkg)); - pkg2Root = tmpProjPath(path.join('libs', pkg2)); - runCLI( - `generate @nx/js:lib ${pkg} --bundler=none --unitTestRunner=none --directory=libs/${pkg}` - ); - runCLI( - `generate @nx/js:lib ${pkg2} --bundler=none --unitTestRunner=none --directory=libs/${pkg2}` - ); - - updateJson('package.json', (v) => { - v.workspaces = ['libs/*']; - return v; - }); - - updateFile( - `libs/${pkg}/package.json`, - JSON.stringify({ - name: pkg, - version: '0.0.1', - scripts: { - build: 'nx exec -- echo HELLO', - 'build:option': 'nx exec -- echo HELLO WITH OPTION', - }, - nx: { - targets: { - build: { - cache: true, - }, - }, - }, - }) - ); - - updateFile( - `libs/${pkg2}/package.json`, - JSON.stringify({ - name: pkg2, - version: '0.0.1', - scripts: { - build: "nx exec -- echo '$NX_PROJECT_NAME'", - }, - }) - ); - - updateJson(`libs/${pkg2}/project.json`, (content) => { - content['implicitDependencies'] = [pkg]; - return content; - }); - }); - - afterAll(() => { - updateJson('package.json', () => originalRootPackageJson); - }); - - it('should work for npm scripts', () => { - const output = runCommand('npm run build', { - cwd: pkgRoot, - }); - expect(output).toContain('HELLO'); - expect(output).toContain(`nx run ${pkg}:build`); - }); - - it('should run adhoc tasks in topological order', () => { - let output = runCLI('exec -- echo HELLO'); - expect(output).toContain('HELLO'); - - output = runCLI(`build ${pkg}`); - expect(output).toContain(pkg); - expect(output).not.toContain(pkg2); - - output = runCommand('npm run build', { - cwd: pkgRoot, - }); - expect(output).toContain(pkg); - expect(output).not.toContain(pkg2); - - output = runCLI(`exec -- echo '$NX_PROJECT_NAME'`).replace(/\s+/g, ' '); - expect(output).toContain(pkg); - expect(output).toContain(pkg2); - - output = runCLI("exec -- echo '$NX_PROJECT_ROOT_PATH'").replace( - /\s+/g, - ' ' - ); - expect(output).toContain(`${path.join('libs', pkg)}`); - expect(output).toContain(`${path.join('libs', pkg2)}`); - - output = runCLI(`exec --projects ${pkg} -- echo WORLD`); - expect(output).toContain('WORLD'); - - output = runCLI(`exec --projects ${pkg} -- echo '$NX_PROJECT_NAME'`); - expect(output).toContain(pkg); - expect(output).not.toContain(pkg2); - }); - - it('should work for npm scripts with delimiter', () => { - const output = runCommand('npm run build:option', { cwd: pkgRoot }); - expect(output).toContain('HELLO WITH OPTION'); - expect(output).toContain(`nx run ${pkg}:"build:option"`); - }); - - it('should pass overrides', () => { - const output = runCommand('npm run build WORLD', { - cwd: pkgRoot, - }); - expect(output).toContain('HELLO WORLD'); - }); - - describe('caching', () => { - it('should cache subsequent calls', () => { - runCommand('npm run build', { - cwd: pkgRoot, - }); - const output = runCommand('npm run build', { - cwd: pkgRoot, - }); - expect(output).toContain('Nx read the output from the cache'); - }); - - it('should read outputs', () => { - const nodeCommands = [ - "const fs = require('fs')", - "fs.mkdirSync('../../tmp/exec-outputs-test', {recursive: true})", - "fs.writeFileSync('../../tmp/exec-outputs-test/file.txt', 'Outputs')", - ]; - updateFile( - `libs/${pkg}/package.json`, - JSON.stringify({ - name: pkg, - version: '0.0.1', - scripts: { - build: `nx exec -- node -e "${nodeCommands.join(';')}"`, - }, - nx: { - targets: { - build: { - cache: true, - outputs: ['{workspaceRoot}/tmp/exec-outputs-test'], - }, - }, - }, - }) - ); - runCommand('npm run build', { - cwd: pkgRoot, - }); - expect( - fileExists(tmpProjPath('tmp/exec-outputs-test/file.txt')) - ).toBeTruthy(); - removeFile('tmp'); - const output = runCommand('npm run build', { - cwd: pkgRoot, - }); - expect(output).toContain('[local cache]'); - expect( - fileExists(tmpProjPath('tmp/exec-outputs-test/file.txt')) - ).toBeTruthy(); - }); - }); - }); -}); diff --git a/e2e/nx/src/workspace-convert-to-monorepo.test.ts b/e2e/nx/src/workspace-convert-to-monorepo.test.ts new file mode 100644 index 00000000000000..1b3148541226a4 --- /dev/null +++ b/e2e/nx/src/workspace-convert-to-monorepo.test.ts @@ -0,0 +1,40 @@ +import { + checkFilesExist, + cleanupProject, + newProject, + runCLI, + runE2ETests, + uniq, +} from '@nx/e2e-utils'; + +let proj: string; + +describe('@nx/workspace:convert-to-monorepo', () => { + beforeEach(() => { + proj = newProject({ packages: ['@nx/react', '@nx/js'] }); + }); + + afterEach(() => cleanupProject()); + + it('should be convert a standalone vite and playwright react project to a monorepo', async () => { + const reactApp = uniq('reactapp'); + runCLI( + `generate @nx/react:app --name=${reactApp} --directory="." --rootProject=true --linter eslint --bundler=vite --unitTestRunner vitest --e2eTestRunner=playwright --no-interactive` + ); + + runCLI('generate @nx/workspace:convert-to-monorepo --no-interactive'); + + checkFilesExist( + `apps/${reactApp}/src/main.tsx`, + `apps/e2e/playwright.config.ts` + ); + + expect(() => runCLI(`build ${reactApp}`)).not.toThrow(); + expect(() => runCLI(`test ${reactApp}`)).not.toThrow(); + expect(() => runCLI(`lint ${reactApp}`)).not.toThrow(); + expect(() => runCLI(`lint e2e`)).not.toThrow(); + if (runE2ETests()) { + expect(() => runCLI(`e2e e2e`)).not.toThrow(); + } + }); +}); diff --git a/e2e/nx/src/workspace-infer-targets.test.ts b/e2e/nx/src/workspace-infer-targets.test.ts new file mode 100644 index 00000000000000..180a85aba88569 --- /dev/null +++ b/e2e/nx/src/workspace-infer-targets.test.ts @@ -0,0 +1,148 @@ +import { + cleanupProject, + newProject, + runCLI, + uniq, + updateJson, +} from '@nx/e2e-utils'; +import { join } from 'path'; + +let proj: string; + +describe('@nx/workspace:infer-targets', () => { + beforeEach(() => { + proj = newProject({ + packages: ['@nx/playwright', '@nx/remix', '@nx/eslint', '@nx/jest'], + }); + }); + + afterEach(() => cleanupProject()); + + it('should run or skip conversions depending on whether executors are present', async () => { + // default case, everything is generated with crystal, everything should be skipped + const remixApp = uniq('remix'); + runCLI( + `generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive` + ); + + const output = runCLI(`generate infer-targets --no-interactive --verbose`); + + expect(output).toContain('@nx/remix:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/playwright:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/eslint:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/jest:convert-to-inferred - Skipped'); + + // if we make sure there are executors to convert, conversions will run + updateJson('nx.json', (json) => { + json.plugins = []; + return json; + }); + + updateJson(join('apps', remixApp, 'project.json'), (json) => { + json.targets = { + build: { + executor: '@nx/remix:build', + }, + lint: { + executor: '@nx/eslint:lint', + }, + }; + return json; + }); + + const output2 = runCLI(`generate infer-targets --no-interactive --verbose`); + + expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); + expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); + }); + + it('should run or skip only specific conversions if --plugins is passed', async () => { + // default case, everything is generated with crystal, relevant plugins should be skipped + const remixApp = uniq('remix'); + runCLI( + `generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive` + ); + + const output = runCLI( + `generate infer-targets --plugins=@nx/eslint,@nx/jest --no-interactive` + ); + + expect(output).toContain('@nx/eslint:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/jest:convert-to-inferred - Skipped'); + + expect(output).not.toContain('@nx/remix'); + expect(output).not.toContain('@nx/playwright'); + + // if we make sure there are executors to convert, relevant conversions will run + updateJson('nx.json', (json) => { + json.plugins = []; + return json; + }); + + updateJson(join('apps', remixApp, 'project.json'), (json) => { + json.targets = { + build: { + executor: '@nx/remix:build', + }, + lint: { + executor: '@nx/eslint:lint', + }, + }; + return json; + }); + + const output2 = runCLI( + `generate infer-targets --plugins=@nx/remix,@nx/eslint --no-interactive` + ); + + expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); + expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); + + expect(output2).not.toContain('@nx/jest'); + expect(output2).not.toContain('@nx/playwright'); + }); + + it('should run only specific conversions for a specific project if --project is passed', async () => { + // even if we make sure there are executors for remix & remix-e2e, only remix conversions will run with --project option + const remixApp = uniq('remix'); + runCLI( + `generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive` + ); + + updateJson('nx.json', (json) => { + json.plugins = []; + return json; + }); + + updateJson(join('apps', remixApp, 'project.json'), (json) => { + json.targets = { + build: { + executor: '@nx/remix:build', + }, + lint: { + executor: '@nx/eslint:lint', + }, + }; + return json; + }); + + updateJson(join('apps', `${remixApp}-e2e`, 'project.json'), (json) => { + json.targets = { + e2e: { + executor: '@nx/playwright:playwright', + }, + }; + return json; + }); + + const output2 = runCLI( + `generate infer-targets --project ${remixApp} --no-interactive` + ); + + expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); + expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); + + expect(output2).toContain('@nx/jest:convert-to-inferred - Skipped'); + expect(output2).toContain('@nx/playwright:convert-to-inferred - Skipped'); + }); +}); diff --git a/e2e/nx/src/workspace-move-project.test.ts b/e2e/nx/src/workspace-move-project.test.ts new file mode 100644 index 00000000000000..544f1aeaefa5d2 --- /dev/null +++ b/e2e/nx/src/workspace-move-project.test.ts @@ -0,0 +1,520 @@ +import { + checkFilesExist, + cleanupProject, + exists, + readFile, + readJson, + runCLI, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { join } from 'path'; +import { setupWorkspaceTests } from './workspace-setup'; + +describe('move project', () => { + let proj: string; + + beforeAll(() => { + proj = setupWorkspaceTests(); + }); + + afterAll(() => cleanupProject()); + + /** + * Tries moving a library from ${lib}/data-access -> shared/${lib}/data-access + */ + it('should work for libraries', async () => { + const lib1 = uniq('mylib'); + const lib2 = uniq('mylib'); + const lib3 = uniq('mylib'); + runCLI( + `generate @nx/js:lib --name=${lib1}-data-access --directory=${lib1}/data-access --unitTestRunner=jest` + ); + + updateFile( + `${lib1}/data-access/src/lib/${lib1}-data-access.ts`, + `export function fromLibOne() { console.log('This is completely pointless'); }` + ); + + updateFile( + `${lib1}/data-access/src/index.ts`, + `export * from './lib/${lib1}-data-access.ts'` + ); + + /** + * Create a library which imports a class from lib1 + */ + + runCLI( + `generate @nx/js:lib --name=${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` + ); + + updateFile( + `${lib2}/ui/src/lib/${lib2}-ui.ts`, + `import { fromLibOne } from '@${proj}/${lib1}-data-access'; + + export const fromLibTwo = () => fromLibOne();` + ); + + /** + * Create a library which has an implicit dependency on lib1 + */ + + runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); + updateFile(join(lib3, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = [`${lib1}-data-access`]; + return JSON.stringify(data, null, 2); + }); + + /** + * Now try to move lib1 + */ + + const moveOutput = runCLI( + `generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access` + ); + + expect(moveOutput).toContain(`DELETE ${lib1}/data-access`); + expect(exists(`${lib1}/data-access`)).toBeFalsy(); + + const newPath = `shared/${lib1}/data-access`; + const newName = `shared-${lib1}-data-access`; + + const readmePath = `${newPath}/README.md`; + expect(moveOutput).toContain(`CREATE ${readmePath}`); + checkFilesExist(readmePath); + + const jestConfigPath = `${newPath}/jest.config.ts`; + expect(moveOutput).toContain(`CREATE ${jestConfigPath}`); + checkFilesExist(jestConfigPath); + const jestConfig = readFile(jestConfigPath); + expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); + expect(jestConfig).toContain(`preset: '../../../jest.preset.js'`); + expect(jestConfig).toContain(`'../../../coverage/${newPath}'`); + + const tsConfigPath = `${newPath}/tsconfig.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); + checkFilesExist(tsConfigPath); + + const tsConfigLibPath = `${newPath}/tsconfig.lib.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`); + checkFilesExist(tsConfigLibPath); + const tsConfigLib = readJson(tsConfigLibPath); + expect(tsConfigLib.compilerOptions.outDir).toEqual('../../../dist/out-tsc'); + + const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`); + checkFilesExist(tsConfigSpecPath); + const tsConfigSpec = readJson(tsConfigSpecPath); + expect(tsConfigSpec.compilerOptions.outDir).toEqual( + '../../../dist/out-tsc' + ); + + const indexPath = `${newPath}/src/index.ts`; + expect(moveOutput).toContain(`CREATE ${indexPath}`); + checkFilesExist(indexPath); + + const rootClassPath = `${newPath}/src/lib/${lib1}-data-access.ts`; + expect(moveOutput).toContain(`CREATE ${rootClassPath}`); + checkFilesExist(rootClassPath); + + let projects = runCLI('show projects').split('\n'); + expect(projects).not.toContain(`${lib1}-data-access`); + const newConfig = readJson(join(newPath, 'project.json')); + expect(newConfig).toMatchObject({ + tags: [], + }); + const lib3Config = readJson(join(lib3, 'project.json')); + expect(lib3Config.implicitDependencies).toEqual([ + `shared-${lib1}-data-access`, + ]); + + expect(moveOutput).toContain('UPDATE tsconfig.base.json'); + const rootTsConfig = readJson('tsconfig.base.json'); + expect( + rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] + ).toBeUndefined(); + expect( + rootTsConfig.compilerOptions.paths[`@${proj}/shared-${lib1}-data-access`] + ).toEqual([`shared/${lib1}/data-access/src/index.ts`]); + + projects = runCLI('show projects').split('\n'); + expect(projects).not.toContain(`${lib1}-data-access`); + const project = readJson(join(newPath, 'project.json')); + expect(project).toBeTruthy(); + expect(project.sourceRoot).toBe(`${newPath}/src`); + + /** + * Check that the import in lib2 has been updated + */ + const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; + const lib2File = readFile(lib2FilePath); + expect(lib2File).toContain( + `import { fromLibOne } from '@${proj}/shared-${lib1}-data-access';` + ); + }); + + it('should work for libs created with --importPath', async () => { + const importPath = '@wibble/fish'; + const lib1 = uniq('mylib'); + const lib2 = uniq('mylib'); + const lib3 = uniq('mylib'); + runCLI( + `generate @nx/js:lib --name=${lib1}-data-access --directory=${lib1}/data-access --importPath=${importPath} --unitTestRunner=jest` + ); + + updateFile( + `${lib1}/data-access/src/lib/${lib1}-data-access.ts`, + `export function fromLibOne() { console.log('This is completely pointless'); }` + ); + + updateFile( + `${lib1}/data-access/src/index.ts`, + `export * from './lib/${lib1}-data-access.ts'` + ); + + /** + * Create a library which imports a class from lib1 + */ + + runCLI( + `generate @nx/js:lib --name=${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` + ); + + updateFile( + `${lib2}/ui/src/lib/${lib2}-ui.ts`, + `import { fromLibOne } from '${importPath}'; + + export const fromLibTwo = () => fromLibOne();` + ); + + /** + * Create a library which has an implicit dependency on lib1 + */ + + runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); + updateFile(join(lib3, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = [`${lib1}-data-access`]; + return JSON.stringify(data, null, 2); + }); + + /** + * Now try to move lib1 + */ + + const moveOutput = runCLI( + `generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access` + ); + + expect(moveOutput).toContain(`DELETE ${lib1}/data-access`); + expect(exists(`${lib1}/data-access`)).toBeFalsy(); + + const newPath = `shared/${lib1}/data-access`; + const newName = `shared-${lib1}-data-access`; + + const readmePath = `${newPath}/README.md`; + expect(moveOutput).toContain(`CREATE ${readmePath}`); + checkFilesExist(readmePath); + + const jestConfigPath = `${newPath}/jest.config.ts`; + expect(moveOutput).toContain(`CREATE ${jestConfigPath}`); + checkFilesExist(jestConfigPath); + const jestConfig = readFile(jestConfigPath); + expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); + expect(jestConfig).toContain(`preset: '../../../jest.preset.js'`); + expect(jestConfig).toContain(`'../../../coverage/${newPath}'`); + + const tsConfigPath = `${newPath}/tsconfig.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); + checkFilesExist(tsConfigPath); + + const tsConfigLibPath = `${newPath}/tsconfig.lib.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`); + checkFilesExist(tsConfigLibPath); + const tsConfigLib = readJson(tsConfigLibPath); + expect(tsConfigLib.compilerOptions.outDir).toEqual('../../../dist/out-tsc'); + + const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`); + checkFilesExist(tsConfigSpecPath); + const tsConfigSpec = readJson(tsConfigSpecPath); + expect(tsConfigSpec.compilerOptions.outDir).toEqual( + '../../../dist/out-tsc' + ); + + const indexPath = `${newPath}/src/index.ts`; + expect(moveOutput).toContain(`CREATE ${indexPath}`); + checkFilesExist(indexPath); + + const rootClassPath = `${newPath}/src/lib/${lib1}-data-access.ts`; + expect(moveOutput).toContain(`CREATE ${rootClassPath}`); + checkFilesExist(rootClassPath); + + expect(moveOutput).toContain('UPDATE tsconfig.base.json'); + const rootTsConfig = readJson('tsconfig.base.json'); + expect( + rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] + ).toBeUndefined(); + expect( + rootTsConfig.compilerOptions.paths[`@${proj}/shared-${lib1}-data-access`] + ).toEqual([`shared/${lib1}/data-access/src/index.ts`]); + + const projects = runCLI('show projects').split('\n'); + expect(projects).not.toContain(`${lib1}-data-access`); + const project = readJson(join(newPath, 'project.json')); + expect(project).toBeTruthy(); + expect(project.sourceRoot).toBe(`${newPath}/src`); + expect(project.tags).toEqual([]); + const lib3Config = readJson(join(lib3, 'project.json')); + expect(lib3Config.implicitDependencies).toEqual([newName]); + + /** + * Check that the import in lib2 has been updated + */ + const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; + const lib2File = readFile(lib2FilePath); + expect(lib2File).toContain( + `import { fromLibOne } from '@${proj}/shared-${lib1}-data-access';` + ); + }); + + it('should work when moving a lib to a subfolder', async () => { + const lib1 = uniq('lib1'); + const lib2 = uniq('lib2'); + const lib3 = uniq('lib3'); + runCLI(`generate @nx/js:lib ${lib1} --unitTestRunner=jest`); + + updateFile( + `${lib1}/src/lib/${lib1}.ts`, + `export function fromLibOne() { console.log('This is completely pointless'); }` + ); + + updateFile(`${lib1}/src/index.ts`, `export * from './lib/${lib1}.ts'`); + + /** + * Create a library which imports a class from lib1 + */ + + runCLI( + `generate @nx/js:lib --name=${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` + ); + + updateFile( + `${lib2}/ui/src/lib/${lib2}-ui.ts`, + `import { fromLibOne } from '@${proj}/${lib1}'; + + export const fromLibTwo = () => fromLibOne();` + ); + + /** + * Create a library which has an implicit dependency on lib1 + */ + + runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); + updateFile(join(lib3, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = [lib1]; + return JSON.stringify(data, null, 2); + }); + + /** + * Now try to move lib1 + */ + + const moveOutput = runCLI( + `generate @nx/workspace:move --project ${lib1} ${lib1}/data-access --newProjectName=${lib1}-data-access` + ); + + expect(moveOutput).toContain(`DELETE ${lib1}/project.json`); + expect(exists(`${lib1}/project.json`)).toBeFalsy(); + + const newPath = `${lib1}/data-access`; + const newName = `${lib1}-data-access`; + + const readmePath = `${newPath}/README.md`; + expect(moveOutput).toContain(`CREATE ${readmePath}`); + checkFilesExist(readmePath); + + const jestConfigPath = `${newPath}/jest.config.ts`; + expect(moveOutput).toContain(`CREATE ${jestConfigPath}`); + checkFilesExist(jestConfigPath); + const jestConfig = readFile(jestConfigPath); + expect(jestConfig).toContain(`displayName: '${lib1}-data-access'`); + expect(jestConfig).toContain(`preset: '../../jest.preset.js'`); + expect(jestConfig).toContain(`'../../coverage/${newPath}'`); + + const tsConfigPath = `${newPath}/tsconfig.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); + checkFilesExist(tsConfigPath); + + const tsConfigLibPath = `${newPath}/tsconfig.lib.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`); + checkFilesExist(tsConfigLibPath); + const tsConfigLib = readJson(tsConfigLibPath); + expect(tsConfigLib.compilerOptions.outDir).toEqual('../../dist/out-tsc'); + + const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`; + expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`); + checkFilesExist(tsConfigSpecPath); + const tsConfigSpec = readJson(tsConfigSpecPath); + expect(tsConfigSpec.compilerOptions.outDir).toEqual('../../dist/out-tsc'); + + const indexPath = `${newPath}/src/index.ts`; + expect(moveOutput).toContain(`CREATE ${indexPath}`); + checkFilesExist(indexPath); + + const rootClassPath = `${newPath}/src/lib/${lib1}.ts`; + expect(moveOutput).toContain(`CREATE ${rootClassPath}`); + checkFilesExist(rootClassPath); + + let projects = runCLI('show projects').split('\n'); + expect(projects).not.toContain(lib1); + const newConfig = readJson(join(newPath, 'project.json')); + expect(newConfig).toMatchObject({ + tags: [], + }); + const lib3Config = readJson(join(lib3, 'project.json')); + expect(lib3Config.implicitDependencies).toEqual([`${lib1}-data-access`]); + + expect(moveOutput).toContain('UPDATE tsconfig.base.json'); + const rootTsConfig = readJson('tsconfig.base.json'); + expect( + rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}`] + ).toBeUndefined(); + expect( + rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] + ).toEqual([`${lib1}/data-access/src/index.ts`]); + + projects = runCLI('show projects').split('\n'); + expect(projects).not.toContain(lib1); + const project = readJson(join(newPath, 'project.json')); + expect(project).toBeTruthy(); + expect(project.sourceRoot).toBe(`${newPath}/src`); + + /** + * Check that the import in lib2 has been updated + */ + const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; + const lib2File = readFile(lib2FilePath); + expect(lib2File).toContain( + `import { fromLibOne } from '@${proj}/${lib1}-data-access';` + ); + }); + + it('should work for libraries when scope is unset', async () => { + const json = readJson('package.json'); + json.name = proj; + updateFile('package.json', JSON.stringify(json)); + + const lib1 = uniq('mylib'); + const lib2 = uniq('mylib'); + const lib3 = uniq('mylib'); + runCLI( + `generate @nx/js:lib --name=${lib1}-data-access --directory=${lib1}/data-access --unitTestRunner=jest` + ); + let rootTsConfig = readJson('tsconfig.base.json'); + expect( + rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] + ).toBeUndefined(); + expect( + rootTsConfig.compilerOptions.paths[`${lib1}-data-access`] + ).toBeDefined(); + + updateFile( + `${lib1}/data-access/src/lib/${lib1}-data-access.ts`, + `export function fromLibOne() { console.log('This is completely pointless'); }` + ); + + updateFile( + `${lib1}/data-access/src/index.ts`, + `export * from './lib/${lib1}-data-access.ts'` + ); + + /** + * Create a library which imports a class from lib1 + */ + + runCLI( + `generate @nx/js:lib --name${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` + ); + + updateFile( + `${lib2}/ui/src/lib/${lib2}-ui.ts`, + `import { fromLibOne } from '${lib1}-data-access'; + + export const fromLibTwo = () => fromLibOne();` + ); + + /** + * Create a library which has an implicit dependency on lib1 + */ + + runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); + updateFile(join(lib3, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = [`${lib1}-data-access`]; + return JSON.stringify(data, null, 2); + }); + + /** + * Now try to move lib1 + */ + + const moveOutput = runCLI( + `generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access` + ); + + expect(moveOutput).toContain(`DELETE ${lib1}/data-access`); + expect(exists(`${lib1}/data-access`)).toBeFalsy(); + + const newPath = `shared/${lib1}/data-access`; + const newName = `shared-${lib1}-data-access`; + + const readmePath = `${newPath}/README.md`; + expect(moveOutput).toContain(`CREATE ${readmePath}`); + checkFilesExist(readmePath); + + const indexPath = `${newPath}/src/index.ts`; + expect(moveOutput).toContain(`CREATE ${indexPath}`); + checkFilesExist(indexPath); + + const rootClassPath = `${newPath}/src/lib/${lib1}-data-access.ts`; + expect(moveOutput).toContain(`CREATE ${rootClassPath}`); + checkFilesExist(rootClassPath); + + const newConfig = readJson(join(newPath, 'project.json')); + expect(newConfig).toMatchObject({ + tags: [], + }); + const lib3Config = readJson(join(lib3, 'project.json')); + expect(lib3Config.implicitDependencies).toEqual([ + `shared-${lib1}-data-access`, + ]); + + expect(moveOutput).toContain('UPDATE tsconfig.base.json'); + rootTsConfig = readJson('tsconfig.base.json'); + expect( + rootTsConfig.compilerOptions.paths[`${lib1}-data-access`] + ).toBeUndefined(); + expect( + rootTsConfig.compilerOptions.paths[`shared-${lib1}-data-access`] + ).toEqual([`shared/${lib1}/data-access/src/index.ts`]); + + const projects = runCLI('show projects').split('\n'); + expect(projects).not.toContain(`${lib1}-data-access`); + const project = readJson(join(newPath, 'project.json')); + expect(project).toBeTruthy(); + expect(project.sourceRoot).toBe(`${newPath}/src`); + + /** + * Check that the import in lib2 has been updated + */ + const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; + const lib2File = readFile(lib2FilePath); + expect(lib2File).toContain( + `import { fromLibOne } from 'shared-${lib1}-data-access';` + ); + }); +}); diff --git a/e2e/nx/src/workspace-npm-package.test.ts b/e2e/nx/src/workspace-npm-package.test.ts new file mode 100644 index 00000000000000..f81419a7717717 --- /dev/null +++ b/e2e/nx/src/workspace-npm-package.test.ts @@ -0,0 +1,41 @@ +import { + cleanupProject, + getPackageManagerCommand, + getSelectedPackageManager, + runCLI, + runCommand, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { setupWorkspaceTests } from './workspace-setup'; + +describe('Workspace Tests - @nx/workspace:npm-package', () => { + let proj: string; + + beforeAll(() => { + proj = setupWorkspaceTests(); + }); + + afterAll(() => cleanupProject()); + + it('should create a minimal npm package', () => { + const npmPackage = uniq('npm-package'); + + runCLI(`generate @nx/workspace:npm-package ${npmPackage}`); + + updateFile('package.json', (content) => { + const json = JSON.parse(content); + json.workspaces = ['libs/*']; + return JSON.stringify(json); + }); + + const pmc = getPackageManagerCommand({ + packageManager: getSelectedPackageManager(), + }); + + runCommand(pmc.install); + + const result = runCLI(`test ${npmPackage}`); + expect(result).toContain('Hello World'); + }); +}); diff --git a/e2e/nx/src/workspace-remove-project.test.ts b/e2e/nx/src/workspace-remove-project.test.ts new file mode 100644 index 00000000000000..ec0c3f25c397de --- /dev/null +++ b/e2e/nx/src/workspace-remove-project.test.ts @@ -0,0 +1,81 @@ +import { + cleanupProject, + exists, + readJson, + runCLI, + tmpProjPath, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { join } from 'path'; +import { setupWorkspaceTests } from './workspace-setup'; + +describe('remove project', () => { + let proj: string; + + beforeAll(() => { + proj = setupWorkspaceTests(); + }); + + afterAll(() => cleanupProject()); + + /** + * Tries creating then deleting a lib + */ + it('should work', async () => { + const lib1 = uniq('myliba'); + const lib2 = uniq('mylibb'); + + runCLI( + `generate @nx/js:lib ${lib1} --unitTestRunner=jest --directory=libs/${lib1}` + ); + expect(exists(tmpProjPath(`libs/${lib1}`))).toBeTruthy(); + + /** + * Create a library which has an implicit dependency on lib1 + */ + + runCLI(`generate @nx/js:lib libs/${lib2} --unitTestRunner=jest`); + updateFile(join('libs', lib2, 'project.json'), (content) => { + const data = JSON.parse(content); + data.implicitDependencies = [lib1]; + return JSON.stringify(data, null, 2); + }); + + /** + * Try removing the project (should fail) + */ + + let error; + try { + console.log(runCLI(`generate @nx/workspace:remove --project ${lib1}`)); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + expect(error.stdout.toString()).toContain( + `${lib1} is still a dependency of the following projects` + ); + expect(error.stdout.toString()).toContain(lib2); + + /** + * Try force removing the project + */ + + const removeOutputForced = runCLI( + `generate @nx/workspace:remove --project ${lib1} --forceRemove` + ); + + expect(removeOutputForced).toContain(`DELETE libs/${lib1}`); + expect(exists(tmpProjPath(`libs/${lib1}`))).toBeFalsy(); + + expect(removeOutputForced).not.toContain(`UPDATE nx.json`); + const projects = runCLI('show projects').split('\n'); + expect(projects).not.toContain(lib1); + const lib2Config = readJson(join('libs', lib2, 'project.json')); + expect(lib2Config.implicitDependencies).toEqual([]); + + expect(projects[`${lib1}`]).toBeUndefined(); + }); +}); diff --git a/e2e/nx/src/workspace-setup.ts b/e2e/nx/src/workspace-setup.ts new file mode 100644 index 00000000000000..ee59fa552b3fb4 --- /dev/null +++ b/e2e/nx/src/workspace-setup.ts @@ -0,0 +1,5 @@ +import { newProject } from '@nx/e2e-utils'; + +export function setupWorkspaceTests() { + return newProject(); +} diff --git a/e2e/nx/src/workspace.test.ts b/e2e/nx/src/workspace.test.ts deleted file mode 100644 index 235bdee00449b2..00000000000000 --- a/e2e/nx/src/workspace.test.ts +++ /dev/null @@ -1,789 +0,0 @@ -import { - checkFilesExist, - cleanupProject, - exists, - getPackageManagerCommand, - getSelectedPackageManager, - newProject, - readFile, - readJson, - runCLI, - runCommand, - runE2ETests, - tmpProjPath, - uniq, - updateFile, - updateJson, -} from '@nx/e2e-utils'; -import { join } from 'path'; - -let proj: string; - -describe('@nx/workspace:infer-targets', () => { - beforeEach(() => { - proj = newProject({ - packages: ['@nx/playwright', '@nx/remix', '@nx/eslint', '@nx/jest'], - }); - }); - - afterEach(() => cleanupProject()); - - it('should run or skip conversions depending on whether executors are present', async () => { - // default case, everything is generated with crystal, everything should be skipped - const remixApp = uniq('remix'); - runCLI( - `generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive` - ); - - const output = runCLI(`generate infer-targets --no-interactive --verbose`); - - expect(output).toContain('@nx/remix:convert-to-inferred - Skipped'); - expect(output).toContain('@nx/playwright:convert-to-inferred - Skipped'); - expect(output).toContain('@nx/eslint:convert-to-inferred - Skipped'); - expect(output).toContain('@nx/jest:convert-to-inferred - Skipped'); - - // if we make sure there are executors to convert, conversions will run - updateJson('nx.json', (json) => { - json.plugins = []; - return json; - }); - - updateJson(join('apps', remixApp, 'project.json'), (json) => { - json.targets = { - build: { - executor: '@nx/remix:build', - }, - lint: { - executor: '@nx/eslint:lint', - }, - }; - return json; - }); - - const output2 = runCLI(`generate infer-targets --no-interactive --verbose`); - - expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); - expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); - }); - - it('should run or skip only specific conversions if --plugins is passed', async () => { - // default case, everything is generated with crystal, relevant plugins should be skipped - const remixApp = uniq('remix'); - runCLI( - `generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive` - ); - - const output = runCLI( - `generate infer-targets --plugins=@nx/eslint,@nx/jest --no-interactive` - ); - - expect(output).toContain('@nx/eslint:convert-to-inferred - Skipped'); - expect(output).toContain('@nx/jest:convert-to-inferred - Skipped'); - - expect(output).not.toContain('@nx/remix'); - expect(output).not.toContain('@nx/playwright'); - - // if we make sure there are executors to convert, relevant conversions will run - updateJson('nx.json', (json) => { - json.plugins = []; - return json; - }); - - updateJson(join('apps', remixApp, 'project.json'), (json) => { - json.targets = { - build: { - executor: '@nx/remix:build', - }, - lint: { - executor: '@nx/eslint:lint', - }, - }; - return json; - }); - - const output2 = runCLI( - `generate infer-targets --plugins=@nx/remix,@nx/eslint --no-interactive` - ); - - expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); - expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); - - expect(output2).not.toContain('@nx/jest'); - expect(output2).not.toContain('@nx/playwright'); - }); - - it('should run only specific conversions for a specific project if --project is passed', async () => { - // even if we make sure there are executors for remix & remix-e2e, only remix conversions will run with --project option - const remixApp = uniq('remix'); - runCLI( - `generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive` - ); - - updateJson('nx.json', (json) => { - json.plugins = []; - return json; - }); - - updateJson(join('apps', remixApp, 'project.json'), (json) => { - json.targets = { - build: { - executor: '@nx/remix:build', - }, - lint: { - executor: '@nx/eslint:lint', - }, - }; - return json; - }); - - updateJson(join('apps', `${remixApp}-e2e`, 'project.json'), (json) => { - json.targets = { - e2e: { - executor: '@nx/playwright:playwright', - }, - }; - return json; - }); - - const output2 = runCLI( - `generate infer-targets --project ${remixApp} --no-interactive` - ); - - expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); - expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); - - expect(output2).toContain('@nx/jest:convert-to-inferred - Skipped'); - expect(output2).toContain('@nx/playwright:convert-to-inferred - Skipped'); - }); -}); - -describe('@nx/workspace:convert-to-monorepo', () => { - beforeEach(() => { - proj = newProject({ packages: ['@nx/react', '@nx/js'] }); - }); - - afterEach(() => cleanupProject()); - - it('should be convert a standalone vite and playwright react project to a monorepo', async () => { - const reactApp = uniq('reactapp'); - runCLI( - `generate @nx/react:app --name=${reactApp} --directory="." --rootProject=true --linter eslint --bundler=vite --unitTestRunner vitest --e2eTestRunner=playwright --no-interactive` - ); - - runCLI('generate @nx/workspace:convert-to-monorepo --no-interactive'); - - checkFilesExist( - `apps/${reactApp}/src/main.tsx`, - `apps/e2e/playwright.config.ts` - ); - - expect(() => runCLI(`build ${reactApp}`)).not.toThrow(); - expect(() => runCLI(`test ${reactApp}`)).not.toThrow(); - expect(() => runCLI(`lint ${reactApp}`)).not.toThrow(); - expect(() => runCLI(`lint e2e`)).not.toThrow(); - if (runE2ETests()) { - expect(() => runCLI(`e2e e2e`)).not.toThrow(); - } - }); -}); - -describe('Workspace Tests', () => { - beforeAll(() => { - proj = newProject(); - }); - - afterAll(() => cleanupProject()); - - describe('@nx/workspace:npm-package', () => { - it('should create a minimal npm package', () => { - const npmPackage = uniq('npm-package'); - - runCLI(`generate @nx/workspace:npm-package ${npmPackage}`); - - updateFile('package.json', (content) => { - const json = JSON.parse(content); - json.workspaces = ['libs/*']; - return JSON.stringify(json); - }); - - const pmc = getPackageManagerCommand({ - packageManager: getSelectedPackageManager(), - }); - - runCommand(pmc.install); - - const result = runCLI(`test ${npmPackage}`); - expect(result).toContain('Hello World'); - }); - }); - - describe('move project', () => { - /** - * Tries moving a library from ${lib}/data-access -> shared/${lib}/data-access - */ - it('should work for libraries', async () => { - const lib1 = uniq('mylib'); - const lib2 = uniq('mylib'); - const lib3 = uniq('mylib'); - runCLI( - `generate @nx/js:lib --name=${lib1}-data-access --directory=${lib1}/data-access --unitTestRunner=jest` - ); - - updateFile( - `${lib1}/data-access/src/lib/${lib1}-data-access.ts`, - `export function fromLibOne() { console.log('This is completely pointless'); }` - ); - - updateFile( - `${lib1}/data-access/src/index.ts`, - `export * from './lib/${lib1}-data-access.ts'` - ); - - /** - * Create a library which imports a class from lib1 - */ - - runCLI( - `generate @nx/js:lib --name=${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` - ); - - updateFile( - `${lib2}/ui/src/lib/${lib2}-ui.ts`, - `import { fromLibOne } from '@${proj}/${lib1}-data-access'; - - export const fromLibTwo = () => fromLibOne();` - ); - - /** - * Create a library which has an implicit dependency on lib1 - */ - - runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); - updateFile(join(lib3, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = [`${lib1}-data-access`]; - return JSON.stringify(data, null, 2); - }); - - /** - * Now try to move lib1 - */ - - const moveOutput = runCLI( - `generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access` - ); - - expect(moveOutput).toContain(`DELETE ${lib1}/data-access`); - expect(exists(`${lib1}/data-access`)).toBeFalsy(); - - const newPath = `shared/${lib1}/data-access`; - const newName = `shared-${lib1}-data-access`; - - const readmePath = `${newPath}/README.md`; - expect(moveOutput).toContain(`CREATE ${readmePath}`); - checkFilesExist(readmePath); - - const jestConfigPath = `${newPath}/jest.config.ts`; - expect(moveOutput).toContain(`CREATE ${jestConfigPath}`); - checkFilesExist(jestConfigPath); - const jestConfig = readFile(jestConfigPath); - expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); - expect(jestConfig).toContain(`preset: '../../../jest.preset.js'`); - expect(jestConfig).toContain(`'../../../coverage/${newPath}'`); - - const tsConfigPath = `${newPath}/tsconfig.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); - checkFilesExist(tsConfigPath); - - const tsConfigLibPath = `${newPath}/tsconfig.lib.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`); - checkFilesExist(tsConfigLibPath); - const tsConfigLib = readJson(tsConfigLibPath); - expect(tsConfigLib.compilerOptions.outDir).toEqual( - '../../../dist/out-tsc' - ); - - const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`); - checkFilesExist(tsConfigSpecPath); - const tsConfigSpec = readJson(tsConfigSpecPath); - expect(tsConfigSpec.compilerOptions.outDir).toEqual( - '../../../dist/out-tsc' - ); - - const indexPath = `${newPath}/src/index.ts`; - expect(moveOutput).toContain(`CREATE ${indexPath}`); - checkFilesExist(indexPath); - - const rootClassPath = `${newPath}/src/lib/${lib1}-data-access.ts`; - expect(moveOutput).toContain(`CREATE ${rootClassPath}`); - checkFilesExist(rootClassPath); - - let projects = runCLI('show projects').split('\n'); - expect(projects).not.toContain(`${lib1}-data-access`); - const newConfig = readJson(join(newPath, 'project.json')); - expect(newConfig).toMatchObject({ - tags: [], - }); - const lib3Config = readJson(join(lib3, 'project.json')); - expect(lib3Config.implicitDependencies).toEqual([ - `shared-${lib1}-data-access`, - ]); - - expect(moveOutput).toContain('UPDATE tsconfig.base.json'); - const rootTsConfig = readJson('tsconfig.base.json'); - expect( - rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] - ).toBeUndefined(); - expect( - rootTsConfig.compilerOptions.paths[ - `@${proj}/shared-${lib1}-data-access` - ] - ).toEqual([`shared/${lib1}/data-access/src/index.ts`]); - - projects = runCLI('show projects').split('\n'); - expect(projects).not.toContain(`${lib1}-data-access`); - const project = readJson(join(newPath, 'project.json')); - expect(project).toBeTruthy(); - expect(project.sourceRoot).toBe(`${newPath}/src`); - - /** - * Check that the import in lib2 has been updated - */ - const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; - const lib2File = readFile(lib2FilePath); - expect(lib2File).toContain( - `import { fromLibOne } from '@${proj}/shared-${lib1}-data-access';` - ); - }); - - it('should work for libs created with --importPath', async () => { - const importPath = '@wibble/fish'; - const lib1 = uniq('mylib'); - const lib2 = uniq('mylib'); - const lib3 = uniq('mylib'); - runCLI( - `generate @nx/js:lib --name=${lib1}-data-access --directory=${lib1}/data-access --importPath=${importPath} --unitTestRunner=jest` - ); - - updateFile( - `${lib1}/data-access/src/lib/${lib1}-data-access.ts`, - `export function fromLibOne() { console.log('This is completely pointless'); }` - ); - - updateFile( - `${lib1}/data-access/src/index.ts`, - `export * from './lib/${lib1}-data-access.ts'` - ); - - /** - * Create a library which imports a class from lib1 - */ - - runCLI( - `generate @nx/js:lib --name=${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` - ); - - updateFile( - `${lib2}/ui/src/lib/${lib2}-ui.ts`, - `import { fromLibOne } from '${importPath}'; - - export const fromLibTwo = () => fromLibOne();` - ); - - /** - * Create a library which has an implicit dependency on lib1 - */ - - runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); - updateFile(join(lib3, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = [`${lib1}-data-access`]; - return JSON.stringify(data, null, 2); - }); - - /** - * Now try to move lib1 - */ - - const moveOutput = runCLI( - `generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access` - ); - - expect(moveOutput).toContain(`DELETE ${lib1}/data-access`); - expect(exists(`${lib1}/data-access`)).toBeFalsy(); - - const newPath = `shared/${lib1}/data-access`; - const newName = `shared-${lib1}-data-access`; - - const readmePath = `${newPath}/README.md`; - expect(moveOutput).toContain(`CREATE ${readmePath}`); - checkFilesExist(readmePath); - - const jestConfigPath = `${newPath}/jest.config.ts`; - expect(moveOutput).toContain(`CREATE ${jestConfigPath}`); - checkFilesExist(jestConfigPath); - const jestConfig = readFile(jestConfigPath); - expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); - expect(jestConfig).toContain(`preset: '../../../jest.preset.js'`); - expect(jestConfig).toContain(`'../../../coverage/${newPath}'`); - - const tsConfigPath = `${newPath}/tsconfig.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); - checkFilesExist(tsConfigPath); - - const tsConfigLibPath = `${newPath}/tsconfig.lib.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`); - checkFilesExist(tsConfigLibPath); - const tsConfigLib = readJson(tsConfigLibPath); - expect(tsConfigLib.compilerOptions.outDir).toEqual( - '../../../dist/out-tsc' - ); - - const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`); - checkFilesExist(tsConfigSpecPath); - const tsConfigSpec = readJson(tsConfigSpecPath); - expect(tsConfigSpec.compilerOptions.outDir).toEqual( - '../../../dist/out-tsc' - ); - - const indexPath = `${newPath}/src/index.ts`; - expect(moveOutput).toContain(`CREATE ${indexPath}`); - checkFilesExist(indexPath); - - const rootClassPath = `${newPath}/src/lib/${lib1}-data-access.ts`; - expect(moveOutput).toContain(`CREATE ${rootClassPath}`); - checkFilesExist(rootClassPath); - - expect(moveOutput).toContain('UPDATE tsconfig.base.json'); - const rootTsConfig = readJson('tsconfig.base.json'); - expect( - rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] - ).toBeUndefined(); - expect( - rootTsConfig.compilerOptions.paths[ - `@${proj}/shared-${lib1}-data-access` - ] - ).toEqual([`shared/${lib1}/data-access/src/index.ts`]); - - const projects = runCLI('show projects').split('\n'); - expect(projects).not.toContain(`${lib1}-data-access`); - const project = readJson(join(newPath, 'project.json')); - expect(project).toBeTruthy(); - expect(project.sourceRoot).toBe(`${newPath}/src`); - expect(project.tags).toEqual([]); - const lib3Config = readJson(join(lib3, 'project.json')); - expect(lib3Config.implicitDependencies).toEqual([newName]); - - /** - * Check that the import in lib2 has been updated - */ - const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; - const lib2File = readFile(lib2FilePath); - expect(lib2File).toContain( - `import { fromLibOne } from '@${proj}/shared-${lib1}-data-access';` - ); - }); - - it('should work when moving a lib to a subfolder', async () => { - const lib1 = uniq('lib1'); - const lib2 = uniq('lib2'); - const lib3 = uniq('lib3'); - runCLI(`generate @nx/js:lib ${lib1} --unitTestRunner=jest`); - - updateFile( - `${lib1}/src/lib/${lib1}.ts`, - `export function fromLibOne() { console.log('This is completely pointless'); }` - ); - - updateFile(`${lib1}/src/index.ts`, `export * from './lib/${lib1}.ts'`); - - /** - * Create a library which imports a class from lib1 - */ - - runCLI( - `generate @nx/js:lib --name=${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` - ); - - updateFile( - `${lib2}/ui/src/lib/${lib2}-ui.ts`, - `import { fromLibOne } from '@${proj}/${lib1}'; - - export const fromLibTwo = () => fromLibOne();` - ); - - /** - * Create a library which has an implicit dependency on lib1 - */ - - runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); - updateFile(join(lib3, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = [lib1]; - return JSON.stringify(data, null, 2); - }); - - /** - * Now try to move lib1 - */ - - const moveOutput = runCLI( - `generate @nx/workspace:move --project ${lib1} ${lib1}/data-access --newProjectName=${lib1}-data-access` - ); - - expect(moveOutput).toContain(`DELETE ${lib1}/project.json`); - expect(exists(`${lib1}/project.json`)).toBeFalsy(); - - const newPath = `${lib1}/data-access`; - const newName = `${lib1}-data-access`; - - const readmePath = `${newPath}/README.md`; - expect(moveOutput).toContain(`CREATE ${readmePath}`); - checkFilesExist(readmePath); - - const jestConfigPath = `${newPath}/jest.config.ts`; - expect(moveOutput).toContain(`CREATE ${jestConfigPath}`); - checkFilesExist(jestConfigPath); - const jestConfig = readFile(jestConfigPath); - expect(jestConfig).toContain(`displayName: '${lib1}-data-access'`); - expect(jestConfig).toContain(`preset: '../../jest.preset.js'`); - expect(jestConfig).toContain(`'../../coverage/${newPath}'`); - - const tsConfigPath = `${newPath}/tsconfig.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); - checkFilesExist(tsConfigPath); - - const tsConfigLibPath = `${newPath}/tsconfig.lib.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`); - checkFilesExist(tsConfigLibPath); - const tsConfigLib = readJson(tsConfigLibPath); - expect(tsConfigLib.compilerOptions.outDir).toEqual('../../dist/out-tsc'); - - const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`; - expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`); - checkFilesExist(tsConfigSpecPath); - const tsConfigSpec = readJson(tsConfigSpecPath); - expect(tsConfigSpec.compilerOptions.outDir).toEqual('../../dist/out-tsc'); - - const indexPath = `${newPath}/src/index.ts`; - expect(moveOutput).toContain(`CREATE ${indexPath}`); - checkFilesExist(indexPath); - - const rootClassPath = `${newPath}/src/lib/${lib1}.ts`; - expect(moveOutput).toContain(`CREATE ${rootClassPath}`); - checkFilesExist(rootClassPath); - - let projects = runCLI('show projects').split('\n'); - expect(projects).not.toContain(lib1); - const newConfig = readJson(join(newPath, 'project.json')); - expect(newConfig).toMatchObject({ - tags: [], - }); - const lib3Config = readJson(join(lib3, 'project.json')); - expect(lib3Config.implicitDependencies).toEqual([`${lib1}-data-access`]); - - expect(moveOutput).toContain('UPDATE tsconfig.base.json'); - const rootTsConfig = readJson('tsconfig.base.json'); - expect( - rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}`] - ).toBeUndefined(); - expect( - rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] - ).toEqual([`${lib1}/data-access/src/index.ts`]); - - projects = runCLI('show projects').split('\n'); - expect(projects).not.toContain(lib1); - const project = readJson(join(newPath, 'project.json')); - expect(project).toBeTruthy(); - expect(project.sourceRoot).toBe(`${newPath}/src`); - - /** - * Check that the import in lib2 has been updated - */ - const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; - const lib2File = readFile(lib2FilePath); - expect(lib2File).toContain( - `import { fromLibOne } from '@${proj}/${lib1}-data-access';` - ); - }); - - it('should work for libraries when scope is unset', async () => { - const json = readJson('package.json'); - json.name = proj; - updateFile('package.json', JSON.stringify(json)); - - const lib1 = uniq('mylib'); - const lib2 = uniq('mylib'); - const lib3 = uniq('mylib'); - runCLI( - `generate @nx/js:lib --name=${lib1}-data-access --directory=${lib1}/data-access --unitTestRunner=jest` - ); - let rootTsConfig = readJson('tsconfig.base.json'); - expect( - rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`] - ).toBeUndefined(); - expect( - rootTsConfig.compilerOptions.paths[`${lib1}-data-access`] - ).toBeDefined(); - - updateFile( - `${lib1}/data-access/src/lib/${lib1}-data-access.ts`, - `export function fromLibOne() { console.log('This is completely pointless'); }` - ); - - updateFile( - `${lib1}/data-access/src/index.ts`, - `export * from './lib/${lib1}-data-access.ts'` - ); - - /** - * Create a library which imports a class from lib1 - */ - - runCLI( - `generate @nx/js:lib --name${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest` - ); - - updateFile( - `${lib2}/ui/src/lib/${lib2}-ui.ts`, - `import { fromLibOne } from '${lib1}-data-access'; - - export const fromLibTwo = () => fromLibOne();` - ); - - /** - * Create a library which has an implicit dependency on lib1 - */ - - runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`); - updateFile(join(lib3, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = [`${lib1}-data-access`]; - return JSON.stringify(data, null, 2); - }); - - /** - * Now try to move lib1 - */ - - const moveOutput = runCLI( - `generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access` - ); - - expect(moveOutput).toContain(`DELETE ${lib1}/data-access`); - expect(exists(`${lib1}/data-access`)).toBeFalsy(); - - const newPath = `shared/${lib1}/data-access`; - const newName = `shared-${lib1}-data-access`; - - const readmePath = `${newPath}/README.md`; - expect(moveOutput).toContain(`CREATE ${readmePath}`); - checkFilesExist(readmePath); - - const indexPath = `${newPath}/src/index.ts`; - expect(moveOutput).toContain(`CREATE ${indexPath}`); - checkFilesExist(indexPath); - - const rootClassPath = `${newPath}/src/lib/${lib1}-data-access.ts`; - expect(moveOutput).toContain(`CREATE ${rootClassPath}`); - checkFilesExist(rootClassPath); - - const newConfig = readJson(join(newPath, 'project.json')); - expect(newConfig).toMatchObject({ - tags: [], - }); - const lib3Config = readJson(join(lib3, 'project.json')); - expect(lib3Config.implicitDependencies).toEqual([ - `shared-${lib1}-data-access`, - ]); - - expect(moveOutput).toContain('UPDATE tsconfig.base.json'); - rootTsConfig = readJson('tsconfig.base.json'); - expect( - rootTsConfig.compilerOptions.paths[`${lib1}-data-access`] - ).toBeUndefined(); - expect( - rootTsConfig.compilerOptions.paths[`shared-${lib1}-data-access`] - ).toEqual([`shared/${lib1}/data-access/src/index.ts`]); - - const projects = runCLI('show projects').split('\n'); - expect(projects).not.toContain(`${lib1}-data-access`); - const project = readJson(join(newPath, 'project.json')); - expect(project).toBeTruthy(); - expect(project.sourceRoot).toBe(`${newPath}/src`); - - /** - * Check that the import in lib2 has been updated - */ - const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`; - const lib2File = readFile(lib2FilePath); - expect(lib2File).toContain( - `import { fromLibOne } from 'shared-${lib1}-data-access';` - ); - }); - }); - - describe('remove project', () => { - /** - * Tries creating then deleting a lib - */ - it('should work', async () => { - const lib1 = uniq('myliba'); - const lib2 = uniq('mylibb'); - - runCLI( - `generate @nx/js:lib ${lib1} --unitTestRunner=jest --directory=libs/${lib1}` - ); - expect(exists(tmpProjPath(`libs/${lib1}`))).toBeTruthy(); - - /** - * Create a library which has an implicit dependency on lib1 - */ - - runCLI(`generate @nx/js:lib libs/${lib2} --unitTestRunner=jest`); - updateFile(join('libs', lib2, 'project.json'), (content) => { - const data = JSON.parse(content); - data.implicitDependencies = [lib1]; - return JSON.stringify(data, null, 2); - }); - - /** - * Try removing the project (should fail) - */ - - let error; - try { - console.log(runCLI(`generate @nx/workspace:remove --project ${lib1}`)); - } catch (e) { - error = e; - } - - expect(error).toBeDefined(); - expect(error.stdout.toString()).toContain( - `${lib1} is still a dependency of the following projects` - ); - expect(error.stdout.toString()).toContain(lib2); - - /** - * Try force removing the project - */ - - const removeOutputForced = runCLI( - `generate @nx/workspace:remove --project ${lib1} --forceRemove` - ); - - expect(removeOutputForced).toContain(`DELETE libs/${lib1}`); - expect(exists(tmpProjPath(`libs/${lib1}`))).toBeFalsy(); - - expect(removeOutputForced).not.toContain(`UPDATE nx.json`); - const projects = runCLI('show projects').split('\n'); - expect(projects).not.toContain(lib1); - const lib2Config = readJson(join('libs', lib2, 'project.json')); - expect(lib2Config.implicitDependencies).toEqual([]); - - expect(projects[`${lib1}`]).toBeUndefined(); - }); - }); -}); diff --git a/e2e/react/src/cypress-component-tests-buildable.test.ts b/e2e/react/src/cypress-component-tests-buildable.test.ts new file mode 100644 index 00000000000000..524a01a080bf46 --- /dev/null +++ b/e2e/react/src/cypress-component-tests-buildable.test.ts @@ -0,0 +1,225 @@ +import { + checkFilesExist, + cleanupProject, + createFile, + ensureCypressInstallation, + killPort, + newProject, + runCLI, + runE2ETests, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { join } from 'path'; + +describe('React Cypress Component Tests - buildable lib', () => { + let projectName; + let appName; + const usedInAppLibName = uniq('cy-react-lib'); + const buildableLibName = uniq('cy-react-buildable-lib'); + + beforeAll(async () => { + process.env.NX_ADD_PLUGINS = 'false'; + const appNameTemp = uniq('cy-react-app'); + appName = appNameTemp; + + projectName = newProject({ + name: uniq('cy-react'), + packages: ['@nx/react'], + }); + ensureCypressInstallation(); + + runCLI( + `generate @nx/react:app apps/${appName} --bundler=webpack --no-interactive` + ); + + updateJson('nx.json', (json) => ({ + ...json, + generators: { + ...json.generators, + '@nx/react': { + library: { + unitTestRunner: 'jest', + }, + }, + }, + })); + + runCLI( + `generate @nx/react:component apps/${appName}/src/app/fancy-cmp/fancy-cmp --no-interactive` + ); + runCLI( + `generate @nx/react:lib libs/${usedInAppLibName} --no-interactive --unitTestRunner=jest` + ); + runCLI( + `generate @nx/react:component libs/${usedInAppLibName}/src/lib/btn/btn --export --no-interactive` + ); + // makes sure custom webpack is loading + createFile( + `apps/${appName}/src/assets/demo.svg`, + ` + + + + nrwl + +` + ); + updateFile( + `libs/${usedInAppLibName}/src/lib/btn/btn.tsx`, + ` +import styles from './btn.module.css'; + +/* eslint-disable-next-line */ +export interface BtnProps { + text: string +} + +export function Btn(props: BtnProps) { + return ( +
+

Welcome to Btn!

+ +
+ ); +} + +export default Btn; +` + ); + + updateFile( + `apps/${appName}/src/app/app.tsx`, + ` +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import styles from './app.module.css'; +import logo from '../assets/demo.svg'; +import { Btn } from '@${projectName}/${usedInAppLibName}'; + +export function App() { + return ( + <> + + logo + + ); +} + +export default App;` + ); + + runCLI( + `generate @nx/react:lib libs/${buildableLibName} --buildable --no-interactive --unitTestRunner=jest` + ); + runCLI( + `generate @nx/react:component libs/${buildableLibName}/src/lib/input/input --export --no-interactive` + ); + + checkFilesExist(`libs/${buildableLibName}/src/lib/input/input.tsx`); + updateFile( + `libs/${buildableLibName}/src/lib/input/input.tsx`, + ` + import styles from './input.module.css'; + +/* eslint-disable-next-line */ +export interface InputProps { + readOnly: boolean +} + +export function Input(props: InputProps) { + return ( + + ); +} + +export default Input; +` + ); + createFile('libs/assets/data.json', JSON.stringify({ data: 'data' })); + updateJson(join('apps', appName, 'project.json'), (config) => { + config.targets['build'].options.assets.push({ + glob: '**/*', + input: 'libs/assets', + output: 'assets', + }); + return config; + }); + }); + + afterAll(() => { + cleanupProject(); + delete process.env.NX_ADD_PLUGINS; + }); + + it('should test buildable lib not being used in app', async () => { + createFile( + `libs/${buildableLibName}/src/lib/input/input.cy.tsx`, + ` +import * as React from 'react' +import Input from './input' + + +describe(Input.name, () => { + it('renders', () => { + cy.mount() + cy.get('label').should('have.css', 'color', 'rgb(0, 0, 0)'); + }) + it('should be read only', () => { + cy.mount() + cy.get('input').should('have.attr', 'readonly'); + }) +}); +` + ); + + runCLI( + `generate @nx/react:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build` + ); + + if (runE2ETests()) { + expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain( + 'All specs passed!' + ); + // Kill the dev server port to prevent EADDRINUSE errors + await killPort(8080); + } + + // add tailwind + runCLI(`generate @nx/react:setup-tailwind --project=${buildableLibName}`); + updateFile( + `libs/${buildableLibName}/src/styles.css`, + ` +@tailwind components; +@tailwind base; +@tailwind utilities; +` + ); + updateFile( + `libs/${buildableLibName}/src/lib/input/input.cy.tsx`, + (content) => { + // text-green-500 should now apply + return content.replace('rgb(0, 0, 0)', 'rgb(34, 197, 94)'); + } + ); + updateFile( + `libs/${buildableLibName}/src/lib/input/input.tsx`, + (content) => { + return `import '../../styles.css'; +${content}`; + } + ); + + if (runE2ETests()) { + expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain( + 'All specs passed!' + ); + // Kill the dev server port to clean up + await killPort(8080); + } + }, 300_000); +}); diff --git a/e2e/react/src/cypress-component-tests.test.ts b/e2e/react/src/cypress-component-tests.test.ts index c669cc0576d175..72191e8f54879b 100644 --- a/e2e/react/src/cypress-component-tests.test.ts +++ b/e2e/react/src/cypress-component-tests.test.ts @@ -192,69 +192,6 @@ export default Input; } }, 300_000); - it('should test buildable lib not being used in app', () => { - createFile( - `libs/${buildableLibName}/src/lib/input/input.cy.tsx`, - ` -import * as React from 'react' -import Input from './input' - - -describe(Input.name, () => { - it('renders', () => { - cy.mount() - cy.get('label').should('have.css', 'color', 'rgb(0, 0, 0)'); - }) - it('should be read only', () => { - cy.mount() - cy.get('input').should('have.attr', 'readonly'); - }) -}); -` - ); - - runCLI( - `generate @nx/react:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build` - ); - - if (runE2ETests()) { - expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain( - 'All specs passed!' - ); - } - - // add tailwind - runCLI(`generate @nx/react:setup-tailwind --project=${buildableLibName}`); - updateFile( - `libs/${buildableLibName}/src/styles.css`, - ` -@tailwind components; -@tailwind base; -@tailwind utilities; -` - ); - updateFile( - `libs/${buildableLibName}/src/lib/input/input.cy.tsx`, - (content) => { - // text-green-500 should now apply - return content.replace('rgb(0, 0, 0)', 'rgb(34, 197, 94)'); - } - ); - updateFile( - `libs/${buildableLibName}/src/lib/input/input.tsx`, - (content) => { - return `import '../../styles.css'; -${content}`; - } - ); - - if (runE2ETests()) { - expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain( - 'All specs passed!' - ); - } - }, 300_000); - it('should work with async webpack config', async () => { // TODO: (caleb) for whatever reason the MF webpack config + CT is running, but cypress is not starting up? // are they overriding some option on top of each other causing cypress to not see it's running? diff --git a/e2e/react/src/module-federation/independent-deployability-rspack-promise-based.test.ts b/e2e/react/src/module-federation/independent-deployability-rspack-promise-based.test.ts new file mode 100644 index 00000000000000..0c38d1c0867924 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-rspack-promise-based.test.ts @@ -0,0 +1,140 @@ +import { + cleanupProject, + getAvailablePort, + killProcessAndPorts, + runCLI, + runCommandUntil, + runE2ETests, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort } from './utils'; +import { setupIndependentDeployabilityTests } from './independent-deployability-rspack-setup'; + +describe('Independent Deployability - promise based remotes', () => { + let proj: string; + + beforeAll(() => { + proj = setupIndependentDeployabilityTests(); + }); + + afterAll(() => { + cleanupProject(); + }); + + it('should support promised based remotes', async () => { + const remote = uniq('remote'); + const host = uniq('host'); + const shellPort = await getAvailablePort(); + + runCLI( + `generate @nx/react:host ${host} --remotes=${remote} --devServerPort=${shellPort} --bundler=rspack --e2eTestRunner=cypress --no-interactive --typescriptConfiguration=false --skipFormat` + ); + + const remotePort = readPort(remote); + // Update remote to be loaded via script + updateFile( + `${remote}/module-federation.config.js`, + stripIndents` + module.exports = { + name: '${remote}', + library: { type: 'var', name: '${remote}' }, + exposes: { + './Module': './src/remote-entry.ts', + }, + }; + ` + ); + + updateFile( + `${remote}/rspack.config.prod.js`, + `module.exports = require('./rspack.config');` + ); + + // Update host to use promise based remote + updateFile( + `${host}/module-federation.config.js`, + `module.exports = { + name: '${host}', + library: { type: 'var', name: '${host}' }, + remotes: [ + [ + '${remote}', + \`promise new Promise(resolve => { + const remoteUrl = 'http://localhost:${remotePort}/remoteEntry.js'; + const script = document.createElement('script'); + script.src = remoteUrl; + script.onload = () => { + const proxy = { + get: (request) => window.${remote}.get(request), + init: (arg) => { + try { + window.${remote}.init(arg); + } catch (e) { + console.log('Remote container already initialized'); + } + } + }; + resolve(proxy); + } + document.head.appendChild(script); + })\`, + ], + ], + }; + ` + ); + + updateFile( + `${host}/rspack.config.prod.js`, + `module.exports = require('./rspack.config');` + ); + + // update e2e + updateFile( + `${host}-e2e/src/e2e/app.cy.ts`, + ` + import { getGreeting } from '../support/app.po'; + + describe('${host}', () => { + beforeEach(() => cy.visit('/')); + + it('should display welcome message', () => { + getGreeting().contains('Welcome ${host}'); + }); + + it('should navigate to /${remote} from /', () => { + cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice( + 1 + )}').click(); + cy.url().should('include', '/${remote}'); + getGreeting().contains('Welcome ${remote}'); + }); + }); + ` + ); + + const hostPort = readPort(host); + + // Build host and remote + const buildOutput = runCLI(`build ${host}`); + const remoteOutput = runCLI(`build ${remote}`); + + expect(buildOutput).toContain('Successfully ran target build'); + expect(remoteOutput).toContain('Successfully ran target build'); + + if (runE2ETests()) { + const hostE2eResults = await runCommandUntil( + `e2e ${host}-e2e --verbose`, + (output) => output.includes('All specs passed!') + ); + await killProcessAndPorts( + hostE2eResults.pid, + hostPort, + hostPort + 1, + remotePort + ); + } + }, 500_000); +}); diff --git a/e2e/react/src/module-federation/independent-deployability-rspack-setup.ts b/e2e/react/src/module-federation/independent-deployability-rspack-setup.ts new file mode 100644 index 00000000000000..9612983b91d2a6 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-rspack-setup.ts @@ -0,0 +1,6 @@ +import { cleanupProject, newProject } from '@nx/e2e-utils'; + +export function setupIndependentDeployabilityTests() { + const proj = newProject(); + return proj; +} diff --git a/e2e/react/src/module-federation/independent-deployability-rspack-var.test.ts b/e2e/react/src/module-federation/independent-deployability-rspack-var.test.ts new file mode 100644 index 00000000000000..a28516d92ff781 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-rspack-var.test.ts @@ -0,0 +1,135 @@ +import { + cleanupProject, + getAvailablePort, + killProcessAndPorts, + runCLI, + runCommandUntil, + runE2ETests, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort } from './utils'; +import { setupIndependentDeployabilityTests } from './independent-deployability-rspack-setup'; + +describe('Independent Deployability - library type var', () => { + let proj: string; + + beforeAll(() => { + proj = setupIndependentDeployabilityTests(); + }); + + afterAll(() => { + cleanupProject(); + }); + + it('should support host and remote with library type var', async () => { + const shell = uniq('shell'); + const remote = uniq('remote'); + + const shellPort = await getAvailablePort(); + + runCLI( + `generate @nx/react:host ${shell} --remotes=${remote} --bundler=rspack --devServerPort=${shellPort} --e2eTestRunner=cypress --no-interactive --skipFormat` + ); + + const remotePort = readPort(remote); + + // update host and remote to use library type var + updateFile( + `${shell}/module-federation.config.ts`, + stripIndents` + import { ModuleFederationConfig } from '@nx/module-federation'; + + const config: ModuleFederationConfig = { + name: '${shell}', + library: { type: 'var', name: '${shell}' }, + remotes: ['${remote}'], + }; + + export default config; + ` + ); + + updateFile( + `${shell}/rspack.config.prod.ts`, + `export { default } from './rspack.config';` + ); + + updateFile( + `${remote}/module-federation.config.ts`, + stripIndents` + import { ModuleFederationConfig } from '@nx/module-federation'; + + const config: ModuleFederationConfig = { + name: '${remote}', + library: { type: 'var', name: '${remote}' }, + exposes: { + './Module': './src/remote-entry.ts', + }, + }; + + export default config; + ` + ); + + updateFile( + `${remote}/rspack.config.prod.ts`, + `export { default } from './rspack.config';` + ); + + // Update host e2e test to check that the remote works with library type var via navigation + updateFile( + `${shell}-e2e/src/e2e/app.cy.ts`, + ` + import { getGreeting } from '../support/app.po'; + + describe('${shell}', () => { + beforeEach(() => cy.visit('/')); + + it('should display welcome message', () => { + getGreeting().contains('Welcome ${shell}'); + + }); + + it('should navigate to /about from /', () => { + cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice( + 1 + )}').click(); + cy.url().should('include', '/${remote}'); + getGreeting().contains('Welcome ${remote}'); + }); + }); + ` + ); + + // Build host and remote + const buildOutput = runCLI(`build ${shell}`); + const remoteOutput = runCLI(`build ${remote}`); + + expect(buildOutput).toContain('Successfully ran target build'); + expect(remoteOutput).toContain('Successfully ran target build'); + + if (runE2ETests()) { + const hostE2eResultsSwc = await runCommandUntil( + `e2e ${shell}-e2e --verbose`, + (output) => + output.includes('NX Successfully ran target e2e for project') + ); + await killProcessAndPorts( + hostE2eResultsSwc.pid, + shellPort, + shellPort + 1, + remotePort + ); + + const remoteE2eResultsSwc = await runCommandUntil( + `e2e ${remote}-e2e --verbose`, + (output) => + output.includes('NX Successfully ran target e2e for project') + ); + + await killProcessAndPorts(remoteE2eResultsSwc.pid, remotePort); + } + }, 500_000); +}); diff --git a/e2e/react/src/module-federation/independent-deployability-rspack-versions.test.ts b/e2e/react/src/module-federation/independent-deployability-rspack-versions.test.ts new file mode 100644 index 00000000000000..ed8cca14e3ed82 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-rspack-versions.test.ts @@ -0,0 +1,176 @@ +import { + cleanupProject, + getAvailablePort, + killProcessAndPorts, + runCLI, + runCommandUntil, + runE2ETests, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort } from './utils'; +import { setupIndependentDeployabilityTests } from './independent-deployability-rspack-setup'; + +describe('Independent Deployability - different versions', () => { + let proj: string; + + beforeAll(() => { + proj = setupIndependentDeployabilityTests(); + }); + + afterAll(() => { + cleanupProject(); + }); + + it('should support different versions workspace libs for host and remote', async () => { + const shell = uniq('shell'); + const remote = uniq('remote'); + const lib = uniq('lib'); + + const shellPort = await getAvailablePort(); + + runCLI( + `generate @nx/react:host ${shell} --remotes=${remote} --devServerPort=${shellPort} --bundler=rspack --e2eTestRunner=cypress --no-interactive --skipFormat` + ); + + runCLI( + `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --skipFormat` + ); + + const remotePort = readPort(remote); + + updateFile( + `${lib}/src/lib/${lib}.ts`, + stripIndents` + export const version = '0.0.1'; + ` + ); + + updateJson(`${lib}/package.json`, (json) => { + return { + ...json, + version: '0.0.1', + }; + }); + + // Update host to use the lib + updateFile( + `${shell}/src/app/app.tsx`, + ` + import * as React from 'react'; + + import NxWelcome from './nx-welcome'; + import { version } from '@acme/${lib}'; + import { Link, Route, Routes } from 'react-router-dom'; + + const About = React.lazy(() => import('${remote}/Module')); + + export function App() { + return ( + +
+ Lib version: { version } +
+
    +
  • + Home +
  • + +
  • + About +
  • +
+ + } /> + + } /> + +
+ ); + } + + export default App;` + ); + + // Update remote to use the lib + updateFile( + `${remote}/src/app/app.tsx`, + `// eslint-disable-next-line @typescript-eslint/no-unused-vars + + import styles from './app.module.css'; + import { version } from '@acme/${lib}'; + + import NxWelcome from './nx-welcome'; + + export function App() { + return ( + +
+ Lib version: { version } + +
+ ); + } + + export default App;` + ); + + // update remote e2e test to check the version + updateFile( + `${remote}-e2e/src/e2e/app.cy.ts`, + `describe('${remote}', () => { + beforeEach(() => cy.visit('/')); + + it('should check the lib version', () => { + cy.get('div.remote').contains('Lib version: 0.0.1'); + }); + }); + ` + ); + + // update shell e2e test to check the version + updateFile( + `${shell}-e2e/src/e2e/app.cy.ts`, + ` + describe('${shell}', () => { + beforeEach(() => cy.visit('/')); + + it('should check the lib version', () => { + cy.get('div.home').contains('Lib version: 0.0.1'); + }); + }); + ` + ); + + if (runE2ETests()) { + // test remote e2e + const remoteE2eResults = await runCommandUntil( + `e2e ${remote}-e2e --verbose`, + (output) => output.includes('All specs passed!') + ); + await killProcessAndPorts(remoteE2eResults.pid, remotePort); + + // test shell e2e + // serve remote first + const remoteProcess = await runCommandUntil( + `serve ${remote} --no-watch --verbose`, + (output) => { + return output.includes(`Loopback: http://localhost:${remotePort}/`); + } + ); + await killProcessAndPorts(remoteProcess.pid, remotePort); + const shellE2eResults = await runCommandUntil( + `e2e ${shell}-e2e --verbose`, + (output) => output.includes('All specs passed!') + ); + await killProcessAndPorts( + shellE2eResults.pid, + shellPort, + shellPort + 1, + remotePort + ); + } + }, 500_000); +}); diff --git a/e2e/react/src/module-federation/independent-deployability.rspack.test.ts b/e2e/react/src/module-federation/independent-deployability.rspack.test.ts deleted file mode 100644 index 4ed502c1404096..00000000000000 --- a/e2e/react/src/module-federation/independent-deployability.rspack.test.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { - cleanupProject, - getAvailablePort, - killProcessAndPorts, - newProject, - runCommandUntil, - runE2ETests, - uniq, - updateFile, - updateJson, -} from '@nx/e2e-utils'; -import { stripIndents } from 'nx/src/utils/strip-indents'; -import { readPort, runCLI } from './utils'; - -describe('Independent Deployability', () => { - let proj: string; - - beforeAll(() => { - proj = newProject(); - }); - - afterAll(() => { - cleanupProject(); - }); - - it('should support promised based remotes', async () => { - const remote = uniq('remote'); - const host = uniq('host'); - const shellPort = await getAvailablePort(); - - runCLI( - `generate @nx/react:host ${host} --remotes=${remote} --devServerPort=${shellPort} --bundler=rspack --e2eTestRunner=cypress --no-interactive --typescriptConfiguration=false --skipFormat` - ); - - const remotePort = readPort(remote); - // Update remote to be loaded via script - updateFile( - `${remote}/module-federation.config.js`, - stripIndents` - module.exports = { - name: '${remote}', - library: { type: 'var', name: '${remote}' }, - exposes: { - './Module': './src/remote-entry.ts', - }, - }; - ` - ); - - updateFile( - `${remote}/rspack.config.prod.js`, - `module.exports = require('./rspack.config');` - ); - - // Update host to use promise based remote - updateFile( - `${host}/module-federation.config.js`, - `module.exports = { - name: '${host}', - library: { type: 'var', name: '${host}' }, - remotes: [ - [ - '${remote}', - \`promise new Promise(resolve => { - const remoteUrl = 'http://localhost:${remotePort}/remoteEntry.js'; - const script = document.createElement('script'); - script.src = remoteUrl; - script.onload = () => { - const proxy = { - get: (request) => window.${remote}.get(request), - init: (arg) => { - try { - window.${remote}.init(arg); - } catch (e) { - console.log('Remote container already initialized'); - } - } - }; - resolve(proxy); - } - document.head.appendChild(script); - })\`, - ], - ], - }; - ` - ); - - updateFile( - `${host}/rspack.config.prod.js`, - `module.exports = require('./rspack.config');` - ); - - // update e2e - updateFile( - `${host}-e2e/src/e2e/app.cy.ts`, - ` - import { getGreeting } from '../support/app.po'; - - describe('${host}', () => { - beforeEach(() => cy.visit('/')); - - it('should display welcome message', () => { - getGreeting().contains('Welcome ${host}'); - }); - - it('should navigate to /${remote} from /', () => { - cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice( - 1 - )}').click(); - cy.url().should('include', '/${remote}'); - getGreeting().contains('Welcome ${remote}'); - }); - }); - ` - ); - - const hostPort = readPort(host); - - // Build host and remote - const buildOutput = runCLI(`build ${host}`); - const remoteOutput = runCLI(`build ${remote}`); - - expect(buildOutput).toContain('Successfully ran target build'); - expect(remoteOutput).toContain('Successfully ran target build'); - - if (runE2ETests()) { - const hostE2eResults = await runCommandUntil( - `e2e ${host}-e2e --verbose`, - (output) => output.includes('All specs passed!') - ); - await killProcessAndPorts( - hostE2eResults.pid, - hostPort, - hostPort + 1, - remotePort - ); - } - }, 500_000); - - it('should support different versions workspace libs for host and remote', async () => { - const shell = uniq('shell'); - const remote = uniq('remote'); - const lib = uniq('lib'); - - const shellPort = await getAvailablePort(); - - runCLI( - `generate @nx/react:host ${shell} --remotes=${remote} --devServerPort=${shellPort} --bundler=rspack --e2eTestRunner=cypress --no-interactive --skipFormat` - ); - - runCLI( - `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --skipFormat` - ); - - const remotePort = readPort(remote); - - updateFile( - `${lib}/src/lib/${lib}.ts`, - stripIndents` - export const version = '0.0.1'; - ` - ); - - updateJson(`${lib}/package.json`, (json) => { - return { - ...json, - version: '0.0.1', - }; - }); - - // Update host to use the lib - updateFile( - `${shell}/src/app/app.tsx`, - ` - import * as React from 'react'; - - import NxWelcome from './nx-welcome'; - import { version } from '@acme/${lib}'; - import { Link, Route, Routes } from 'react-router-dom'; - - const About = React.lazy(() => import('${remote}/Module')); - - export function App() { - return ( - -
- Lib version: { version } -
-
    -
  • - Home -
  • - -
  • - About -
  • -
- - } /> - - } /> - -
- ); - } - - export default App;` - ); - - // Update remote to use the lib - updateFile( - `${remote}/src/app/app.tsx`, - `// eslint-disable-next-line @typescript-eslint/no-unused-vars - - import styles from './app.module.css'; - import { version } from '@acme/${lib}'; - - import NxWelcome from './nx-welcome'; - - export function App() { - return ( - -
- Lib version: { version } - -
- ); - } - - export default App;` - ); - - // update remote e2e test to check the version - updateFile( - `${remote}-e2e/src/e2e/app.cy.ts`, - `describe('${remote}', () => { - beforeEach(() => cy.visit('/')); - - it('should check the lib version', () => { - cy.get('div.remote').contains('Lib version: 0.0.1'); - }); - }); - ` - ); - - // update shell e2e test to check the version - updateFile( - `${shell}-e2e/src/e2e/app.cy.ts`, - ` - describe('${shell}', () => { - beforeEach(() => cy.visit('/')); - - it('should check the lib version', () => { - cy.get('div.home').contains('Lib version: 0.0.1'); - }); - }); - ` - ); - - if (runE2ETests()) { - // test remote e2e - const remoteE2eResults = await runCommandUntil( - `e2e ${remote}-e2e --verbose`, - (output) => output.includes('All specs passed!') - ); - await killProcessAndPorts(remoteE2eResults.pid, remotePort); - - // test shell e2e - // serve remote first - const remoteProcess = await runCommandUntil( - `serve ${remote} --no-watch --verbose`, - (output) => { - return output.includes(`Loopback: http://localhost:${remotePort}/`); - } - ); - await killProcessAndPorts(remoteProcess.pid, remotePort); - const shellE2eResults = await runCommandUntil( - `e2e ${shell}-e2e --verbose`, - (output) => output.includes('All specs passed!') - ); - await killProcessAndPorts( - shellE2eResults.pid, - shellPort, - shellPort + 1, - remotePort - ); - } - }, 500_000); - - it('should support host and remote with library type var', async () => { - const shell = uniq('shell'); - const remote = uniq('remote'); - - const shellPort = await getAvailablePort(); - - runCLI( - `generate @nx/react:host ${shell} --remotes=${remote} --bundler=rspack --devServerPort=${shellPort} --e2eTestRunner=cypress --no-interactive --skipFormat` - ); - - const remotePort = readPort(remote); - - // update host and remote to use library type var - updateFile( - `${shell}/module-federation.config.ts`, - stripIndents` - import { ModuleFederationConfig } from '@nx/module-federation'; - - const config: ModuleFederationConfig = { - name: '${shell}', - library: { type: 'var', name: '${shell}' }, - remotes: ['${remote}'], - }; - - export default config; - ` - ); - - updateFile( - `${shell}/rspack.config.prod.ts`, - `export { default } from './rspack.config';` - ); - - updateFile( - `${remote}/module-federation.config.ts`, - stripIndents` - import { ModuleFederationConfig } from '@nx/module-federation'; - - const config: ModuleFederationConfig = { - name: '${remote}', - library: { type: 'var', name: '${remote}' }, - exposes: { - './Module': './src/remote-entry.ts', - }, - }; - - export default config; - ` - ); - - updateFile( - `${remote}/rspack.config.prod.ts`, - `export { default } from './rspack.config';` - ); - - // Update host e2e test to check that the remote works with library type var via navigation - updateFile( - `${shell}-e2e/src/e2e/app.cy.ts`, - ` - import { getGreeting } from '../support/app.po'; - - describe('${shell}', () => { - beforeEach(() => cy.visit('/')); - - it('should display welcome message', () => { - getGreeting().contains('Welcome ${shell}'); - - }); - - it('should navigate to /about from /', () => { - cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice( - 1 - )}').click(); - cy.url().should('include', '/${remote}'); - getGreeting().contains('Welcome ${remote}'); - }); - }); - ` - ); - - // Build host and remote - const buildOutput = runCLI(`build ${shell}`); - const remoteOutput = runCLI(`build ${remote}`); - - expect(buildOutput).toContain('Successfully ran target build'); - expect(remoteOutput).toContain('Successfully ran target build'); - - if (runE2ETests()) { - const hostE2eResultsSwc = await runCommandUntil( - `e2e ${shell}-e2e --verbose`, - (output) => - output.includes('NX Successfully ran target e2e for project') - ); - await killProcessAndPorts( - hostE2eResultsSwc.pid, - shellPort, - shellPort + 1, - remotePort - ); - - const remoteE2eResultsSwc = await runCommandUntil( - `e2e ${remote}-e2e --verbose`, - (output) => - output.includes('NX Successfully ran target e2e for project') - ); - - await killProcessAndPorts(remoteE2eResultsSwc.pid, remotePort); - } - }, 500_000); -}); diff --git a/nx.json b/nx.json index 0c6ca02ce7bf2f..f7f2298df07693 100644 --- a/nx.json +++ b/nx.json @@ -330,7 +330,7 @@ } ], "parallel": 1, - "bust": 3, + "bust": 12, "defaultBase": "master", "sync": { "applyChanges": true