diff --git a/e2e/node/src/node-server.test.ts b/e2e/node/src/node-server.test.ts index 43bb152768..f5eb58552d 100644 --- a/e2e/node/src/node-server.test.ts +++ b/e2e/node/src/node-server.test.ts @@ -58,15 +58,11 @@ describe('Node Applications + webpack', () => { async function runE2eTests(appName: string, port: number = 5000) { process.env.PORT = `${port}`; - const childProcess = await runCommandUntil(`serve ${appName}`, (output) => { - return output.includes(`http://localhost:${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 promisifiedTreeKill(childProcess.pid, 'SIGKILL'); await killPort(port); process.env.PORT = ''; } @@ -96,7 +92,7 @@ describe('Node Applications + webpack', () => { `generate @nx/node:app apps/${koaApp} --framework=koa --port=7002 --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` ); runCLI( - `generate @nx/node:app apps/${nestApp} --framework=nest --port=7003 --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` + `generate @nx/node:app apps/${nestApp} --framework=nest --port=7003 --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest --verbose` ); addLibImport(expressApp, testLib1); diff --git a/e2e/node/src/node.test.ts b/e2e/node/src/node.test.ts index a9d52b3d44..8a850f9c69 100644 --- a/e2e/node/src/node.test.ts +++ b/e2e/node/src/node.test.ts @@ -337,13 +337,12 @@ module.exports = { }, } ); + await killPorts(port); + await promisifiedTreeKill(p.pid, 'SIGKILL'); const e2eRsult = await runCLIAsync(`e2e ${nestapp}-e2e`); expect(e2eRsult.combinedOutput).toContain('Test Suites: 1 passed, 1 total'); - - await killPorts(port); - await promisifiedTreeKill(p.pid, 'SIGKILL'); }, 120000); it('should generate a nest application with docker', async () => { diff --git a/packages/nest/src/generators/application/application.spec.ts b/packages/nest/src/generators/application/application.spec.ts index d4966938c3..40cbebfe05 100644 --- a/packages/nest/src/generators/application/application.spec.ts +++ b/packages/nest/src/generators/application/application.spec.ts @@ -444,6 +444,7 @@ describe('application generator', () => { "e2e": { "dependsOn": [ "@proj/myapp:build", + "@proj/myapp:serve", ], "executor": "@nx/jest:jest", "options": { diff --git a/packages/node/package.json b/packages/node/package.json index 8483e603d6..e74b754e8a 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -35,7 +35,9 @@ "@nx/devkit": "file:../devkit", "@nx/jest": "file:../jest", "@nx/js": "file:../js", - "@nx/eslint": "file:../eslint" + "@nx/eslint": "file:../eslint", + "tcp-port-used": "^1.0.2", + "kill-port": "^1.6.1" }, "publishConfig": { "access": "public" diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index e390e1f0d1..2821bf52a3 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -934,6 +934,7 @@ describe('app', () => { "e2e": { "dependsOn": [ "@proj/myapp:build", + "@proj/myapp:serve", ], "executor": "@nx/jest:jest", "options": { diff --git a/packages/node/src/generators/e2e-project/e2e-project.ts b/packages/node/src/generators/e2e-project/e2e-project.ts index e5a35e8227..0680a2ae9c 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.ts @@ -76,7 +76,7 @@ export async function e2eProjectGeneratorInternal( jestConfig: `${options.e2eProjectRoot}/jest.config.ts`, passWithNoTests: true, }, - dependsOn: [`${options.project}:build`], + dependsOn: [`${options.project}:build`, `${options.project}:serve`], }, }, }; @@ -93,7 +93,7 @@ export async function e2eProjectGeneratorInternal( jestConfig: `${options.e2eProjectRoot}/jest.config.ts`, passWithNoTests: true, }, - dependsOn: [`${options.project}:build`], + dependsOn: [`${options.project}:build`, `${options.project}:serve`], }, }, }); diff --git a/packages/node/src/generators/e2e-project/files/server/common/src/support/global-setup.ts__tmpl__ b/packages/node/src/generators/e2e-project/files/server/common/src/support/global-setup.ts__tmpl__ index d18e0f2947..04c42f84ea 100644 --- a/packages/node/src/generators/e2e-project/files/server/common/src/support/global-setup.ts__tmpl__ +++ b/packages/node/src/generators/e2e-project/files/server/common/src/support/global-setup.ts__tmpl__ @@ -1,3 +1,5 @@ +import { waitForPortOpen } from '@nx/node/utils'; + /* eslint-disable */ var __TEARDOWN_MESSAGE__: string; @@ -5,6 +7,10 @@ module.exports = async function() { // Start services that that the app needs to run (e.g. database, docker-compose, etc.). console.log('\nSetting up...\n'); + const host = process.env.HOST ?? 'localhost'; + const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>; + await waitForPortOpen(port, { host }); + // Hint: Use `globalThis` to pass variables to global teardown. globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; }; diff --git a/packages/node/src/generators/e2e-project/files/server/common/src/support/global-teardown.ts__tmpl__ b/packages/node/src/generators/e2e-project/files/server/common/src/support/global-teardown.ts__tmpl__ index 67746cebd3..a72a86d61a 100644 --- a/packages/node/src/generators/e2e-project/files/server/common/src/support/global-teardown.ts__tmpl__ +++ b/packages/node/src/generators/e2e-project/files/server/common/src/support/global-teardown.ts__tmpl__ @@ -1,7 +1,10 @@ +import { killPort } from '@nx/node/utils'; /* eslint-disable */ module.exports = async function() { // Put clean up logic here (e.g. stopping services, docker-compose, etc.). // Hint: `globalThis` is shared between setup and teardown. + const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>; + await killPort(port); console.log(globalThis.__TEARDOWN_MESSAGE__); }; diff --git a/packages/node/src/utils/kill-port.ts b/packages/node/src/utils/kill-port.ts new file mode 100644 index 0000000000..d9e66a0e41 --- /dev/null +++ b/packages/node/src/utils/kill-port.ts @@ -0,0 +1,38 @@ +import { logger } from '@nx/devkit'; +import { check as portCheck } from 'tcp-port-used'; + +export const kill = require('kill-port'); + +/** + * Kills the process on the given port + * @param port + * @param killPortDelay + */ +export async function killPort( + port: number, + killPortDelay = 2500 +): Promise { + if (await portCheck(port)) { + let killPortResult; + try { + logger.info(`Attempting to close port ${port}`); + killPortResult = await kill(port); + await new Promise((resolve) => + setTimeout(() => resolve(), killPortDelay) + ); + if (await portCheck(port)) { + logger.error( + `Port ${port} still open ${JSON.stringify(killPortResult)}` + ); + } else { + logger.info(`Port ${port} successfully closed`); + return true; + } + } catch { + logger.error(`Port ${port} closing failed`); + } + return false; + } else { + return true; + } +} diff --git a/packages/node/src/utils/wait-for-port-open.ts b/packages/node/src/utils/wait-for-port-open.ts new file mode 100644 index 0000000000..afb9ccf903 --- /dev/null +++ b/packages/node/src/utils/wait-for-port-open.ts @@ -0,0 +1,71 @@ +import * as net from 'net'; +import { logger } from '@nx/devkit'; + +interface WaitForPortOpenOptions { + /** + * The host to connect to + * @default 'localhost' + */ + host?: string; + /** + * The number of retries to attempt + * @default 120 + */ + retries?: number; + /** + * The delay between retries + * @default 1000 + */ + retryDelay?: number; +} + +/** + * Waits for the given port to be open + * @param port + * @param options + */ +export function waitForPortOpen( + port: number, + options: WaitForPortOpenOptions = {} +): Promise { + const host = options.host ?? 'localhost'; + const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT']; + + return new Promise((resolve, reject) => { + const checkPort = (retries = options.retries ?? 120) => { + const client = new net.Socket(); + const cleanupClient = () => { + client.removeAllListeners('connect'); + client.removeAllListeners('error'); + client.end(); + client.destroy(); + client.unref(); + }; + client.once('connect', () => { + cleanupClient(); + resolve(); + }); + + client.once('error', (err) => { + if (retries === 0 || !allowedErrorCodes.includes(err['code'])) { + if (process.env['NX_VERBOSE_LOGGING'] === 'true') { + logger.info( + `Error connecting on ${host}:${port}: ${err['code'] || err}` + ); + } + cleanupClient(); + reject(err); + } else { + setTimeout(() => checkPort(retries - 1), options.retryDelay ?? 1000); + } + }); + + if (process.env['NX_VERBOSE_LOGGING'] === 'true') { + logger.info(`Connecting on ${host}:${port}`); + } + client.connect({ port, host }); + }; + + checkPort(); + }); +} diff --git a/packages/node/utils.ts b/packages/node/utils.ts new file mode 100644 index 0000000000..f145130992 --- /dev/null +++ b/packages/node/utils.ts @@ -0,0 +1,2 @@ +export { waitForPortOpen } from './src/utils/wait-for-port-open'; +export { killPort } from './src/utils/kill-port';