diff --git a/.nx/workflows/dynamic-changesets.yaml b/.nx/workflows/dynamic-changesets.yaml index 8580c122f8c9c..3d14e9ab1acd2 100644 --- a/.nx/workflows/dynamic-changesets.yaml +++ b/.nx/workflows/dynamic-changesets.yaml @@ -26,16 +26,7 @@ assignment-rules: - e2e-web - e2e-eslint 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/react/src/module-federation/independent-deployability-different-lib-versions.test.ts b/e2e/react/src/module-federation/independent-deployability-different-lib-versions.test.ts new file mode 100644 index 0000000000000..69ce3beb6d892 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-different-lib-versions.test.ts @@ -0,0 +1,178 @@ +import { + getAvailablePort, + killProcessAndPorts, + runCommandUntil, + runE2ETests, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort, runCLI } from './utils'; +import { + setupIndependentDeployabilityTest, + cleanupIndependentDeployabilityTest, +} from './independent-deployability-setup'; + +describe('Independent Deployability', () => { + let proj: string; + beforeAll(() => { + proj = setupIndependentDeployabilityTest(); + }); + + afterAll(() => { + cleanupIndependentDeployabilityTest(); + }); + + 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=webpack --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 } +
+ + + } /> + + } /> + +
+ ); + } + + 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 --no-watch --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( + `Web Development Server is listening at http://localhost:${remotePort}/` + ); + } + ); + await killProcessAndPorts(remoteProcess.pid, remotePort); + const shellE2eResults = await runCommandUntil( + `e2e ${shell}-e2e --no-watch --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-library-type-var.test.ts b/e2e/react/src/module-federation/independent-deployability-library-type-var.test.ts new file mode 100644 index 0000000000000..b179b36904b06 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-library-type-var.test.ts @@ -0,0 +1,153 @@ +import { + getAvailablePort, + killProcessAndPorts, + runCommandUntil, + runE2ETests, + uniq, + updateFile, +} from '@nx/e2e-utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort, runCLI } from './utils'; +import { + setupIndependentDeployabilityTest, + cleanupIndependentDeployabilityTest, +} from './independent-deployability-setup'; + +describe('Independent Deployability', () => { + let proj: string; + beforeAll(() => { + proj = setupIndependentDeployabilityTest(); + }); + + afterAll(() => { + cleanupIndependentDeployabilityTest(); + }); + + 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} --devServerPort=${shellPort} --remotes=${remote} --bundler=webpack --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/webpack'; + + const config: ModuleFederationConfig = { + name: '${shell}', + library: { type: 'var', name: '${shell}' }, + remotes: ['${remote}'], + }; + + export default config; + ` + ); + + updateFile( + `${shell}/webpack.config.prod.ts`, + `export { default } from './webpack.config';` + ); + + updateFile( + `${remote}/module-federation.config.ts`, + stripIndents` + import { ModuleFederationConfig } from '@nx/webpack'; + + const config: ModuleFederationConfig = { + name: '${remote}', + library: { type: 'var', name: '${remote}' }, + exposes: { + './Module': './src/remote-entry.ts', + }, + }; + + export default config; + ` + ); + + updateFile( + `${remote}/webpack.config.prod.ts`, + `export { default } from './webpack.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 --no-watch --verbose`, + (output) => output.includes('All specs passed!') + ); + await killProcessAndPorts( + hostE2eResultsSwc.pid, + shellPort, + shellPort + 1, + remotePort + ); + + const remoteE2eResultsSwc = await runCommandUntil( + `e2e ${remote}-e2e --no-watch --verbose`, + (output) => output.includes('All specs passed!') + ); + + await killProcessAndPorts(remoteE2eResultsSwc.pid, remotePort); + + const hostE2eResultsTsNode = await runCommandUntil( + `e2e ${shell}-e2e --no-watch --verbose`, + (output) => output.includes('All specs passed!'), + { env: { NX_PREFER_TS_NODE: 'true' } } + ); + + await killProcessAndPorts( + hostE2eResultsTsNode.pid, + shellPort, + shellPort + 1, + remotePort + ); + + const remoteE2eResultsTsNode = await runCommandUntil( + `e2e ${remote}-e2e --no-watch --verbose`, + (output) => output.includes('All specs passed!'), + { env: { NX_PREFER_TS_NODE: 'true' } } + ); + + await killProcessAndPorts(remoteE2eResultsTsNode.pid, remotePort); + } + }, 500_000); +}); diff --git a/e2e/react/src/module-federation/independent-deployability-promise-based-remotes.test.ts b/e2e/react/src/module-federation/independent-deployability-promise-based-remotes.test.ts new file mode 100644 index 0000000000000..c83d49e446e2c --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-promise-based-remotes.test.ts @@ -0,0 +1,162 @@ +import { + getAvailablePort, + killProcessAndPorts, + runCommandUntil, + runE2ETests, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort, runCLI } from './utils'; +import { + setupIndependentDeployabilityTest, + cleanupIndependentDeployabilityTest, +} from './independent-deployability-setup'; + +describe('Independent Deployability', () => { + let proj: string; + beforeAll(() => { + proj = setupIndependentDeployabilityTest(); + }); + + afterAll(() => { + cleanupIndependentDeployabilityTest(); + }); + + 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=webpack --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}/webpack.config.prod.js`, + `module.exports = require('./webpack.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}/webpack.config.prod.js`, + `module.exports = require('./webpack.config');` + ); + + // Update e2e project.json + updateJson(`${host}-e2e/project.json`, (json) => { + return { + ...json, + targets: { + ...json.targets, + e2e: { + ...json.targets.e2e, + options: { + ...json.targets.e2e.options, + devServerTarget: `${host}:serve-static:production`, + }, + }, + }, + }; + }); + + // 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 remoteProcess = await runCommandUntil( + `serve-static ${remote} --no-watch --verbose`, + () => { + return true; + } + ); + const hostE2eResults = await runCommandUntil( + `e2e ${host}-e2e --no-watch --verbose`, + (output) => output.includes('All specs passed!') + ); + await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1); + await killProcessAndPorts(remoteProcess.pid, remotePort); + } + }, 500_000); +}); diff --git a/e2e/react/src/module-federation/independent-deployability-setup.ts b/e2e/react/src/module-federation/independent-deployability-setup.ts new file mode 100644 index 0000000000000..d41c09f8396d8 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability-setup.ts @@ -0,0 +1,13 @@ +import { cleanupProject, newProject } from '@nx/e2e-utils'; + +export function setupIndependentDeployabilityTest() { + let proj: string; + process.env.NX_ADD_PLUGINS = 'false'; + proj = newProject(); + return proj; +} + +export function cleanupIndependentDeployabilityTest() { + cleanupProject(); + delete process.env.NX_ADD_PLUGINS; +} diff --git a/e2e/react/src/module-federation/independent-deployability.webpack.test.ts b/e2e/react/src/module-federation/independent-deployability.webpack.test.ts deleted file mode 100644 index f2a2afb63412e..0000000000000 --- a/e2e/react/src/module-federation/independent-deployability.webpack.test.ts +++ /dev/null @@ -1,442 +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(() => { - process.env.NX_ADD_PLUGINS = 'false'; - proj = newProject(); - }); - - afterAll(() => { - cleanupProject(); - delete process.env.NX_ADD_PLUGINS; - }); - - 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=webpack --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}/webpack.config.prod.js`, - `module.exports = require('./webpack.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}/webpack.config.prod.js`, - `module.exports = require('./webpack.config');` - ); - - // Update e2e project.json - updateJson(`${host}-e2e/project.json`, (json) => { - return { - ...json, - targets: { - ...json.targets, - e2e: { - ...json.targets.e2e, - options: { - ...json.targets.e2e.options, - devServerTarget: `${host}:serve-static:production`, - }, - }, - }, - }; - }); - - // 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 remoteProcess = await runCommandUntil( - `serve-static ${remote} --no-watch --verbose`, - () => { - return true; - } - ); - const hostE2eResults = await runCommandUntil( - `e2e ${host}-e2e --no-watch --verbose`, - (output) => output.includes('All specs passed!') - ); - await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1); - await killProcessAndPorts(remoteProcess.pid, 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=webpack --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 } -
- - - } /> - - } /> - -
- ); - } - - 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 --no-watch --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( - `Web Development Server is listening at http://localhost:${remotePort}/` - ); - } - ); - await killProcessAndPorts(remoteProcess.pid, remotePort); - const shellE2eResults = await runCommandUntil( - `e2e ${shell}-e2e --no-watch --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} --devServerPort=${shellPort} --remotes=${remote} --bundler=webpack --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/webpack'; - - const config: ModuleFederationConfig = { - name: '${shell}', - library: { type: 'var', name: '${shell}' }, - remotes: ['${remote}'], - }; - - export default config; - ` - ); - - updateFile( - `${shell}/webpack.config.prod.ts`, - `export { default } from './webpack.config';` - ); - - updateFile( - `${remote}/module-federation.config.ts`, - stripIndents` - import { ModuleFederationConfig } from '@nx/webpack'; - - const config: ModuleFederationConfig = { - name: '${remote}', - library: { type: 'var', name: '${remote}' }, - exposes: { - './Module': './src/remote-entry.ts', - }, - }; - - export default config; - ` - ); - - updateFile( - `${remote}/webpack.config.prod.ts`, - `export { default } from './webpack.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 --no-watch --verbose`, - (output) => output.includes('All specs passed!') - ); - await killProcessAndPorts( - hostE2eResultsSwc.pid, - shellPort, - shellPort + 1, - remotePort - ); - - const remoteE2eResultsSwc = await runCommandUntil( - `e2e ${remote}-e2e --no-watch --verbose`, - (output) => output.includes('All specs passed!') - ); - - await killProcessAndPorts(remoteE2eResultsSwc.pid, remotePort); - - const hostE2eResultsTsNode = await runCommandUntil( - `e2e ${shell}-e2e --no-watch --verbose`, - (output) => output.includes('All specs passed!'), - { env: { NX_PREFER_TS_NODE: 'true' } } - ); - - await killProcessAndPorts( - hostE2eResultsTsNode.pid, - shellPort, - shellPort + 1, - remotePort - ); - - const remoteE2eResultsTsNode = await runCommandUntil( - `e2e ${remote}-e2e --no-watch --verbose`, - (output) => output.includes('All specs passed!'), - { env: { NX_PREFER_TS_NODE: 'true' } } - ); - - await killProcessAndPorts(remoteE2eResultsTsNode.pid, remotePort); - } - }, 500_000); -}); diff --git a/e2e/remix/src/nx-remix-npm.test.ts b/e2e/remix/src/nx-remix-npm.test.ts new file mode 100644 index 0000000000000..128106ab24ad7 --- /dev/null +++ b/e2e/remix/src/nx-remix-npm.test.ts @@ -0,0 +1,23 @@ +import { runCLI, uniq, runCommandAsync } from '@nx/e2e-utils'; +import { setupNxRemixTestNpm, cleanupNxRemixTest } from './nx-remix-setup-npm'; + +describe('Remix E2E Tests', () => { + describe('--integrated (npm)', () => { + beforeAll(() => { + setupNxRemixTestNpm(); + }); + + afterAll(() => { + cleanupNxRemixTest(); + }); + + it('should not cause peer dependency conflicts', async () => { + const plugin = uniq('remix'); + runCLI( + `generate @nx/remix:app ${plugin} --linter=eslint --unitTestRunner=vitest` + ); + + await runCommandAsync('npm install'); + }, 120000); + }); +}); diff --git a/e2e/remix/src/nx-remix-setup-npm.ts b/e2e/remix/src/nx-remix-setup-npm.ts new file mode 100644 index 0000000000000..cf0a1b7c11826 --- /dev/null +++ b/e2e/remix/src/nx-remix-setup-npm.ts @@ -0,0 +1,13 @@ +import { cleanupProject, killPorts, newProject } from '@nx/e2e-utils'; + +export function setupNxRemixTestNpm() { + newProject({ + packages: ['@nx/remix', '@nx/react'], + packageManager: 'npm', + }); +} + +export function cleanupNxRemixTest() { + killPorts(); + cleanupProject(); +} diff --git a/e2e/remix/src/nx-remix-setup-standalone.ts b/e2e/remix/src/nx-remix-setup-standalone.ts new file mode 100644 index 0000000000000..41ce544eac10d --- /dev/null +++ b/e2e/remix/src/nx-remix-setup-standalone.ts @@ -0,0 +1,11 @@ +import { cleanupProject, killPorts, newProject } from '@nx/e2e-utils'; + +export function setupNxRemixTestStandalone() { + const proj = newProject({ packages: ['@nx/remix'] }); + return proj; +} + +export function cleanupNxRemixTest() { + killPorts(); + cleanupProject(); +} diff --git a/e2e/remix/src/nx-remix-standalone.test.ts b/e2e/remix/src/nx-remix-standalone.test.ts new file mode 100644 index 0000000000000..5320df1600572 --- /dev/null +++ b/e2e/remix/src/nx-remix-standalone.test.ts @@ -0,0 +1,43 @@ +import { runCLI, uniq, updateFile } from '@nx/e2e-utils'; +import { + setupNxRemixTestStandalone, + cleanupNxRemixTest, +} from './nx-remix-setup-standalone'; + +describe('Remix E2E Tests', () => { + describe('--standalone', () => { + let proj: string; + + beforeAll(() => { + proj = setupNxRemixTestStandalone(); + }); + + afterAll(() => { + cleanupNxRemixTest(); + }); + + it('should create a standalone remix app', async () => { + const appName = uniq('remix'); + runCLI( + `generate @nx/remix:preset --name ${appName} --directory=apps/${appName} --verbose` + ); + + // Can import using ~ alias like a normal Remix setup. + updateFile(`app/foo.ts`, `export const foo = 'foo';`); + updateFile( + `app/routes/index.tsx`, + ` + import { foo } from '~/foo'; + export default function Index() { + return ( +

{foo}

+ ); + } + ` + ); + + const result = runCLI(`build ${appName}`); + expect(result).toContain('Successfully ran target build'); + }, 120_000); + }); +}); diff --git a/e2e/remix/src/nx-remix.test.ts b/e2e/remix/src/nx-remix.test.ts index 2368859b9c360..6d4e957fffd70 100644 --- a/e2e/remix/src/nx-remix.test.ts +++ b/e2e/remix/src/nx-remix.test.ts @@ -11,28 +11,6 @@ import { } from '@nx/e2e-utils'; describe('Remix E2E Tests', () => { - describe('--integrated (npm)', () => { - beforeAll(() => { - newProject({ - packages: ['@nx/remix', '@nx/react'], - packageManager: 'npm', - }); - }); - - afterAll(() => { - killPorts(); - cleanupProject(); - }); - - it('should not cause peer dependency conflicts', async () => { - const plugin = uniq('remix'); - runCLI( - `generate @nx/remix:app ${plugin} --linter=eslint --unitTestRunner=vitest` - ); - - await runCommandAsync('npm install'); - }, 120000); - }); describe('--integrated (yarn)', () => { beforeAll(async () => { newProject({ @@ -188,41 +166,4 @@ describe('Remix E2E Tests', () => { }, 120000); }); }); - - describe('--standalone', () => { - let proj: string; - - beforeAll(() => { - proj = newProject({ packages: ['@nx/remix'] }); - }); - - afterAll(() => { - killPorts(); - cleanupProject(); - }); - - it('should create a standalone remix app', async () => { - const appName = uniq('remix'); - runCLI( - `generate @nx/remix:preset --name ${appName} --directory=apps/${appName} --verbose` - ); - - // Can import using ~ alias like a normal Remix setup. - updateFile(`app/foo.ts`, `export const foo = 'foo';`); - updateFile( - `app/routes/index.tsx`, - ` - import { foo } from '~/foo'; - export default function Index() { - return ( -

{foo}

- ); - } - ` - ); - - const result = runCLI(`build ${appName}`); - expect(result).toContain('Successfully ran target build'); - }, 120_000); - }); }); diff --git a/e2e/remix/src/remix-ts-solution-import-path.test.ts b/e2e/remix/src/remix-ts-solution-import-path.test.ts new file mode 100644 index 0000000000000..8a8e554038103 --- /dev/null +++ b/e2e/remix/src/remix-ts-solution-import-path.test.ts @@ -0,0 +1,39 @@ +import { runCLI, readJson, uniq } from '@nx/e2e-utils'; +import { + setupRemixTsSolutionTest, + cleanupRemixTsSolutionTest, +} from './remix-ts-solution-setup'; + +describe('Remix - TS solution setup', () => { + beforeEach(() => { + setupRemixTsSolutionTest(); + }); + + afterEach(() => { + cleanupRemixTsSolutionTest(); + }); + + it('should respect and support generating libraries with a name different than the import path', async () => { + const lib = uniq('lib'); + + runCLI( + `generate @nx/remix:library packages/${lib} --name=${lib} --linter=eslint --unitTestRunner=vitest --buildable` + ); + + const packageJson = readJson(`packages/${lib}/package.json`); + expect(packageJson.nx.name).toBe(lib); + + expect(runCLI(`build ${lib}`)).toContain( + `Successfully ran target build for project ${lib}` + ); + expect(runCLI(`typecheck ${lib}`)).toContain( + `Successfully ran target typecheck for project ${lib}` + ); + expect(runCLI(`lint ${lib}`)).toContain( + `Successfully ran target lint for project ${lib}` + ); + expect(runCLI(`test ${lib}`)).toContain( + `Successfully ran target test for project ${lib}` + ); + }, 120_000); +}); diff --git a/e2e/remix/src/remix-ts-solution.test.ts b/e2e/remix/src/remix-ts-solution-jest-vitest.test.ts similarity index 80% rename from e2e/remix/src/remix-ts-solution.test.ts rename to e2e/remix/src/remix-ts-solution-jest-vitest.test.ts index 3921c126b1a4e..dac63fd91378b 100644 --- a/e2e/remix/src/remix-ts-solution.test.ts +++ b/e2e/remix/src/remix-ts-solution-jest-vitest.test.ts @@ -1,21 +1,16 @@ +import { runCLI, uniq } from '@nx/e2e-utils'; import { - cleanupProject, - newProject, - readJson, - runCLI, - uniq, -} from '@nx/e2e-utils'; + setupRemixTsSolutionTest, + cleanupRemixTsSolutionTest, +} from './remix-ts-solution-setup'; describe('Remix - TS solution setup', () => { beforeEach(() => { - newProject({ - packages: ['@nx/remix'], - preset: 'ts', - }); + setupRemixTsSolutionTest(); }); afterEach(() => { - cleanupProject(); + cleanupRemixTsSolutionTest(); }); it('should generate apps and libraries with jest and vitest and work correctly', async () => { @@ -119,28 +114,4 @@ describe('Remix - TS solution setup', () => { `Successfully ran target test for project @proj/${buildableLibJest}` ); }, 120_000); - - it('should respect and support generating libraries with a name different than the import path', async () => { - const lib = uniq('lib'); - - runCLI( - `generate @nx/remix:library packages/${lib} --name=${lib} --linter=eslint --unitTestRunner=vitest --buildable` - ); - - const packageJson = readJson(`packages/${lib}/package.json`); - expect(packageJson.nx.name).toBe(lib); - - expect(runCLI(`build ${lib}`)).toContain( - `Successfully ran target build for project ${lib}` - ); - expect(runCLI(`typecheck ${lib}`)).toContain( - `Successfully ran target typecheck for project ${lib}` - ); - expect(runCLI(`lint ${lib}`)).toContain( - `Successfully ran target lint for project ${lib}` - ); - expect(runCLI(`test ${lib}`)).toContain( - `Successfully ran target test for project ${lib}` - ); - }, 120_000); }); diff --git a/e2e/remix/src/remix-ts-solution-setup.ts b/e2e/remix/src/remix-ts-solution-setup.ts new file mode 100644 index 0000000000000..eaeddbd73a3a1 --- /dev/null +++ b/e2e/remix/src/remix-ts-solution-setup.ts @@ -0,0 +1,12 @@ +import { cleanupProject, newProject } from '@nx/e2e-utils'; + +export function setupRemixTsSolutionTest() { + newProject({ + packages: ['@nx/remix'], + preset: 'ts', + }); +} + +export function cleanupRemixTsSolutionTest() { + cleanupProject(); +} diff --git a/e2e/vite/src/vite-legacy-esm-only.test.ts b/e2e/vite/src/vite-legacy-esm-only.test.ts new file mode 100644 index 0000000000000..34cbe26146063 --- /dev/null +++ b/e2e/vite/src/vite-legacy-esm-only.test.ts @@ -0,0 +1,97 @@ +import { + checkFilesExist, + cleanupProject, + getPackageManagerCommand, + newProject, + runCLI, + runCommand, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; + +describe('Vite Plugin', () => { + let proj: string; + let originalEnv: string; + beforeAll(() => { + originalEnv = process.env.NX_ADD_PLUGINS; + process.env.NX_ADD_PLUGINS = 'false'; + proj = newProject({ + packages: ['@nx/react', '@nx/web'], + }); + }); + + afterAll(() => { + process.env.NX_ADD_PLUGINS = originalEnv; + cleanupProject(); + }); + + describe('ESM-only apps', () => { + beforeAll(() => { + newProject({ + packages: ['@nx/react'], + }); + }); + + it('should support ESM-only plugins in vite.config.ts for root apps (#NXP-168)', () => { + // ESM-only plugin to test with + updateFile( + 'foo/package.json', + JSON.stringify({ + name: '@acme/foo', + type: 'module', + version: '1.0.0', + main: 'index.js', + }) + ); + updateFile( + 'foo/index.js', + ` + export default function fooPlugin() { + return { + name: 'foo-plugin', + configResolved() { + console.log('Foo plugin'); + } + } + }` + ); + updateJson('package.json', (json) => { + json.devDependencies['@acme/foo'] = 'file:./foo'; + return json; + }); + runCommand(getPackageManagerCommand().install); + + const rootApp = uniq('root'); + runCLI( + `generate @nx/react:app ${rootApp} --rootProject --bundler=vite --unitTestRunner=none --e2eTestRunner=none --style=css --no-interactive` + ); + updateJson(`package.json`, (json) => { + // This allows us to use ESM-only packages in vite.config.ts. + json.type = 'module'; + return json; + }); + updateFile( + `vite.config.ts`, + ` + import fooPlugin from '@acme/foo'; + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + + export default defineConfig({ + cacheDir: '../../node_modules/.vite/root-app', + server: { + port: 4200, + host: 'localhost', + }, + plugins: [react(), nxViteTsPaths(), fooPlugin()], + });` + ); + + runCLI(`build ${rootApp}`); + + checkFilesExist(`dist/${rootApp}/index.html`); + }); + }); +}); diff --git a/e2e/vite/src/vite-legacy-incremental-building.test.ts b/e2e/vite/src/vite-legacy-incremental-building.test.ts new file mode 100644 index 0000000000000..6d59fcfcf61f4 --- /dev/null +++ b/e2e/vite/src/vite-legacy-incremental-building.test.ts @@ -0,0 +1,116 @@ +import { names } from '@nx/devkit'; +import { + cleanupProject, + newProject, + removeFile, + runCLI, + uniq, + updateFile, +} from '@nx/e2e-utils'; + +describe('Vite Plugin', () => { + let proj: string; + let originalEnv: string; + beforeAll(() => { + originalEnv = process.env.NX_ADD_PLUGINS; + process.env.NX_ADD_PLUGINS = 'false'; + proj = newProject({ + packages: ['@nx/react', '@nx/web'], + }); + }); + + afterAll(() => { + process.env.NX_ADD_PLUGINS = originalEnv; + cleanupProject(); + }); + + describe('incremental building', () => { + const app = uniq('demo'); + const lib = uniq('my-lib'); + beforeAll(() => { + proj = newProject({ + name: uniq('vite-incr-build'), + packages: ['@nx/react'], + }); + runCLI( + `generate @nx/react:app ${app} --bundler=vite --unitTestRunner=vitest --no-interactive --directory=${app}` + ); + + // only this project will be directly used from dist + runCLI( + `generate @nx/react:lib ${lib}-buildable --unitTestRunner=none --bundler=vite --importPath="@acme/buildable" --no-interactive --directory=${lib}-buildable` + ); + + runCLI( + `generate @nx/react:lib ${lib} --unitTestRunner=none --bundler=none --importPath="@acme/non-buildable" --no-interactive --directory=${lib}` + ); + + // because the default js lib builds as cjs it cannot be loaded from dist + // so the paths plugin should always resolve to the libs source + runCLI( + `generate @nx/js:lib ${lib}-js --bundler=tsc --importPath="@acme/js-lib" --no-interactive --directory=${lib}-js` + ); + const buildableLibCmp = names(`${lib}-buildable`).className; + const nonBuildableLibCmp = names(lib).className; + const buildableJsLibFn = names(`${lib}-js`).propertyName; + + updateFile(`${app}/src/app/app.tsx`, () => { + return ` +import styles from './app.module.css'; +import NxWelcome from './nx-welcome'; +import { ${buildableLibCmp} } from '@acme/buildable'; +import { ${buildableJsLibFn} } from '@acme/js-lib'; +import { ${nonBuildableLibCmp} } from '@acme/non-buildable'; + +export function App() { + return ( +
+ <${buildableLibCmp} /> + <${nonBuildableLibCmp} /> +

{${buildableJsLibFn}()}

+ +
+ ); +} +export default App; +`; + }); + }); + + afterAll(() => { + cleanupProject(); + }); + + it('should build app from libs source', () => { + const results = runCLI(`build ${app} --buildLibsFromSource=true`); + expect(results).toContain('Successfully ran target build for project'); + // this should be more modules than build from dist + expect(results).toContain('38 modules transformed'); + }); + + it('should build app from libs dist', () => { + const results = runCLI(`build ${app} --buildLibsFromSource=false`); + expect(results).toContain('Successfully ran target build for project'); + // this should be less modules than building from source + expect(results).toContain('36 modules transformed'); + }); + + it('should build app from libs without package.json in lib', () => { + removeFile(`${lib}-buildable/package.json`); + + const buildFromSourceResults = runCLI( + `build ${app} --buildLibsFromSource=true` + ); + expect(buildFromSourceResults).toContain( + 'Successfully ran target build for project' + ); + + const noBuildFromSourceResults = runCLI( + `build ${app} --buildLibsFromSource=false` + ); + expect(noBuildFromSourceResults).toContain( + 'Successfully ran target build for project' + ); + }); + }); +}); diff --git a/e2e/vite/src/vite-legacy-libs-vitest-custom.test.ts b/e2e/vite/src/vite-legacy-libs-vitest-custom.test.ts new file mode 100644 index 0000000000000..31032b2a79faf --- /dev/null +++ b/e2e/vite/src/vite-legacy-libs-vitest-custom.test.ts @@ -0,0 +1,190 @@ +import { + cleanupProject, + directoryExists, + exists, + newProject, + runCLI, + runCLIAsync, + tmpProjPath, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { join } from 'path'; + +describe('Vite Plugin', () => { + let proj: string; + let originalEnv: string; + beforeAll(() => { + originalEnv = process.env.NX_ADD_PLUGINS; + process.env.NX_ADD_PLUGINS = 'false'; + proj = newProject({ + packages: ['@nx/react', '@nx/web'], + }); + }); + + afterAll(() => { + process.env.NX_ADD_PLUGINS = originalEnv; + cleanupProject(); + }); + + describe('should be able to create libs that use vitest', () => { + describe('using custom project configuration', () => { + const lib = uniq('my-custom-lib'); + beforeEach(() => { + proj = newProject({ name: uniq('vite-proj'), packages: ['@nx/react'] }); + }); + + it('should be able to run tests', async () => { + runCLI( + `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` + ); + expect(exists(tmpProjPath(`libs/${lib}/vite.config.ts`))).toBeTruthy(); + + const result = await runCLIAsync(`test ${lib}`); + expect(result.combinedOutput).toContain( + `Successfully ran target test for project ${lib}` + ); + + const nestedResults = await runCLIAsync(`test ${lib} --skip-nx-cache`, { + cwd: `${tmpProjPath()}/libs/${lib}`, + }); + expect(nestedResults.combinedOutput).toContain( + `Successfully ran target test for project ${lib}` + ); + }, 100_000); + + it('should collect coverage', () => { + runCLI( + `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` + ); + updateFile(`libs/${lib}/vite.config.ts`, () => { + return `/// + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + + export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/libs/${lib}', + plugins: [react(), nxViteTsPaths()], + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/libs/${lib}', + provider: 'v8', + enabled: true, + thresholds: { + lines: 100, + statements: 100, + functions: 100, + branches: 1000, + } + }, + }, + }); + `; + }); + + const coverageDir = `${tmpProjPath()}/coverage/libs/${lib}`; + + const results = runCLI(`test ${lib} --coverage`, { + silenceError: true, + }); + expect(results).toContain( + `Running target test for project ${lib} failed` + ); + expect(results).toContain(`ERROR: Coverage`); + expect(directoryExists(coverageDir)).toBeTruthy(); + }, 100_000); + + it('should not delete the project directory when coverage is enabled', async () => { + // when coverage is enabled in the vite.config.ts but reportsDirectory is removed + // from the @nx/vite:test executor options, vite will delete the project root directory + runCLI( + `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` + ); + updateFile(`libs/${lib}/vite.config.ts`, () => { + return `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + + +export default defineConfig({ + server: { + port: 4200, + host: 'localhost', + }, + plugins: [ + react(), + nxViteTsPaths() + ], + test: { + globals: true, + cache: { + dir: './node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['junit'], + outputFile: 'junit.xml', + coverage: { + enabled: true, + reportsDirectory: 'coverage', + } + }, +}); +`; + }); + updateJson(join('libs', lib, 'project.json'), (config) => { + delete config.targets.test.options.reportsDirectory; + return config; + }); + + const projectRoot = `${tmpProjPath()}/libs/${lib}`; + + const results = runCLI(`test ${lib}`, { + env: { + CI: 'true', // prevent vitest from watching for file changes and making the process hang + }, + }); + + expect(directoryExists(projectRoot)).toBeTruthy(); + expect(results).toContain( + `Successfully ran target test for project ${lib}` + ); + expect(results).toContain(`JUNIT report written`); + }, 100_000); + + it('should be able to run tests with inSourceTests set to true', async () => { + runCLI( + `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest --inSourceTests` + ); + expect( + exists(tmpProjPath(`libs/${lib}/src/lib/${lib}.spec.tsx`)) + ).toBeFalsy(); + + updateFile(`libs/${lib}/src/lib/${lib}.tsx`, (content) => { + content += ` + if (import.meta.vitest) { + const { expect, it } = import.meta.vitest; + it('should be successful', () => { + expect(1 + 1).toBe(2); + }); + } + `; + return content; + }); + + const result = await runCLIAsync(`test ${lib}`); + expect(result.combinedOutput).toContain(`1 passed`); + }, 100_000); + }); + }); +}); diff --git a/e2e/vite/src/vite-legacy-libs-vitest-default.test.ts b/e2e/vite/src/vite-legacy-libs-vitest-default.test.ts new file mode 100644 index 0000000000000..1de2e949496b0 --- /dev/null +++ b/e2e/vite/src/vite-legacy-libs-vitest-default.test.ts @@ -0,0 +1,68 @@ +import { + cleanupProject, + killProcessAndPorts, + newProject, + runCLI, + runCommandUntil, + uniq, +} from '@nx/e2e-utils'; +import { ChildProcess } from 'child_process'; + +describe('Vite Plugin', () => { + let proj: string; + let originalEnv: string; + beforeAll(() => { + originalEnv = process.env.NX_ADD_PLUGINS; + process.env.NX_ADD_PLUGINS = 'false'; + proj = newProject({ + packages: ['@nx/react', '@nx/web'], + }); + }); + + afterAll(() => { + process.env.NX_ADD_PLUGINS = originalEnv; + cleanupProject(); + }); + + describe('should be able to create libs that use vitest', () => { + describe('using default project configuration', () => { + const lib = uniq('my-default-lib'); + beforeAll(() => { + proj = newProject({ name: uniq('vite-proj'), packages: ['@nx/react'] }); + runCLI( + `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` + ); + }); + + it('should collect coverage when --coverage is set', () => { + const results = runCLI(`test ${lib} --coverage`); + expect(results).toContain(`Coverage report`); + }, 100_000); + + it('should be able to watch tests', async () => { + let cp: ChildProcess; + try { + cp = await runCommandUntil(`test ${lib} --watch`, (output) => { + return output.includes('Waiting for file changes...'); + }); + } catch (error) { + console.error(error); + } + + if (cp && cp.pid) { + await killProcessAndPorts(cp.pid); + } + }, 100_000); + + it('should not watch tests when --watch is not set', async () => { + const results = runCLI(`test ${lib}`); + + expect(results).not.toContain('Waiting for file changes...'); + + expect(results).toContain( + `Successfully ran target test for project ${lib}` + ); + }, 100_000); + }); + }); +}); diff --git a/e2e/vite/src/vite-legacy-react-apps.test.ts b/e2e/vite/src/vite-legacy-react-apps.test.ts new file mode 100644 index 0000000000000..c625bf6563579 --- /dev/null +++ b/e2e/vite/src/vite-legacy-react-apps.test.ts @@ -0,0 +1,188 @@ +import { + cleanupProject, + createFile, + newProject, + readFile, + removeFile, + rmDist, + runCLI, + uniq, + updateFile, + updateJson, + checkFilesExist, +} from '@nx/e2e-utils'; + +describe('Vite Plugin', () => { + let proj: string; + let originalEnv: string; + beforeAll(() => { + originalEnv = process.env.NX_ADD_PLUGINS; + process.env.NX_ADD_PLUGINS = 'false'; + proj = newProject({ + packages: ['@nx/react', '@nx/web'], + }); + }); + + afterAll(() => { + process.env.NX_ADD_PLUGINS = originalEnv; + cleanupProject(); + }); + + describe('Vite on React apps', () => { + describe('set up new React app with --bundler=vite option', () => { + let myApp; + + beforeAll(() => { + myApp = uniq('my-app'); + runCLI( + `generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest` + ); + }); + + afterEach(() => { + rmDist(); + }); + + describe('build the app', () => { + it('should build application', async () => { + runCLI(`build ${myApp}`); + expect(readFile(`dist/${myApp}/favicon.ico`)).toBeDefined(); + expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); + }, 200_000); + + describe('when the app has static assets', () => { + beforeAll(() => { + createFile(`${myApp}/public/hello.md`, `# Hello World`); + }); + + afterAll(() => { + removeFile(`${myApp}/public/hello.md`); + }); + + it('should copy the assets to the output path', async () => { + runCLI(`build ${myApp}`); + expect(readFile(`dist/${myApp}/favicon.ico`)).toBeDefined(); + expect(readFile(`dist/${myApp}/hello.md`)).toBeDefined(); + expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); + }, 200_000); + }); + }); + + describe('test the app', () => { + it('should test application', async () => { + const result = runCLI(`test ${myApp}`); + expect(result).toContain('Successfully ran target test'); + }, 200_000); + + it('should generate a coverage file specified by the executor', async () => { + updateJson(`${myApp}/project.json`, (json) => { + json.targets.test.options.reportsDirectory = '../coverage/test-dir'; + return json; + }); + + const result = runCLI(`test ${myApp} --coverage`); + + checkFilesExist(`coverage/test-dir/index.html`); + expect(result).toContain('Coverage report'); + }, 200_000); + }); + }); + + describe('set up new React app with --bundler=vite option and use environments api', () => { + let myApp; + + beforeAll(() => { + myApp = uniq('my-app'); + runCLI( + `generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest` + ); + updateJson(`${myApp}/project.json`, (json) => { + json.targets.build.options.useEnvironmentsApi = true; + return json; + }); + updateFile( + `${myApp}/vite.config.ts`, + `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: './node_modules/.vite/${myApp}', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + builder: {}, + environments: { + ssr: { + build: { + rollupOptions: { + input: '${myApp}/src/main.server.tsx' + } + } + } + }, + build: { + outDir: './dist/${myApp}', + emptyOutDir: false, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './coverage/${myApp}', + provider: 'v8', + }, + }, +}); +` + ); + updateFile( + `${myApp}/src/main.server.tsx`, + `import React from 'react' +import ReactDOMServer from 'react-dom/server' +import App from './app/app'; + +export default async function render(_url: string, document: string) { + const html = ReactDOMServer.renderToString( + + + + ) + return document.replace('', html); +}` + ); + }); + + afterEach(() => { + rmDist(); + }); + + it('should build application', async () => { + runCLI(`build ${myApp}`); + expect(readFile(`dist/${myApp}/favicon.ico`)).toBeDefined(); + expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); + expect(readFile(`dist/${myApp}/main.server.mjs`)).toBeDefined(); + }, 200_000); + }); + }); +}); diff --git a/e2e/vite/src/vite-legacy-setup.ts b/e2e/vite/src/vite-legacy-setup.ts new file mode 100644 index 0000000000000..c644cf5027cd7 --- /dev/null +++ b/e2e/vite/src/vite-legacy-setup.ts @@ -0,0 +1,15 @@ +import { cleanupProject, newProject } from '@nx/e2e-utils'; + +export function setupViteLegacyTest() { + const originalEnv = process.env.NX_ADD_PLUGINS; + process.env.NX_ADD_PLUGINS = 'false'; + const proj = newProject({ + packages: ['@nx/react', '@nx/web'], + }); + return { proj, originalEnv }; +} + +export function cleanupViteLegacyTest(originalEnv: string) { + process.env.NX_ADD_PLUGINS = originalEnv; + cleanupProject(); +} diff --git a/e2e/vite/src/vite-legacy-web-apps.test.ts b/e2e/vite/src/vite-legacy-web-apps.test.ts new file mode 100644 index 0000000000000..e0acd2ea10487 --- /dev/null +++ b/e2e/vite/src/vite-legacy-web-apps.test.ts @@ -0,0 +1,114 @@ +import { + cleanupProject, + createFile, + newProject, + readFile, + readJson, + rmDist, + runCLI, + uniq, + listFiles, + fileExists, +} from '@nx/e2e-utils'; + +describe('Vite Plugin', () => { + let proj: string; + let originalEnv: string; + beforeAll(() => { + originalEnv = process.env.NX_ADD_PLUGINS; + process.env.NX_ADD_PLUGINS = 'false'; + proj = newProject({ + packages: ['@nx/react', '@nx/web'], + }); + }); + + afterAll(() => { + process.env.NX_ADD_PLUGINS = originalEnv; + cleanupProject(); + }); + + describe('Vite on Web apps', () => { + describe('set up new @nx/web app with --bundler=vite option', () => { + let myApp; + beforeEach(() => { + myApp = uniq('my-app'); + runCLI( + `generate @nx/web:app ${myApp} --bundler=vite --unitTestRunner=vitest --directory=${myApp}` + ); + }); + it('should build application', async () => { + runCLI(`build ${myApp}`); + expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); + const fileArray = listFiles(`dist/${myApp}/assets`); + const mainBundle = fileArray.find((file) => file.endsWith('.js')); + expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); + expect(fileExists(`dist/${myApp}/package.json`)).toBeFalsy(); + rmDist(); + }, 200_000); + + it('should build application with new package json generation', async () => { + runCLI(`build ${myApp} --generatePackageJson`); + expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); + const fileArray = listFiles(`dist/${myApp}/assets`); + const mainBundle = fileArray.find((file) => file.endsWith('.js')); + expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); + + const packageJson = readJson(`dist/${myApp}/package.json`); + expect(packageJson.name).toEqual(myApp); + expect(packageJson.version).toEqual('0.0.1'); + expect(packageJson.type).toEqual('module'); + rmDist(); + }, 200_000); + + it('should build application with existing package json generation', async () => { + createFile( + `${myApp}/package.json`, + JSON.stringify({ + name: 'my-existing-app', + version: '1.0.1', + scripts: { + start: 'node server.js', + }, + }) + ); + runCLI(`build ${myApp} --generatePackageJson`); + expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); + const fileArray = listFiles(`dist/${myApp}/assets`); + const mainBundle = fileArray.find((file) => file.endsWith('.js')); + expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); + + const packageJson = readJson(`dist/${myApp}/package.json`); + expect(packageJson.name).toEqual('my-existing-app'); + expect(packageJson.version).toEqual('1.0.1'); + expect(packageJson.type).toEqual('module'); + expect(packageJson.scripts).toEqual({ + start: 'node server.js', + }); + rmDist(); + }, 200_000); + + it('should build application without copying exisiting package json when generatePackageJson=false', async () => { + createFile( + `${myApp}/package.json`, + JSON.stringify({ + name: 'my-existing-app', + version: '1.0.1', + scripts: { + start: 'node server.js', + }, + }) + ); + runCLI(`build ${myApp} --generatePackageJson=false`); + expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); + const fileArray = listFiles(`dist/${myApp}/assets`); + const mainBundle = fileArray.find((file) => file.endsWith('.js')); + expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); + + expect(fileExists(`dist/${myApp}/package.json`)).toBe(false); + rmDist(); + }, 200_000); + }); + + 100_000; + }); +}); diff --git a/e2e/vite/src/vite-legacy.test.ts b/e2e/vite/src/vite-legacy.test.ts deleted file mode 100644 index 80d8c511a26ff..0000000000000 --- a/e2e/vite/src/vite-legacy.test.ts +++ /dev/null @@ -1,646 +0,0 @@ -import { names } from '@nx/devkit'; -import { - cleanupProject, - createFile, - directoryExists, - exists, - fileExists, - getPackageManagerCommand, - listFiles, - newProject, - readFile, - readJson, - removeFile, - rmDist, - runCLI, - runCommand, - runCommandUntil, - runCLIAsync, - tmpProjPath, - uniq, - updateFile, - updateJson, - checkFilesExist, - killProcessAndPorts, -} from '@nx/e2e-utils'; -import { join } from 'path'; -import { ChildProcess } from 'child_process'; - -describe('Vite Plugin', () => { - let proj: string; - let originalEnv: string; - beforeAll(() => { - originalEnv = process.env.NX_ADD_PLUGINS; - process.env.NX_ADD_PLUGINS = 'false'; - proj = newProject({ - packages: ['@nx/react', '@nx/web'], - }); - }); - - afterAll(() => { - process.env.NX_ADD_PLUGINS = originalEnv; - cleanupProject(); - }); - - describe('Vite on React apps', () => { - describe('set up new React app with --bundler=vite option', () => { - let myApp; - - beforeAll(() => { - myApp = uniq('my-app'); - runCLI( - `generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest` - ); - }); - - afterEach(() => { - rmDist(); - }); - - describe('build the app', () => { - it('should build application', async () => { - runCLI(`build ${myApp}`); - expect(readFile(`dist/${myApp}/favicon.ico`)).toBeDefined(); - expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); - }, 200_000); - - describe('when the app has static assets', () => { - beforeAll(() => { - createFile(`${myApp}/public/hello.md`, `# Hello World`); - }); - - afterAll(() => { - removeFile(`${myApp}/public/hello.md`); - }); - - it('should copy the assets to the output path', async () => { - runCLI(`build ${myApp}`); - expect(readFile(`dist/${myApp}/favicon.ico`)).toBeDefined(); - expect(readFile(`dist/${myApp}/hello.md`)).toBeDefined(); - expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); - }, 200_000); - }); - }); - - describe('test the app', () => { - it('should test application', async () => { - const result = runCLI(`test ${myApp}`); - expect(result).toContain('Successfully ran target test'); - }, 200_000); - - it('should generate a coverage file specified by the executor', async () => { - updateJson(`${myApp}/project.json`, (json) => { - json.targets.test.options.reportsDirectory = '../coverage/test-dir'; - return json; - }); - - const result = runCLI(`test ${myApp} --coverage`); - - checkFilesExist(`coverage/test-dir/index.html`); - expect(result).toContain('Coverage report'); - }, 200_000); - }); - }); - - describe('set up new React app with --bundler=vite option and use environments api', () => { - let myApp; - - beforeAll(() => { - myApp = uniq('my-app'); - runCLI( - `generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest` - ); - updateJson(`${myApp}/project.json`, (json) => { - json.targets.build.options.useEnvironmentsApi = true; - return json; - }); - updateFile( - `${myApp}/vite.config.ts`, - `/// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig({ - root: __dirname, - cacheDir: './node_modules/.vite/${myApp}', - server: { - port: 4200, - host: 'localhost', - }, - preview: { - port: 4300, - host: 'localhost', - }, - plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - builder: {}, - environments: { - ssr: { - build: { - rollupOptions: { - input: '${myApp}/src/main.server.tsx' - } - } - } - }, - build: { - outDir: './dist/${myApp}', - emptyOutDir: false, - reportCompressedSize: true, - commonjsOptions: { - transformMixedEsModules: true, - }, - }, - test: { - watch: false, - globals: true, - environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - coverage: { - reportsDirectory: './coverage/${myApp}', - provider: 'v8', - }, - }, -}); -` - ); - updateFile( - `${myApp}/src/main.server.tsx`, - `import React from 'react' -import ReactDOMServer from 'react-dom/server' -import App from './app/app'; - -export default async function render(_url: string, document: string) { - const html = ReactDOMServer.renderToString( - - - - ) - return document.replace('', html); -}` - ); - }); - - afterEach(() => { - rmDist(); - }); - - it('should build application', async () => { - runCLI(`build ${myApp}`); - expect(readFile(`dist/${myApp}/favicon.ico`)).toBeDefined(); - expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); - expect(readFile(`dist/${myApp}/main.server.mjs`)).toBeDefined(); - }, 200_000); - }); - }); - - describe('Vite on Web apps', () => { - describe('set up new @nx/web app with --bundler=vite option', () => { - let myApp; - beforeEach(() => { - myApp = uniq('my-app'); - runCLI( - `generate @nx/web:app ${myApp} --bundler=vite --unitTestRunner=vitest --directory=${myApp}` - ); - }); - it('should build application', async () => { - runCLI(`build ${myApp}`); - expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); - const fileArray = listFiles(`dist/${myApp}/assets`); - const mainBundle = fileArray.find((file) => file.endsWith('.js')); - expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); - expect(fileExists(`dist/${myApp}/package.json`)).toBeFalsy(); - rmDist(); - }, 200_000); - - it('should build application with new package json generation', async () => { - runCLI(`build ${myApp} --generatePackageJson`); - expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); - const fileArray = listFiles(`dist/${myApp}/assets`); - const mainBundle = fileArray.find((file) => file.endsWith('.js')); - expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); - - const packageJson = readJson(`dist/${myApp}/package.json`); - expect(packageJson.name).toEqual(myApp); - expect(packageJson.version).toEqual('0.0.1'); - expect(packageJson.type).toEqual('module'); - rmDist(); - }, 200_000); - - it('should build application with existing package json generation', async () => { - createFile( - `${myApp}/package.json`, - JSON.stringify({ - name: 'my-existing-app', - version: '1.0.1', - scripts: { - start: 'node server.js', - }, - }) - ); - runCLI(`build ${myApp} --generatePackageJson`); - expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); - const fileArray = listFiles(`dist/${myApp}/assets`); - const mainBundle = fileArray.find((file) => file.endsWith('.js')); - expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); - - const packageJson = readJson(`dist/${myApp}/package.json`); - expect(packageJson.name).toEqual('my-existing-app'); - expect(packageJson.version).toEqual('1.0.1'); - expect(packageJson.type).toEqual('module'); - expect(packageJson.scripts).toEqual({ - start: 'node server.js', - }); - rmDist(); - }, 200_000); - - it('should build application without copying exisiting package json when generatePackageJson=false', async () => { - createFile( - `${myApp}/package.json`, - JSON.stringify({ - name: 'my-existing-app', - version: '1.0.1', - scripts: { - start: 'node server.js', - }, - }) - ); - runCLI(`build ${myApp} --generatePackageJson=false`); - expect(readFile(`dist/${myApp}/index.html`)).toBeDefined(); - const fileArray = listFiles(`dist/${myApp}/assets`); - const mainBundle = fileArray.find((file) => file.endsWith('.js')); - expect(readFile(`dist/${myApp}/assets/${mainBundle}`)).toBeDefined(); - - expect(fileExists(`dist/${myApp}/package.json`)).toBe(false); - rmDist(); - }, 200_000); - }); - - 100_000; - }); - - describe('incremental building', () => { - const app = uniq('demo'); - const lib = uniq('my-lib'); - beforeAll(() => { - proj = newProject({ - name: uniq('vite-incr-build'), - packages: ['@nx/react'], - }); - runCLI( - `generate @nx/react:app ${app} --bundler=vite --unitTestRunner=vitest --no-interactive --directory=${app}` - ); - - // only this project will be directly used from dist - runCLI( - `generate @nx/react:lib ${lib}-buildable --unitTestRunner=none --bundler=vite --importPath="@acme/buildable" --no-interactive --directory=${lib}-buildable` - ); - - runCLI( - `generate @nx/react:lib ${lib} --unitTestRunner=none --bundler=none --importPath="@acme/non-buildable" --no-interactive --directory=${lib}` - ); - - // because the default js lib builds as cjs it cannot be loaded from dist - // so the paths plugin should always resolve to the libs source - runCLI( - `generate @nx/js:lib ${lib}-js --bundler=tsc --importPath="@acme/js-lib" --no-interactive --directory=${lib}-js` - ); - const buildableLibCmp = names(`${lib}-buildable`).className; - const nonBuildableLibCmp = names(lib).className; - const buildableJsLibFn = names(`${lib}-js`).propertyName; - - updateFile(`${app}/src/app/app.tsx`, () => { - return ` -import styles from './app.module.css'; -import NxWelcome from './nx-welcome'; -import { ${buildableLibCmp} } from '@acme/buildable'; -import { ${buildableJsLibFn} } from '@acme/js-lib'; -import { ${nonBuildableLibCmp} } from '@acme/non-buildable'; - -export function App() { - return ( -
- <${buildableLibCmp} /> - <${nonBuildableLibCmp} /> -

