import { checkFilesDoNotExist, checkFilesExist, cleanupProject, getSelectedPackageManager, killPort, killProcessAndPorts, newProject, readFile, reservePort, runCLI, runCommandUntil, uniq, updateFile, updateJson, } from '@nx/e2e-utils'; import { join } from 'path'; import { stripVTControlCharacters } from 'node:util'; describe('Node Applications + webpack', () => { let proj: string; const selectedPm = getSelectedPackageManager(); // TODO(nicholas): Look into how this can work with npm instead of forcing pnpm. beforeAll(() => { proj = newProject({ packages: ['@nx/node', '@nx/nest', '@nx/webpack'], // npm has resolution for ajv some packages require ajv6 and some require ajv8 and npm resolves it to ajv6 (Error: Cannot find module 'ajv/dist/compile/codegen') // - ajv@6 (fork-ts-checker-webpack-plugin, terser-webpack-plugin, webpack) // - ajv@8 (babel-loader) // Solution is to use pnpm or run fix it via npm ex.(npm dedupe --force) packageManager: selectedPm === 'npm' ? 'pnpm' : selectedPm, }); }); afterAll(() => cleanupProject()); function addLibImport(appName: string, libName: string, importPath?: string) { const content = readFile(`apps/${appName}/src/main.ts`); if (importPath) { updateFile( `apps/${appName}/src/main.ts`, ` import { ${libName} } from '${importPath}'; ${content} console.log(${libName}()); ` ); } else { updateFile( `apps/${appName}/src/main.ts`, ` import { ${libName} } from '@${proj}/${libName}'; ${content} console.log(${libName}()); ` ); } } async function runE2eTests(appName: string, port: number) { process.env.PORT = `${port}`; const result = runCLI(`e2e ${appName}-e2e --verbose`); expect(result).toContain('Setting up...'); expect(result).toContain('Tearing down..'); expect(result).toContain('Successfully ran target e2e'); await killPort(port); process.env.PORT = ''; } describe('frameworks', () => { const testLib1 = uniq('test1'); const testLib2 = uniq('test2'); const expressApp = uniq('expressapp'); const fastifyApp = uniq('fastifyapp'); const koaApp = uniq('koaapp'); const nestApp = uniq('nest'); let expressPort: number; let fastifyPort: number; let koaPort: number; let nestPort: number; beforeAll(async () => { expressPort = await reservePort(); fastifyPort = await reservePort(); koaPort = await reservePort(); nestPort = await reservePort(); runCLI( `generate @nx/node:lib libs/${testLib1} --linter=eslint --unitTestRunner=jest --buildable=false` ); runCLI( `generate @nx/node:lib libs/${testLib2} --importPath=@acme/test2 --linter=eslint --unitTestRunner=jest --buildable=false` ); runCLI( `generate @nx/node:app apps/${expressApp} --framework=express --port=${expressPort} --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` ); runCLI( `generate @nx/node:app apps/${fastifyApp} --framework=fastify --port=${fastifyPort} --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` ); runCLI( `generate @nx/node:app apps/${koaApp} --framework=koa --port=${koaPort} --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` ); runCLI( `generate @nx/node:app apps/${nestApp} --framework=nest --port=${nestPort} --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest --verbose` ); addLibImport(expressApp, testLib1); addLibImport(expressApp, testLib2, '@acme/test2'); addLibImport(fastifyApp, testLib1); addLibImport(fastifyApp, testLib2, '@acme/test2'); addLibImport(koaApp, testLib1); addLibImport(koaApp, testLib2, '@acme/test2'); addLibImport(nestApp, testLib1); addLibImport(nestApp, testLib2, '@acme/test2'); }); it('should generate an app defaults using webpack or esbuild', async () => { // Use esbuild by default checkFilesDoNotExist(`apps/${expressApp}/webpack.config.js`); checkFilesDoNotExist(`apps/${fastifyApp}/webpack.config.js`); checkFilesDoNotExist(`apps/${koaApp}/webpack.config.js`); // Uses only webpack checkFilesExist(`apps/${nestApp}/webpack.config.js`); expect(() => runCLI(`lint ${expressApp}`)).not.toThrow(); expect(() => runCLI(`lint ${fastifyApp}`)).not.toThrow(); expect(() => runCLI(`lint ${koaApp}`)).not.toThrow(); expect(() => runCLI(`lint ${nestApp}`)).not.toThrow(); expect(() => runCLI(`lint ${expressApp}-e2e`)).not.toThrow(); expect(() => runCLI(`lint ${fastifyApp}-e2e`)).not.toThrow(); expect(() => runCLI(`lint ${koaApp}-e2e`)).not.toThrow(); expect(() => runCLI(`lint ${nestApp}-e2e`)).not.toThrow(); // Only Fastify generates with unit tests since it supports them without additional libraries. expect(() => runCLI(`test ${fastifyApp}`)).not.toThrow(); // https://github.com/nrwl/nx/issues/16601 const nestMainContent = readFile(`apps/${nestApp}/src/main.ts`); updateFile( `apps/${nestApp}/src/main.ts`, ` ${nestMainContent} // Make sure this is not replaced during build time console.log('env: ' + process.env['NODE_ENV']); ` ); runCLI(`build ${nestApp}`); expect(readFile(`dist/apps/${nestApp}/main.js`)).toContain( `'env: ' + process.env['NODE_ENV']` ); }, 300_000); it('should e2e test express app', async () => { await runE2eTests(expressApp, expressPort); }); it('should e2e test fastify app', async () => { await runE2eTests(fastifyApp, fastifyPort); }); it('should e2e test koa app', async () => { await runE2eTests(koaApp, koaPort); }); it('should e2e test nest app', async () => { await runE2eTests(nestApp, nestPort); }); }); it('should generate a Dockerfile', async () => { const expressApp = 'docker-express-app'; // needs to be consistent for the Dockerfile snapshot runCLI( `generate @nx/node:app apps/${expressApp} --framework=express --docker --no-interactive --linter=eslint --unitTestRunner=jest` ); checkFilesExist(`apps/${expressApp}/Dockerfile`); }, 300_000); it('should support waitUntilTargets for serve target', async () => { const nodeApp1 = uniq('nodeapp1'); const nodeApp2 = uniq('nodeapp2'); const nodeApp1Port = await reservePort(); const nodeApp2Port = await reservePort(); // Set ports to avoid conflicts with other tests that might run in parallel runCLI( `generate @nx/node:app apps/${nodeApp1} --framework=none --no-interactive --port=${nodeApp1Port} --linter=eslint --unitTestRunner=jest` ); runCLI( `generate @nx/node:app apps/${nodeApp2} --framework=none --no-interactive --port=${nodeApp2Port} --linter=eslint --unitTestRunner=jest` ); updateJson(join('apps', nodeApp1, 'project.json'), (config) => { config.targets.serve.options.waitUntilTargets = [`${nodeApp2}:build`]; return config; }); await runCommandUntil(`serve ${nodeApp1} `, (output) => output.includes('Hello World') ); checkFilesExist(`dist/apps/${nodeApp1}/main.js`); checkFilesExist(`dist/apps/${nodeApp2}/main.js`); }, 300_000); it('should wait for the previous process to exit on watch restart so the port is released', async () => { const app = uniq('slowshutdown'); const port = await reservePort(); runCLI( `generate @nx/node:app apps/${app} --framework=none --no-interactive --port=${port} --linter=eslint --unitTestRunner=jest` ); // Server with a slow graceful shutdown that holds the port after SIGTERM. // A watch-mode restart must wait for it to exit, otherwise the new // process fails with EADDRINUSE. The 8s hold exceeds the 5s kill grace // period, so the executor force-kills it rather than waiting forever. updateFile( `apps/${app}/src/main.ts`, ` import { createServer } from 'http'; const server = createServer((req, res) => res.end('ok')); server.listen(${port}, () => console.log('Listening on ${port} pid ' + process.pid)); process.on('SIGTERM', () => { setTimeout(() => process.exit(0), 8000); }); ` ); const p = await runCommandUntil(`serve ${app}`, (output) => output.includes(`Listening on ${port}`) ); try { const restarted = new Promise((resolve, reject) => { let output = ''; const timeoutId = setTimeout(() => { reject(new Error(`Server did not restart. Output:\n${output}`)); }, 120_000); const check = (chunk: Buffer) => { output += stripVTControlCharacters(chunk.toString()); if (output.includes('EADDRINUSE')) { clearTimeout(timeoutId); reject(new Error(`Restarted server hit EADDRINUSE:\n${output}`)); } else if (output.includes(`Listening on ${port}`)) { clearTimeout(timeoutId); resolve(); } }; p.stdout?.on('data', check); p.stderr?.on('data', check); }); // Trigger a watch rebuild and restart. updateFile(`apps/${app}/src/main.ts`, (content) => content.replace(`res.end('ok')`, `res.end('ok v2')`) ); await restarted; } finally { await killProcessAndPorts(p.pid, port); } }, 300_000); });