import { stripIndents } from '@angular-devkit/core/src/utils/literals'; import { checkFilesExist, cleanupProject, getPackageManagerCommand, newProject, reservePort, runCLI, runCLIAsync, runCommand, runCommandUntil, promisifiedTreeKill, tmpProjPath, uniq, updateFile, updateJson, detectPackageManager, } from '@nx/e2e-utils'; import { execSync } from 'child_process'; // Starts a built node server app, polls its output until it reports it is // listening (up to 30s), then kills it and returns everything it printed. // Polling for the "ready" line avoids a fixed `sleep` race that loses on a // loaded machine, where node boot + module evaluation can take several seconds. function runBuiltApp(appName: string): string { return execSync( `node dist/apps/${appName}/main.js > ${appName}-run.log 2>&1 & PID=$!; ` + `for i in $(seq 1 30); do grep -q "server ready on port" ${appName}-run.log 2>/dev/null && break; sleep 1; done; ` + `kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true; cat ${appName}-run.log`, { cwd: tmpProjPath(), timeout: 60_000 } ).toString(); } function configureAsEsm(projectPath: string) { // Update project.json to configure build target for ESM const projectJsonPath = `${projectPath}/project.json`; updateJson(projectJsonPath, (config) => { if (config.targets?.build?.options) { // Configure build executor to output ESM format config.targets.build.options.format = ['esm']; } return config; }); // Also update TypeScript config to match ESM const tsconfigPath = `${projectPath}/tsconfig.app.json`; updateJson(tsconfigPath, (config) => { if (config.compilerOptions) { config.compilerOptions.module = 'ES2022'; config.compilerOptions.target = 'ES2022'; } return config; }); } const selectedPm = detectPackageManager(); describe('Node.js Framework ESM Support', () => { beforeAll(() => { newProject({ packages: ['@nx/node', '@nx/nest', '@nx/webpack'], packageManager: selectedPm === 'npm' ? 'pnpm' : selectedPm, }); }); afterAll(() => { cleanupProject(); }); describe('Express Framework with ESM', () => { it('should build and run Express app with ESM modules', async () => { const port = await reservePort(); const expressApp = uniq('express-esm'); runCLI( `generate @nx/node:app apps/${expressApp} --framework=express --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) const pmc = getPackageManagerCommand(); runCommand(`${pmc.addDev} node-fetch@3.3.0`); // Configure as ESM configureAsEsm(`apps/${expressApp}`); // Update the Express app to use ESM imports updateFile( `apps/${expressApp}/src/main.ts`, stripIndents` import express from 'express'; import fetch from 'node-fetch'; const app = express(); app.get('/api', (req, res) => { res.json({ message: 'Welcome to ${expressApp}!', framework: 'express', fetch_available: typeof fetch === 'function' }); }); console.log('Express ESM app starting'); console.log('fetch type:', typeof fetch); const port = process.env.PORT || ${port}; const server = app.listen(port, () => { console.log('Express ESM server ready on port ' + port); }); server.on('error', console.error); ` ); await runCLIAsync(`build ${expressApp}`); checkFilesExist(`dist/apps/${expressApp}/main.js`); const result = runBuiltApp(expressApp); expect(result).toContain('Express ESM app starting'); expect(result).toContain('Express ESM server ready on port'); expect(result).toContain('fetch type: function'); }, 600000); it('should serve Express app with ESM modules', async () => { const port = await reservePort(); const expressApp = uniq('express-esm-serve'); runCLI( `generate @nx/node:app apps/${expressApp} --framework=express --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) const pmc = getPackageManagerCommand(); runCommand(`${pmc.addDev} node-fetch@3.3.0`); // Configure as ESM configureAsEsm(`apps/${expressApp}`); // Create Express server with ESM imports updateFile( `apps/${expressApp}/src/main.ts`, stripIndents` import express from 'express'; import fetch from 'node-fetch'; const app = express(); console.log('Express ESM serve starting'); console.log('fetch type:', typeof fetch); app.get('/api', (req, res) => { res.json({ message: 'Express ESM serve working' }); }); const port = process.env.PORT || ${port}; const server = app.listen(port, () => { console.log('Express ESM serve ready on port ' + port); }); server.on('error', console.error); ` ); const serveProcess = await runCommandUntil( `serve ${expressApp}`, (output) => { return output.includes('Express ESM serve ready on port'); }, { timeout: 120000 } ); await promisifiedTreeKill(serveProcess.pid, 'SIGKILL'); }, 600000); }); describe('Fastify Framework with ESM', () => { it('should build and run Fastify app with ESM modules', async () => { const port = await reservePort(); const fastifyApp = uniq('fastify-esm'); runCLI( `generate @nx/node:app apps/${fastifyApp} --framework=fastify --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) const pmc = getPackageManagerCommand(); runCommand(`${pmc.addDev} node-fetch@3.3.0`); // Configure as ESM configureAsEsm(`apps/${fastifyApp}`); // Update the Fastify app to use ESM imports updateFile( `apps/${fastifyApp}/src/main.ts`, stripIndents` import Fastify from 'fastify'; import fetch from 'node-fetch'; const fastify = Fastify({ logger: false }); fastify.get('/api', async (request, reply) => { return { message: 'Welcome to ${fastifyApp}!', framework: 'fastify', fetch_available: typeof fetch === 'function' }; }); console.log('Fastify ESM app starting'); console.log('fetch type:', typeof fetch); const start = async () => { try { const port = +(process.env.PORT || ${port}); await fastify.listen({ port }); console.log('Fastify ESM server ready on port ' + port); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); ` ); await runCLIAsync(`build ${fastifyApp}`); checkFilesExist(`dist/apps/${fastifyApp}/main.js`); const result = runBuiltApp(fastifyApp); expect(result).toContain('Fastify ESM app starting'); expect(result).toContain('Fastify ESM server ready on port'); expect(result).toContain('fetch type: function'); }, 600000); it('should serve Fastify app with ESM modules', async () => { const port = await reservePort(); const fastifyApp = uniq('fastify-esm-serve'); runCLI( `generate @nx/node:app apps/${fastifyApp} --framework=fastify --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) const pmc = getPackageManagerCommand(); runCommand(`${pmc.addDev} node-fetch@3.3.0`); // Configure as ESM configureAsEsm(`apps/${fastifyApp}`); // Create Fastify server with ESM imports updateFile( `apps/${fastifyApp}/src/main.ts`, stripIndents` import Fastify from 'fastify'; import fetch from 'node-fetch'; const fastify = Fastify({ logger: false }); console.log('Fastify ESM serve starting'); console.log('fetch type:', typeof fetch); fastify.get('/api', async (request, reply) => { return { message: 'Fastify ESM serve working' }; }); const start = async () => { try { const port = +(process.env.PORT || ${port}); await fastify.listen({ port }); console.log('Fastify ESM serve ready on port ' + port); } catch (err) { process.exit(1); } }; start(); ` ); const serveProcess = await runCommandUntil( `serve ${fastifyApp}`, (output) => { return output.includes('Fastify ESM serve ready on port'); }, { timeout: 120000 } ); await promisifiedTreeKill(serveProcess.pid, 'SIGKILL'); }, 600000); }); describe('Koa Framework with ESM', () => { it('should build and run Koa app with ESM modules', async () => { const port = await reservePort(); const koaApp = uniq('koa-esm'); runCLI( `generate @nx/node:app apps/${koaApp} --framework=koa --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) and koa router const pmc = getPackageManagerCommand(); runCommand( `${pmc.addDev} node-fetch@3.3.0 @koa/router @types/koa__router` ); // Configure as ESM configureAsEsm(`apps/${koaApp}`); // Update the Koa app to use ESM imports updateFile( `apps/${koaApp}/src/main.ts`, stripIndents` import Koa from 'koa'; import Router from '@koa/router'; import fetch from 'node-fetch'; const app = new Koa(); const router = new Router(); router.get('/api', (ctx) => { ctx.body = { message: 'Welcome to ${koaApp}!', framework: 'koa', fetch_available: typeof fetch === 'function' }; }); app.use(router.routes()).use(router.allowedMethods()); console.log('Koa ESM app starting'); console.log('fetch type:', typeof fetch); const port = +(process.env.PORT || ${port}); const server = app.listen(port, () => { console.log('Koa ESM server ready on port ' + port); }); server.on('error', console.error); ` ); await runCLIAsync(`build ${koaApp}`); checkFilesExist(`dist/apps/${koaApp}/main.js`); const result = runBuiltApp(koaApp); expect(result).toContain('Koa ESM app starting'); expect(result).toContain('Koa ESM server ready on port'); expect(result).toContain('fetch type: function'); }, 600000); it('should serve Koa app with ESM modules', async () => { const port = await reservePort(); const koaApp = uniq('koa-esm-serve'); runCLI( `generate @nx/node:app apps/${koaApp} --framework=koa --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) and koa router const pmc = getPackageManagerCommand(); runCommand( `${pmc.addDev} node-fetch@3.3.0 @koa/router @types/koa__router` ); // Configure as ESM configureAsEsm(`apps/${koaApp}`); // Create Koa server with ESM imports updateFile( `apps/${koaApp}/src/main.ts`, stripIndents` import Koa from 'koa'; import Router from '@koa/router'; import fetch from 'node-fetch'; const app = new Koa(); const router = new Router(); console.log('Koa ESM serve starting'); console.log('fetch type:', typeof fetch); router.get('/api', (ctx) => { ctx.body = { message: 'Koa ESM serve working' }; }); app.use(router.routes()).use(router.allowedMethods()); const port = +(process.env.PORT || ${port}); const server = app.listen(port, () => { console.log('Koa ESM serve ready on port ' + port); }); server.on('error', console.error); ` ); const serveProcess = await runCommandUntil( `serve ${koaApp}`, (output) => { return output.includes('Koa ESM serve ready on port'); }, { timeout: 120000 } ); await promisifiedTreeKill(serveProcess.pid, 'SIGKILL'); }, 600000); }); describe('Nest Framework with ESM', () => { it('should build and run Nest app with ESM modules', async () => { const port = await reservePort(); const nestApp = uniq('nest-esm'); runCLI( `generate @nx/node:app apps/${nestApp} --framework=nest --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) const pmc = getPackageManagerCommand(); runCommand(`${pmc.addDev} node-fetch@3.3.0`); // Configure as ESM configureAsEsm(`apps/${nestApp}`); // Update the Nest app to use ESM imports updateFile( `apps/${nestApp}/src/main.ts`, stripIndents` import { NestFactory } from '@nestjs/core'; import { AppModule } from './app/app.module'; async function bootstrap() { console.log('Nest ESM app starting'); try { // Test ESM-only package import const { default: fetch } = await import('node-fetch'); console.log('fetch type:', typeof fetch); } catch (error) { console.error('Failed to import node-fetch:', error.message); } const app = await NestFactory.create(AppModule); const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); const port = process.env.PORT || ${port}; await app.listen(port); console.log('Nest ESM server ready on port ' + port); } bootstrap(); ` ); await runCLIAsync(`build ${nestApp}`); checkFilesExist(`dist/apps/${nestApp}/main.js`); const serveProcess = await runCommandUntil( `serve ${nestApp}`, (output) => { return output.includes('Nest ESM server ready on port'); }, { timeout: 120000 } ); await promisifiedTreeKill(serveProcess.pid, 'SIGKILL'); }, 600000); it('should serve Nest app with ESM modules', async () => { const port = await reservePort(); const nestApp = uniq('nest-esm-serve'); runCLI( `generate @nx/node:app apps/${nestApp} --framework=nest --linter=eslint --unitTestRunner=jest` ); // Install node-fetch (ESM-only package) and ajv@8 to fix dependency conflicts const pmc = getPackageManagerCommand(); runCommand(`${pmc.addDev} node-fetch@3.3.0`); runCommand(`${pmc.addDev} ajv@^8.17.1`); // Configure as ESM configureAsEsm(`apps/${nestApp}`); // Create Nest server with ESM imports updateFile( `apps/${nestApp}/src/main.ts`, stripIndents` import { NestFactory } from '@nestjs/core'; import { AppModule } from './app/app.module'; async function bootstrap() { console.log('Nest ESM serve starting'); try { const { default: fetch } = await import('node-fetch'); console.log('fetch type:', typeof fetch); } catch (error) { console.error('Failed to import node-fetch:', error.message); } const app = await NestFactory.create(AppModule); const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); const port = process.env.PORT || ${port}; await app.listen(port); console.log('Nest ESM serve ready on port ' + port); } bootstrap(); ` ); const serveProcess = await runCommandUntil( `serve ${nestApp}`, (output) => { return output.includes('Nest ESM serve ready on port'); }, { timeout: 120000 } ); await promisifiedTreeKill(serveProcess.pid, 'SIGKILL'); }, 600000); }); describe('Mixed Imports across Node.js Frameworks', () => { it('should handle CommonJS and ESM packages together', async () => { const port = await reservePort(); const nodeApp = uniq('node-mixed-imports'); runCLI( `generate @nx/node:app apps/${nodeApp} --framework=express --linter=eslint --unitTestRunner=jest` ); // Install packages with different module formats const pmc = getPackageManagerCommand(); runCommand( `${pmc.addDev} express @types/express node-fetch@3.3.0 lodash @types/lodash` ); // Create an Express app that uses both CommonJS (lodash) and ESM (node-fetch) packages updateFile( `apps/${nodeApp}/src/main.ts`, stripIndents` import express from 'express'; import * as lodash from 'lodash'; const app = express(); app.get('/api', async (req, res) => { try { // Import ESM-only package dynamically const { default: fetch } = await import('node-fetch'); // Use CommonJS package (lodash) const data = lodash.pick({ a: 1, b: 2, c: 3 }, ['a', 'b']); res.json({ message: 'Welcome to ${nodeApp}!', data, fetch_available: typeof fetch === 'function', lodash_available: typeof lodash.pick === 'function' }); } catch (error) { res.status(500).json({ error: error.message }); } }); console.log('Mixed imports Node.js app starting'); console.log('lodash.pick type:', typeof lodash.pick); const port = process.env.PORT || ${port}; const server = app.listen(port, () => { console.log('Mixed imports server ready on port ' + port); }); server.on('error', console.error); ` ); await runCLIAsync(`build ${nodeApp}`); checkFilesExist(`dist/apps/${nodeApp}/main.js`); const result = runBuiltApp(nodeApp); expect(result).toContain('Mixed imports Node.js app starting'); expect(result).toContain('Mixed imports server ready on port'); expect(result).toContain('lodash.pick type: function'); }, 600000); }); });