{${buildableJsLibFn}()}

- -
- ); -} -export default App; -`; - }); - }); - - afterAll(() => { - cleanupProject(); - }); - - it('should build app from libs source', () => { - const results = runCLI(`build ${app} --buildLibsFromSource=true`); - expect(results).toContain('Successfully ran target build for project'); - // this should be more modules than build from dist - expect(results).toContain('38 modules transformed'); - }); - - it('should build app from libs dist', () => { - const results = runCLI(`build ${app} --buildLibsFromSource=false`); - expect(results).toContain('Successfully ran target build for project'); - // this should be less modules than building from source - expect(results).toContain('36 modules transformed'); - }); - - it('should build app from libs without package.json in lib', () => { - removeFile(`${lib}-buildable/package.json`); - - const buildFromSourceResults = runCLI( - `build ${app} --buildLibsFromSource=true` - ); - expect(buildFromSourceResults).toContain( - 'Successfully ran target build for project' - ); - - const noBuildFromSourceResults = runCLI( - `build ${app} --buildLibsFromSource=false` - ); - expect(noBuildFromSourceResults).toContain( - 'Successfully ran target build for project' - ); - }); - }); - - describe('should be able to create libs that use vitest', () => { - describe('using default project configuration', () => { - const lib = uniq('my-default-lib'); - beforeAll(() => { - proj = newProject({ name: uniq('vite-proj'), packages: ['@nx/react'] }); - runCLI( - `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` - ); - }); - - it('should collect coverage when --coverage is set', () => { - const results = runCLI(`test ${lib} --coverage`); - expect(results).toContain(`Coverage report`); - }, 100_000); - - it('should be able to watch tests', async () => { - let cp: ChildProcess; - try { - cp = await runCommandUntil(`test ${lib} --watch`, (output) => { - return output.includes('Waiting for file changes...'); - }); - } catch (error) { - console.error(error); - } - - if (cp && cp.pid) { - await killProcessAndPorts(cp.pid); - } - }, 100_000); - - it('should not watch tests when --watch is not set', async () => { - const results = runCLI(`test ${lib}`); - - expect(results).not.toContain('Waiting for file changes...'); - - expect(results).toContain( - `Successfully ran target test for project ${lib}` - ); - }, 100_000); - }); - - describe('using custom project configuration', () => { - const lib = uniq('my-custom-lib'); - beforeEach(() => { - proj = newProject({ name: uniq('vite-proj'), packages: ['@nx/react'] }); - }); - - it('should be able to run tests', async () => { - runCLI( - `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` - ); - expect(exists(tmpProjPath(`libs/${lib}/vite.config.ts`))).toBeTruthy(); - - const result = await runCLIAsync(`test ${lib}`); - expect(result.combinedOutput).toContain( - `Successfully ran target test for project ${lib}` - ); - - const nestedResults = await runCLIAsync(`test ${lib} --skip-nx-cache`, { - cwd: `${tmpProjPath()}/libs/${lib}`, - }); - expect(nestedResults.combinedOutput).toContain( - `Successfully ran target test for project ${lib}` - ); - }, 100_000); - - it('should collect coverage', () => { - runCLI( - `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` - ); - updateFile(`libs/${lib}/vite.config.ts`, () => { - return `/// - import { defineConfig } from 'vite'; - import react from '@vitejs/plugin-react'; - import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; - - export default defineConfig({ - root: __dirname, - cacheDir: '../../node_modules/.vite/libs/${lib}', - plugins: [react(), nxViteTsPaths()], - test: { - globals: true, - cache: { - dir: '../../node_modules/.vitest', - }, - environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../coverage/libs/${lib}', - provider: 'v8', - enabled: true, - thresholds: { - lines: 100, - statements: 100, - functions: 100, - branches: 1000, - } - }, - }, - }); - `; - }); - - const coverageDir = `${tmpProjPath()}/coverage/libs/${lib}`; - - const results = runCLI(`test ${lib} --coverage`, { - silenceError: true, - }); - expect(results).toContain( - `Running target test for project ${lib} failed` - ); - expect(results).toContain(`ERROR: Coverage`); - expect(directoryExists(coverageDir)).toBeTruthy(); - }, 100_000); - - it('should not delete the project directory when coverage is enabled', async () => { - // when coverage is enabled in the vite.config.ts but reportsDirectory is removed - // from the @nx/vite:test executor options, vite will delete the project root directory - runCLI( - `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest` - ); - updateFile(`libs/${lib}/vite.config.ts`, () => { - return `import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; - - -export default defineConfig({ - server: { - port: 4200, - host: 'localhost', - }, - plugins: [ - react(), - nxViteTsPaths() - ], - test: { - globals: true, - cache: { - dir: './node_modules/.vitest', - }, - environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['junit'], - outputFile: 'junit.xml', - coverage: { - enabled: true, - reportsDirectory: 'coverage', - } - }, -}); -`; - }); - updateJson(join('libs', lib, 'project.json'), (config) => { - delete config.targets.test.options.reportsDirectory; - return config; - }); - - const projectRoot = `${tmpProjPath()}/libs/${lib}`; - - const results = runCLI(`test ${lib}`, { - env: { - CI: 'true', // prevent vitest from watching for file changes and making the process hang - }, - }); - - expect(directoryExists(projectRoot)).toBeTruthy(); - expect(results).toContain( - `Successfully ran target test for project ${lib}` - ); - expect(results).toContain(`JUNIT report written`); - }, 100_000); - - it('should be able to run tests with inSourceTests set to true', async () => { - runCLI( - `generate @nx/react:lib ${lib} --directory=libs/${lib} --unitTestRunner=vitest --inSourceTests` - ); - expect( - exists(tmpProjPath(`libs/${lib}/src/lib/${lib}.spec.tsx`)) - ).toBeFalsy(); - - updateFile(`libs/${lib}/src/lib/${lib}.tsx`, (content) => { - content += ` - if (import.meta.vitest) { - const { expect, it } = import.meta.vitest; - it('should be successful', () => { - expect(1 + 1).toBe(2); - }); - } - `; - return content; - }); - - const result = await runCLIAsync(`test ${lib}`); - expect(result.combinedOutput).toContain(`1 passed`); - }, 100_000); - }); - }); - - describe('ESM-only apps', () => { - beforeAll(() => { - newProject({ - packages: ['@nx/react'], - }); - }); - - it('should support ESM-only plugins in vite.config.ts for root apps (#NXP-168)', () => { - // ESM-only plugin to test with - updateFile( - 'foo/package.json', - JSON.stringify({ - name: '@acme/foo', - type: 'module', - version: '1.0.0', - main: 'index.js', - }) - ); - updateFile( - 'foo/index.js', - ` - export default function fooPlugin() { - return { - name: 'foo-plugin', - configResolved() { - console.log('Foo plugin'); - } - } - }` - ); - updateJson('package.json', (json) => { - json.devDependencies['@acme/foo'] = 'file:./foo'; - return json; - }); - runCommand(getPackageManagerCommand().install); - - const rootApp = uniq('root'); - runCLI( - `generate @nx/react:app ${rootApp} --rootProject --bundler=vite --unitTestRunner=none --e2eTestRunner=none --style=css --no-interactive` - ); - updateJson(`package.json`, (json) => { - // This allows us to use ESM-only packages in vite.config.ts. - json.type = 'module'; - return json; - }); - updateFile( - `vite.config.ts`, - ` - import fooPlugin from '@acme/foo'; - import { defineConfig } from 'vite'; - import react from '@vitejs/plugin-react'; - import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; - - export default defineConfig({ - cacheDir: '../../node_modules/.vite/root-app', - server: { - port: 4200, - host: 'localhost', - }, - plugins: [react(), nxViteTsPaths(), fooPlugin()], - });` - ); - - runCLI(`build ${rootApp}`); - - checkFilesExist(`dist/${rootApp}/index.html`); - }); - }); -}); diff --git a/e2e/web/src/web-legacy-build-options.test.ts b/e2e/web/src/web-legacy-build-options.test.ts new file mode 100644 index 0000000000000..e01603113f1c9 --- /dev/null +++ b/e2e/web/src/web-legacy-build-options.test.ts @@ -0,0 +1,96 @@ +import { + createFile, + newProject, + readFile, + runCLI, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { join } from 'path'; + +describe('Build Options (legacy) ', () => { + it('should inject/bundle external scripts and styles', async () => { + newProject(); + + const appName = uniq('app'); + + runCLI( + `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive`, + { + env: { + NX_ADD_PLUGINS: 'false', + }, + } + ); + + const srcPath = `apps/${appName}/src`; + const fooCss = `${srcPath}/foo.css`; + const barCss = `${srcPath}/bar.css`; + const fooJs = `${srcPath}/foo.js`; + const barJs = `${srcPath}/bar.js`; + const fooCssContent = `/* ${uniq('foo')} */`; + const barCssContent = `/* ${uniq('bar')} */`; + const fooJsContent = `/* ${uniq('foo')} */`; + const barJsContent = `/* ${uniq('bar')} */`; + + createFile(fooCss); + createFile(barCss); + createFile(fooJs); + createFile(barJs); + + // createFile could not create a file with content + updateFile(fooCss, fooCssContent); + updateFile(barCss, barCssContent); + updateFile(fooJs, fooJsContent); + updateFile(barJs, barJsContent); + + const barScriptsBundleName = 'bar-scripts'; + const barStylesBundleName = 'bar-styles'; + + updateJson(join('apps', appName, 'project.json'), (config) => { + const buildOptions = config.targets.build.options; + + buildOptions.scripts = [ + { + input: fooJs, + inject: true, + }, + { + input: barJs, + inject: false, + bundleName: barScriptsBundleName, + }, + ]; + + buildOptions.styles = [ + { + input: fooCss, + inject: true, + }, + { + input: barCss, + inject: false, + bundleName: barStylesBundleName, + }, + ]; + return config; + }); + + runCLI(`build ${appName} --optimization=false --outputHashing=none`); + + const distPath = `dist/apps/${appName}`; + const scripts = readFile(`${distPath}/scripts.js`); + const styles = readFile(`${distPath}/styles.css`); + const barScripts = readFile(`${distPath}/${barScriptsBundleName}.js`); + const barStyles = readFile(`${distPath}/${barStylesBundleName}.css`); + + expect(scripts).toContain(fooJsContent); + expect(scripts).not.toContain(barJsContent); + expect(barScripts).toContain(barJsContent); + + expect(styles).toContain(fooCssContent); + expect(styles).not.toContain(barCssContent); + expect(barStyles).toContain(barCssContent); + }); +}); diff --git a/e2e/web/src/web-legacy-components.test.ts b/e2e/web/src/web-legacy-components.test.ts new file mode 100644 index 0000000000000..059fe48673074 --- /dev/null +++ b/e2e/web/src/web-legacy-components.test.ts @@ -0,0 +1,119 @@ +import { + checkFilesDoNotExist, + checkFilesExist, + createFile, + readFile, + rmDist, + runCLI, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { join } from 'path'; +import { setupWebLegacyTest, cleanupWebLegacyTest } from './web-legacy-setup'; + +describe('Web Components Applications (legacy)', () => { + beforeEach(() => setupWebLegacyTest()); + afterEach(() => cleanupWebLegacyTest()); + + it('should remove previous output before building', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI( + `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive --compiler swc`, + { + env: { + NX_ADD_PLUGINS: 'false', + }, + } + ); + runCLI( + `generate @nx/react:lib libs/${libName} --bundler=rollup --no-interactive --compiler swc --unitTestRunner=jest`, + { + env: { + NX_ADD_PLUGINS: 'false', + }, + } + ); + + createFile(`dist/apps/${appName}/_should_remove.txt`); + createFile(`dist/libs/${libName}/_should_remove.txt`); + createFile(`dist/apps/_should_not_remove.txt`); + checkFilesExist( + `dist/apps/${appName}/_should_remove.txt`, + `dist/apps/_should_not_remove.txt` + ); + runCLI(`build ${appName} --outputHashing none`); + runCLI(`build ${libName}`); + checkFilesDoNotExist( + `dist/apps/${appName}/_should_remove.txt`, + `dist/libs/${libName}/_should_remove.txt` + ); + + // Asset that React runtime is imported + expect(readFile(`dist/libs/${libName}/index.esm.js`)).toMatch( + /react\/jsx-runtime/ + ); + }, 120000); + + it('should support custom webpackConfig option', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive`, + { + env: { + NX_ADD_PLUGINS: 'false', + }, + } + ); + + updateJson(join('apps', appName, 'project.json'), (config) => { + config.targets.build.options.webpackConfig = `apps/${appName}/webpack.config.js`; + return config; + }); + + // Return sync function + updateFile( + `apps/${appName}/webpack.config.js`, + ` + const { composePlugins, withNx, withWeb } = require('@nx/webpack'); + module.exports = composePlugins(withNx(), withWeb(), (config, context) => { + return config; + }); + ` + ); + runCLI(`build ${appName} --outputHashing=none`); + checkFilesExist(`dist/apps/${appName}/main.js`); + + rmDist(); + + // Return async function + updateFile( + `apps/${appName}/webpack.config.js`, + ` + const { composePlugins, withNx, withWeb } = require('@nx/webpack'); + module.exports = composePlugins(withNx(), withWeb(), async (config, context) => { + return config; + }); + ` + ); + runCLI(`build ${appName} --outputHashing=none`); + checkFilesExist(`dist/apps/${appName}/main.js`); + + rmDist(); + + // Return promise of function + updateFile( + `apps/${appName}/webpack.config.js`, + ` + const { composePlugins, withNx, withWeb } = require('@nx/webpack'); + module.exports = composePlugins(withNx(), withWeb(), Promise.resolve((config, context) => { + return config; + })); + ` + ); + runCLI(`build ${appName} --outputHashing=none`); + checkFilesExist(`dist/apps/${appName}/main.js`); + }, 100000); +}); diff --git a/e2e/web/src/web-legacy-index-html-interpolation.test.ts b/e2e/web/src/web-legacy-index-html-interpolation.test.ts new file mode 100644 index 0000000000000..5738667cd8828 --- /dev/null +++ b/e2e/web/src/web-legacy-index-html-interpolation.test.ts @@ -0,0 +1,73 @@ +import { + createFile, + newProject, + cleanupProject, + readFile, + runCLI, + uniq, + updateFile, + updateJson, +} from '@nx/e2e-utils'; +import { join } from 'path'; + +describe('index.html interpolation (legacy)', () => { + beforeAll(() => newProject()); + afterAll(() => cleanupProject()); + + test('should interpolate environment variables', async () => { + const appName = uniq('app'); + + runCLI( + `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive`, + { + env: { + NX_ADD_PLUGINS: 'false', + }, + } + ); + + const srcPath = `apps/${appName}/src`; + const indexPath = `${srcPath}/index.html`; + const indexContent = ` + + + + BestReactApp + + + + + +
+
Nx Variable: %NX_PUBLIC_VARIABLE%
+
Some other variable: %SOME_OTHER_VARIABLE%
+
Deploy Url: %DEPLOY_URL%
+ + +`; + const envFilePath = `apps/${appName}/.env`; + const envFileContents = ` + NX_PUBLIC_VARIABLE=foo + SOME_OTHER_VARIABLE=bar + }`; + + createFile(envFilePath); + + // createFile could not create a file with content + updateFile(envFilePath, envFileContents); + updateFile(indexPath, indexContent); + + updateJson(join('apps', appName, 'project.json'), (config) => { + const buildOptions = config.targets.build.options; + buildOptions.deployUrl = 'baz'; + return config; + }); + + runCLI(`build ${appName}`); + + const distPath = `dist/apps/${appName}`; + const resultIndexContents = readFile(`${distPath}/index.html`); + + expect(resultIndexContents).toMatch(/Nx Variable: foo/); + }); +}); diff --git a/e2e/web/src/web-legacy-setup.ts b/e2e/web/src/web-legacy-setup.ts new file mode 100644 index 0000000000000..6ca8422bbe3f6 --- /dev/null +++ b/e2e/web/src/web-legacy-setup.ts @@ -0,0 +1,9 @@ +import { cleanupProject, newProject } from '@nx/e2e-utils'; + +export function setupWebLegacyTest() { + newProject({ packages: ['@nx/web', '@nx/react'] }); +} + +export function cleanupWebLegacyTest() { + cleanupProject(); +} diff --git a/e2e/web/src/web-legacy.test.ts b/e2e/web/src/web-legacy.test.ts deleted file mode 100644 index 2299567c26120..0000000000000 --- a/e2e/web/src/web-legacy.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { - checkFilesDoNotExist, - checkFilesExist, - cleanupProject, - createFile, - newProject, - readFile, - rmDist, - runCLI, - uniq, - updateFile, - updateJson, -} from '@nx/e2e-utils'; -import { join } from 'path'; - -describe('Web Components Applications (legacy)', () => { - beforeEach(() => newProject({ packages: ['@nx/web', '@nx/react'] })); - afterEach(() => cleanupProject()); - - it('should remove previous output before building', async () => { - const appName = uniq('app'); - const libName = uniq('lib'); - - runCLI( - `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive --compiler swc`, - { - env: { - NX_ADD_PLUGINS: 'false', - }, - } - ); - runCLI( - `generate @nx/react:lib libs/${libName} --bundler=rollup --no-interactive --compiler swc --unitTestRunner=jest`, - { - env: { - NX_ADD_PLUGINS: 'false', - }, - } - ); - - createFile(`dist/apps/${appName}/_should_remove.txt`); - createFile(`dist/libs/${libName}/_should_remove.txt`); - createFile(`dist/apps/_should_not_remove.txt`); - checkFilesExist( - `dist/apps/${appName}/_should_remove.txt`, - `dist/apps/_should_not_remove.txt` - ); - runCLI(`build ${appName} --outputHashing none`); - runCLI(`build ${libName}`); - checkFilesDoNotExist( - `dist/apps/${appName}/_should_remove.txt`, - `dist/libs/${libName}/_should_remove.txt` - ); - - // Asset that React runtime is imported - expect(readFile(`dist/libs/${libName}/index.esm.js`)).toMatch( - /react\/jsx-runtime/ - ); - }, 120000); - - it('should support custom webpackConfig option', async () => { - const appName = uniq('app'); - runCLI( - `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive`, - { - env: { - NX_ADD_PLUGINS: 'false', - }, - } - ); - - updateJson(join('apps', appName, 'project.json'), (config) => { - config.targets.build.options.webpackConfig = `apps/${appName}/webpack.config.js`; - return config; - }); - - // Return sync function - updateFile( - `apps/${appName}/webpack.config.js`, - ` - const { composePlugins, withNx, withWeb } = require('@nx/webpack'); - module.exports = composePlugins(withNx(), withWeb(), (config, context) => { - return config; - }); - ` - ); - runCLI(`build ${appName} --outputHashing=none`); - checkFilesExist(`dist/apps/${appName}/main.js`); - - rmDist(); - - // Return async function - updateFile( - `apps/${appName}/webpack.config.js`, - ` - const { composePlugins, withNx, withWeb } = require('@nx/webpack'); - module.exports = composePlugins(withNx(), withWeb(), async (config, context) => { - return config; - }); - ` - ); - runCLI(`build ${appName} --outputHashing=none`); - checkFilesExist(`dist/apps/${appName}/main.js`); - - rmDist(); - - // Return promise of function - updateFile( - `apps/${appName}/webpack.config.js`, - ` - const { composePlugins, withNx, withWeb } = require('@nx/webpack'); - module.exports = composePlugins(withNx(), withWeb(), Promise.resolve((config, context) => { - return config; - })); - ` - ); - runCLI(`build ${appName} --outputHashing=none`); - checkFilesExist(`dist/apps/${appName}/main.js`); - }, 100000); -}); - -describe('Build Options (legacy) ', () => { - it('should inject/bundle external scripts and styles', async () => { - newProject(); - - const appName = uniq('app'); - - runCLI( - `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive`, - { - env: { - NX_ADD_PLUGINS: 'false', - }, - } - ); - - const srcPath = `apps/${appName}/src`; - const fooCss = `${srcPath}/foo.css`; - const barCss = `${srcPath}/bar.css`; - const fooJs = `${srcPath}/foo.js`; - const barJs = `${srcPath}/bar.js`; - const fooCssContent = `/* ${uniq('foo')} */`; - const barCssContent = `/* ${uniq('bar')} */`; - const fooJsContent = `/* ${uniq('foo')} */`; - const barJsContent = `/* ${uniq('bar')} */`; - - createFile(fooCss); - createFile(barCss); - createFile(fooJs); - createFile(barJs); - - // createFile could not create a file with content - updateFile(fooCss, fooCssContent); - updateFile(barCss, barCssContent); - updateFile(fooJs, fooJsContent); - updateFile(barJs, barJsContent); - - const barScriptsBundleName = 'bar-scripts'; - const barStylesBundleName = 'bar-styles'; - - updateJson(join('apps', appName, 'project.json'), (config) => { - const buildOptions = config.targets.build.options; - - buildOptions.scripts = [ - { - input: fooJs, - inject: true, - }, - { - input: barJs, - inject: false, - bundleName: barScriptsBundleName, - }, - ]; - - buildOptions.styles = [ - { - input: fooCss, - inject: true, - }, - { - input: barCss, - inject: false, - bundleName: barStylesBundleName, - }, - ]; - return config; - }); - - runCLI(`build ${appName} --optimization=false --outputHashing=none`); - - const distPath = `dist/apps/${appName}`; - const scripts = readFile(`${distPath}/scripts.js`); - const styles = readFile(`${distPath}/styles.css`); - const barScripts = readFile(`${distPath}/${barScriptsBundleName}.js`); - const barStyles = readFile(`${distPath}/${barStylesBundleName}.css`); - - expect(scripts).toContain(fooJsContent); - expect(scripts).not.toContain(barJsContent); - expect(barScripts).toContain(barJsContent); - - expect(styles).toContain(fooCssContent); - expect(styles).not.toContain(barCssContent); - expect(barStyles).toContain(barCssContent); - }); -}); - -describe('index.html interpolation (legacy)', () => { - beforeAll(() => newProject()); - afterAll(() => cleanupProject()); - - test('should interpolate environment variables', async () => { - const appName = uniq('app'); - - runCLI( - `generate @nx/web:app apps/${appName} --bundler=webpack --no-interactive`, - { - env: { - NX_ADD_PLUGINS: 'false', - }, - } - ); - - const srcPath = `apps/${appName}/src`; - const indexPath = `${srcPath}/index.html`; - const indexContent = ` - - - - BestReactApp - - - - - -
-
Nx Variable: %NX_PUBLIC_VARIABLE%
-
Some other variable: %SOME_OTHER_VARIABLE%
-
Deploy Url: %DEPLOY_URL%
- - -`; - const envFilePath = `apps/${appName}/.env`; - const envFileContents = ` - NX_PUBLIC_VARIABLE=foo - SOME_OTHER_VARIABLE=bar - }`; - - createFile(envFilePath); - - // createFile could not create a file with content - updateFile(envFilePath, envFileContents); - updateFile(indexPath, indexContent); - - updateJson(join('apps', appName, 'project.json'), (config) => { - const buildOptions = config.targets.build.options; - buildOptions.deployUrl = 'baz'; - return config; - }); - - runCLI(`build ${appName}`); - - const distPath = `dist/apps/${appName}`; - const resultIndexContents = readFile(`${distPath}/index.html`); - - expect(resultIndexContents).toMatch(/Nx Variable: foo/); - }); -}); diff --git a/nx.json b/nx.json index 0c6ca02ce7bf2..f7f2298df0769 100644 --- a/nx.json +++ b/nx.json @@ -330,7 +330,7 @@ } ], "parallel": 1, - "bust": 3, + "bust": 12, "defaultBase": "master", "sync": { "applyChanges": true