Files
nrwl--nx/e2e/node/src/node-esm-support.test.ts
T
2026-07-13 12:38:36 +08:00

578 lines
19 KiB
TypeScript

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);
});
});