chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import {
|
||||
cleanupProject,
|
||||
getPackageManagerCommand,
|
||||
newProject,
|
||||
reservePort,
|
||||
runCLI,
|
||||
runCommand,
|
||||
isDockerAvailable,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e-utils';
|
||||
|
||||
let originalEnvPort;
|
||||
describe('Node Docker Applications', () => {
|
||||
if (isDockerAvailable()) {
|
||||
describe.each(['npm', 'yarn', 'pnpm'])(
|
||||
'%s',
|
||||
(packageManager: 'npm' | 'yarn' | 'pnpm') => {
|
||||
beforeEach(() => {
|
||||
originalEnvPort = process.env.PORT;
|
||||
newProject({
|
||||
preset: 'ts',
|
||||
packageManager,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.PORT = originalEnvPort;
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it(`should generate ${packageManager} compliant dockerfile and use it`, async () => {
|
||||
const { nodeapp } = await setupTest(packageManager);
|
||||
expect(runCLI(`docker:build ${nodeapp} --network=host`)).toContain(
|
||||
`Successfully ran target docker:build for project @proj/${nodeapp}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
it('docker is not available', () => expect(true).toBe(true));
|
||||
}
|
||||
});
|
||||
|
||||
async function setupTest(packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun') {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const lib = uniq('lib');
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${nodeapp} --port=${port} --bundler=esbuild --framework=fastify --docker=true --no-interactive`
|
||||
);
|
||||
|
||||
runCLI(
|
||||
`generate @nx/js:lib packages/${lib} --bundler=none --e2eTestRunner=none --unitTestRunner=none`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/main.ts`,
|
||||
(content) => `import { ${names(lib).propertyName} } from '@proj/${lib}';
|
||||
|
||||
console.log(${names(lib).propertyName}());
|
||||
|
||||
${content}
|
||||
`
|
||||
);
|
||||
|
||||
// App is CJS by default so lets update the lib to follow the same pattern
|
||||
updateJson(`packages/${lib}/tsconfig.lib.json`, (json) => {
|
||||
json.compilerOptions.module = 'commonjs';
|
||||
json.compilerOptions.moduleResolution = 'node';
|
||||
return json;
|
||||
});
|
||||
|
||||
updateJson('tsconfig.base.json', (json) => {
|
||||
json.compilerOptions.moduleResolution = 'node';
|
||||
json.compilerOptions.module = 'esnext';
|
||||
delete json.compilerOptions.customConditions;
|
||||
return json;
|
||||
});
|
||||
|
||||
if (packageManager === 'pnpm' || packageManager === 'yarn') {
|
||||
updateJson(`apps/${nodeapp}/package.json`, (json) => {
|
||||
json.dependencies ??= {};
|
||||
json.dependencies[`@proj/${lib}`] = 'workspace:*';
|
||||
return json;
|
||||
});
|
||||
} else if (packageManager === 'npm') {
|
||||
updateJson(`apps/${nodeapp}/package.json`, (json) => {
|
||||
json.dependencies ??= {};
|
||||
json.dependencies[`@proj/${lib}`] = '*';
|
||||
return json;
|
||||
});
|
||||
}
|
||||
const pmc = getPackageManagerCommand({ packageManager });
|
||||
runCommand(pmc.install);
|
||||
|
||||
runCLI('sync');
|
||||
|
||||
return { nodeapp };
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
checkFilesDoNotExist,
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
newProject,
|
||||
promisifiedTreeKill,
|
||||
readFile,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
|
||||
describe('Node Applications + esbuild', () => {
|
||||
beforeAll(() =>
|
||||
newProject({
|
||||
packages: ['@nx/node'],
|
||||
})
|
||||
);
|
||||
|
||||
afterAll(() => cleanupProject());
|
||||
|
||||
it('should generate an app using esbuild', async () => {
|
||||
const app = uniq('nodeapp');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${app} --bundler=esbuild --no-interactive --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
checkFilesDoNotExist(`apps/${app}/webpack.config.js`);
|
||||
|
||||
updateFile(`apps/${app}/src/main.ts`, `console.log('Hello World!');`);
|
||||
|
||||
const p = await runCommandUntil(`serve ${app} --watch=false`, (output) => {
|
||||
process.stdout.write(output);
|
||||
return output.includes(`Hello World!`);
|
||||
});
|
||||
checkFilesExist(`dist/apps/${app}/main.js`);
|
||||
|
||||
// Check that updating the file won't trigger a rebuild since --watch=false.
|
||||
updateFile(`apps/${app}/src/main.ts`, `console.log('Bye1');`);
|
||||
await new Promise((res) => setTimeout(res, 2000));
|
||||
|
||||
expect(readFile(`dist/apps/${app}/apps/${app}/src/main.js`)).not.toContain(
|
||||
`Bye!`
|
||||
);
|
||||
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
}, 300_000);
|
||||
});
|
||||
@@ -0,0 +1,577 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
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<void>((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);
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import {
|
||||
cleanupProject,
|
||||
getPackageManagerCommand,
|
||||
getSelectedPackageManager,
|
||||
newProject,
|
||||
reservePort,
|
||||
runCLI,
|
||||
runCommand,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e-utils';
|
||||
|
||||
let originalEnvPort;
|
||||
|
||||
describe('Node Esbuild Applications', () => {
|
||||
beforeAll(() => {
|
||||
originalEnvPort = process.env.PORT;
|
||||
newProject({
|
||||
preset: 'ts',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.PORT = originalEnvPort;
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
// TODO: Re-enable this test once https://github.com/pinojs/pino/issues/2253 is resolved
|
||||
it.skip('it should generate an app that cosumes a non-buildable ts library', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const lib = uniq('lib');
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${nodeapp} --port=${port} --bundler=esbuild --framework=fastify --no-interactive`
|
||||
);
|
||||
|
||||
runCLI(
|
||||
`generate @nx/js:lib packages/${lib} --bundler=none --e2eTestRunner=none --unitTestRunner=none`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/main.ts`,
|
||||
(content) => `import { ${names(lib).propertyName} } from '@proj/${lib}';
|
||||
|
||||
console.log(${names(lib).propertyName}());
|
||||
|
||||
${content}
|
||||
`
|
||||
);
|
||||
|
||||
// App is CJS by default so lets update the lib to follow the same pattern
|
||||
updateJson(`packages/${lib}/tsconfig.lib.json`, (json) => {
|
||||
json.compilerOptions.module = 'commonjs';
|
||||
json.compilerOptions.moduleResolution = 'node';
|
||||
return json;
|
||||
});
|
||||
|
||||
updateJson('tsconfig.base.json', (json) => {
|
||||
json.compilerOptions.moduleResolution = 'node';
|
||||
json.compilerOptions.module = 'esnext';
|
||||
delete json.compilerOptions.customConditions;
|
||||
return json;
|
||||
});
|
||||
|
||||
const pm = getSelectedPackageManager();
|
||||
if (pm === 'pnpm') {
|
||||
updateJson(`apps/${nodeapp}/package.json`, (json) => {
|
||||
json.dependencies ??= {};
|
||||
json.dependencies[`@proj/${lib}`] = 'workspace:*';
|
||||
return json;
|
||||
});
|
||||
|
||||
const pmc = getPackageManagerCommand({ packageManager: pm });
|
||||
runCommand(pmc.install);
|
||||
}
|
||||
|
||||
runCLI('sync');
|
||||
|
||||
// check build
|
||||
expect(runCLI(`build ${nodeapp}`)).toContain(
|
||||
`Successfully ran target build for project @proj/${nodeapp}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,354 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
getSelectedPackageManager,
|
||||
getPackageManagerCommand,
|
||||
killPorts,
|
||||
newProject,
|
||||
promisifiedTreeKill,
|
||||
readJson,
|
||||
runCLI,
|
||||
runCommand,
|
||||
runCommandUntil,
|
||||
tmpProjPath,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
reservePort,
|
||||
} from '@nx/e2e-utils';
|
||||
import { execSync } from 'child_process';
|
||||
import * as http from 'http';
|
||||
|
||||
let originalEnvPort;
|
||||
|
||||
describe('Node Applications', () => {
|
||||
const pm = getSelectedPackageManager();
|
||||
let workspaceName: string;
|
||||
|
||||
beforeAll(() => {
|
||||
originalEnvPort = process.env.PORT;
|
||||
workspaceName = newProject({
|
||||
packages: [
|
||||
'@nx/node',
|
||||
'@nx/express',
|
||||
'@nx/nest',
|
||||
'@nx/webpack',
|
||||
'@nx/jest',
|
||||
],
|
||||
preset: 'ts',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.PORT = originalEnvPort;
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it('should be able to generate an empty application', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow();
|
||||
expect(() => runCLI(`test ${nodeapp}`)).not.toThrow();
|
||||
|
||||
updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
|
||||
runCLI(`build ${nodeapp}`);
|
||||
|
||||
checkFilesExist(`apps/${nodeapp}/dist/main.js`);
|
||||
const result = execSync(`node apps/${nodeapp}/dist/main.js`, {
|
||||
cwd: tmpProjPath(),
|
||||
}).toString();
|
||||
expect(result).toContain('Hello World!');
|
||||
await killPorts(port);
|
||||
}, 300_000);
|
||||
|
||||
// TODO(jack): fix and re-enable
|
||||
xit('should be able to generate an express application', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const nodelib = uniq('nodelib');
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
runCLI(
|
||||
`generate @nx/express:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/node:lib packages/${nodelib} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
// No tests are generated by default, add a stub one.
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/app/test.spec.ts`,
|
||||
`
|
||||
describe('test', () => {
|
||||
it('should work', () => {
|
||||
expect(true).toEqual(true);
|
||||
})
|
||||
})
|
||||
`
|
||||
);
|
||||
|
||||
updateFile(`apps/${nodeapp}/src/assets/file.txt`, `Test`);
|
||||
updateFile(`apps/${nodeapp}/src/main.ts`, (content) => {
|
||||
return `import { ${nodelib} } from '@${workspaceName}/${nodelib}';\n${content}\nconsole.log(${nodelib}());`;
|
||||
});
|
||||
// pnpm does not link packages unless they are deps
|
||||
// npm, yarn, and bun will link them in the root node_modules regardless
|
||||
if (pm === 'pnpm') {
|
||||
updateJson(`apps/${nodeapp}/package.json`, (json) => {
|
||||
json.dependencies = {
|
||||
[`@${workspaceName}/${nodelib}`]: 'workspace:*',
|
||||
};
|
||||
return json;
|
||||
});
|
||||
runCommand(`cd apps/${nodeapp} && ${getPackageManagerCommand().install}`);
|
||||
}
|
||||
runCLI(`sync`);
|
||||
|
||||
expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow();
|
||||
expect(() => runCLI(`test ${nodeapp}`)).not.toThrow();
|
||||
expect(() => runCLI(`build ${nodeapp}`)).not.toThrow();
|
||||
expect(() => runCLI(`typecheck ${nodeapp}`)).not.toThrow();
|
||||
expect(() => runCLI(`lint ${nodelib}`)).not.toThrow();
|
||||
expect(() => runCLI(`test ${nodelib}`)).not.toThrow();
|
||||
expect(() => runCLI(`build ${nodelib}`)).not.toThrow();
|
||||
expect(() => runCLI(`typecheck ${nodelib}`)).not.toThrow();
|
||||
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nodeapp}`,
|
||||
(output) => output.includes(`Listening at http://localhost:${port}`),
|
||||
|
||||
{
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let result = await getData(port);
|
||||
expect(result.message).toMatch(`Welcome to ${nodeapp}!`);
|
||||
|
||||
result = await getData(port, '/assets/file.txt');
|
||||
expect(result).toMatch(`Test`);
|
||||
|
||||
try {
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
expect(await killPorts(port)).toBeTruthy();
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error during cleanup (may be expected, especially ECONNRESET):',
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
it('should be able to generate a nest application', async () => {
|
||||
const nestapp = uniq('nodeapp');
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
runCLI(
|
||||
`generate @nx/nest:app apps/${nestapp} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
expect(() => runCLI(`lint ${nestapp}`)).not.toThrow();
|
||||
expect(() => runCLI(`test ${nestapp}`)).not.toThrow();
|
||||
|
||||
runCLI(`build ${nestapp}`);
|
||||
checkFilesExist(`apps/${nestapp}/dist/main.js`);
|
||||
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nestapp}`,
|
||||
(output) =>
|
||||
output.includes(
|
||||
`Application is running on: http://localhost:${port}/api`
|
||||
),
|
||||
|
||||
{
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await getData(port, '/api');
|
||||
expect(result.message).toMatch('Hello');
|
||||
|
||||
try {
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
expect(await killPorts(port)).toBeTruthy();
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error during cleanup (may be expected, especially ECONNRESET):',
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
// TODO(jack): fix and re-enable
|
||||
xit('should be able to import a lib into a nest application', async () => {
|
||||
const nestApp = uniq('nestapp');
|
||||
const nestLib = uniq('nestlib');
|
||||
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
runCLI(`generate @nx/nest:app apps/${nestApp} --no-interactive`);
|
||||
|
||||
runCLI(`generate @nx/nest:lib packages/${nestLib} --no-interactive`);
|
||||
|
||||
updateFile(`apps/${nestApp}/src/app/app.module.ts`, (content) => {
|
||||
return `import '@${workspaceName}/${nestLib}';\n${content}\n`;
|
||||
});
|
||||
|
||||
if (pm === 'pnpm') {
|
||||
updateJson(`apps/${nestApp}/package.json`, (json) => {
|
||||
json.dependencies = {
|
||||
[`@${workspaceName}/${nestLib}`]: 'workspace:*',
|
||||
};
|
||||
return json;
|
||||
});
|
||||
runCommand(`${getPackageManagerCommand().install}`);
|
||||
}
|
||||
runCLI(`sync`);
|
||||
|
||||
runCLI(`build ${nestApp} --verbose`);
|
||||
checkFilesExist(`apps/${nestApp}/dist/main.js`);
|
||||
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nestApp}`,
|
||||
(output) =>
|
||||
output.includes(
|
||||
`Application is running on: http://localhost:${port}/api`
|
||||
),
|
||||
|
||||
{
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await getData(port, '/api');
|
||||
expect(result.message).toMatch('Hello');
|
||||
|
||||
try {
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
expect(await killPorts(port)).toBeTruthy();
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error during cleanup (may be expected, especially ECONNRESET):',
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
// Dependencies that are not directly imported but are still required for the build to succeed
|
||||
// App -> LibA -> LibB
|
||||
// LibB is transitive, it's not imported in the app, but is required by LibA
|
||||
|
||||
it('should be able to work with transitive non-dependencies', async () => {
|
||||
const nestApp = uniq('nestApp');
|
||||
const nestLibA = uniq('nestliba');
|
||||
const nestLibB = uniq('nestlibb');
|
||||
const port = await reservePort();
|
||||
|
||||
process.env.PORT = `${port}`;
|
||||
runCLI(`generate @nx/nest:app apps/${nestApp} --no-interactive`);
|
||||
|
||||
runCLI(`generate @nx/nest:lib packages/${nestLibA} --no-interactive`);
|
||||
runCLI(`generate @nx/nest:lib packages/${nestLibB} --no-interactive`);
|
||||
|
||||
if (pm === 'pnpm') {
|
||||
updateJson(`apps/${nestApp}/package.json`, (json) => {
|
||||
json.dependencies = {
|
||||
[`@${workspaceName}/${nestLibA}`]: 'workspace:*',
|
||||
};
|
||||
return json;
|
||||
});
|
||||
|
||||
updateJson(`packages/${nestLibA}/package.json`, (json) => {
|
||||
json.dependencies = {
|
||||
[`@${workspaceName}/${nestLibB}`]: 'workspace:*',
|
||||
};
|
||||
return json;
|
||||
});
|
||||
runCommand(`${getPackageManagerCommand().install}`);
|
||||
}
|
||||
runCLI(`sync`);
|
||||
|
||||
runCLI(`build ${nestApp} --verbose`);
|
||||
checkFilesExist(`apps/${nestApp}/dist/main.js`);
|
||||
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nestApp}`,
|
||||
(output) =>
|
||||
output.includes(
|
||||
`Application is running on: http://localhost:${port}/api`
|
||||
),
|
||||
|
||||
{
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await getData(port, '/api');
|
||||
expect(result.message).toMatch('Hello');
|
||||
|
||||
try {
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
expect(await killPorts(port)).toBeTruthy();
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error during cleanup (may be expected, especially ECONNRESET):',
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
it('should respect and support generating libraries with a name different than the import path', () => {
|
||||
const nodeLib = uniq('node-lib');
|
||||
const nestLib = uniq('nest-lib');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:lib packages/${nodeLib} --name=${nodeLib} --buildable`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/nest:lib packages/${nestLib} --name=${nestLib} --buildable`
|
||||
);
|
||||
|
||||
const packageJson = readJson(`packages/${nodeLib}/package.json`);
|
||||
expect(packageJson.nx.name).toBe(nodeLib);
|
||||
const nestPackageJson = readJson(`packages/${nestLib}/package.json`);
|
||||
expect(nestPackageJson.nx.name).toBe(nestLib);
|
||||
|
||||
expect(runCLI(`build ${nodeLib}`)).toContain(
|
||||
`Successfully ran target build for project ${nodeLib}`
|
||||
);
|
||||
expect(runCLI(`build ${nestLib}`)).toContain(
|
||||
`Successfully ran target build for project ${nestLib}`
|
||||
);
|
||||
}, 300_000);
|
||||
});
|
||||
|
||||
function getData(port, path = '/api'): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
http.get(`http://localhost:${port}${path}`, (res) => {
|
||||
expect(res.statusCode).toEqual(200);
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.once('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
newProject,
|
||||
readFile,
|
||||
runCLI,
|
||||
runCLIAsync,
|
||||
runCommandUntil,
|
||||
waitUntil,
|
||||
tmpProjPath,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
promisifiedTreeKill,
|
||||
} from '@nx/e2e-utils';
|
||||
import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('Node Applications + webpack', () => {
|
||||
beforeAll(() =>
|
||||
newProject({
|
||||
packages: ['@nx/node', '@nx/webpack', '@nx/esbuild'],
|
||||
})
|
||||
);
|
||||
|
||||
afterAll(() => cleanupProject());
|
||||
|
||||
it('should generate an app using webpack', async () => {
|
||||
const app = uniq('nodeapp');
|
||||
|
||||
// This fails with Crystal enabled because `--optimization` is not a correct flag to pass to `webpack`.
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${app} --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest`,
|
||||
{
|
||||
env: { NX_ADD_PLUGINS: 'false' },
|
||||
}
|
||||
);
|
||||
|
||||
checkFilesExist(`apps/${app}/webpack.config.js`);
|
||||
|
||||
updateFile(
|
||||
`apps/${app}/src/main.ts`,
|
||||
`
|
||||
function foo(x: string) {
|
||||
return "foo " + x;
|
||||
};
|
||||
console.log(foo("bar"));
|
||||
`
|
||||
);
|
||||
await runCLIAsync(`build ${app}`);
|
||||
|
||||
checkFilesExist(`dist/apps/${app}/main.js`);
|
||||
// no optimization by default
|
||||
const content = readFile(`dist/apps/${app}/main.js`);
|
||||
expect(content).toContain('console.log(foo("bar"))');
|
||||
|
||||
const result = execSync(`node dist/apps/${app}/main.js`, {
|
||||
cwd: tmpProjPath(),
|
||||
}).toString();
|
||||
expect(result).toMatch(/foo bar/);
|
||||
|
||||
await runCLIAsync(`build ${app} --optimization`);
|
||||
const optimizedContent = readFile(`dist/apps/${app}/main.js`);
|
||||
expect(optimizedContent).toContain('console.log("foo "+"bar")');
|
||||
|
||||
// Test that serve can re-run dependency builds.
|
||||
const lib = uniq('nodelib');
|
||||
runCLI(
|
||||
`generate @nx/js:lib libs/${lib} --bundler=esbuild --no-interactive`
|
||||
);
|
||||
|
||||
updateJson(join('apps', app, 'project.json'), (config) => {
|
||||
// Since we read from lib from dist, we should re-build it when lib changes.
|
||||
config.targets.build.options.buildLibsFromSource = false;
|
||||
config.targets.serve.options.runBuildTargetDependencies = true;
|
||||
return config;
|
||||
});
|
||||
|
||||
updateFile(
|
||||
`apps/${app}/src/main.ts`,
|
||||
`
|
||||
import { ${lib} } from '@proj/${lib}';
|
||||
console.log('Hello ' + ${lib}());
|
||||
`
|
||||
);
|
||||
|
||||
const serveProcess = await runCommandUntil(
|
||||
`serve ${app} --watch --runBuildTargetDependencies`,
|
||||
(output) => {
|
||||
return output.includes(`Hello`);
|
||||
},
|
||||
{
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Update library source and check that it triggers rebuild.
|
||||
const terminalOutputs: string[] = [];
|
||||
serveProcess.stdout.on('data', (chunk) => {
|
||||
const data = chunk.toString();
|
||||
terminalOutputs.push(data);
|
||||
});
|
||||
|
||||
updateFile(
|
||||
`libs/${lib}/src/index.ts`,
|
||||
`export function ${lib}() { return 'should rebuild lib'; }`
|
||||
);
|
||||
|
||||
await waitUntil(
|
||||
() => {
|
||||
return terminalOutputs.some((output) =>
|
||||
output.includes(`should rebuild lib`)
|
||||
);
|
||||
},
|
||||
{ timeout: 60_000, ms: 200 }
|
||||
);
|
||||
|
||||
await promisifiedTreeKill(serveProcess.pid, 'SIGKILL');
|
||||
}, 300_000);
|
||||
});
|
||||
@@ -0,0 +1,777 @@
|
||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
||||
import { joinPathFragments } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesDoNotExist,
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
createFile,
|
||||
detectPackageManager,
|
||||
getPackageManagerCommand,
|
||||
killPorts,
|
||||
newProject,
|
||||
reservePort,
|
||||
packageInstall,
|
||||
packageManagerLockFile,
|
||||
promisifiedTreeKill,
|
||||
readFile,
|
||||
runCLI,
|
||||
runCLIAsync,
|
||||
runCommand,
|
||||
runCommandUntil,
|
||||
tmpProjPath,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e-utils';
|
||||
import { execSync } from 'child_process';
|
||||
import * as http from 'http';
|
||||
import { getLockFileName } from '@nx/js';
|
||||
import { satisfies } from 'semver';
|
||||
import { join } from 'path';
|
||||
|
||||
let originalEnvPort;
|
||||
|
||||
function getData(port, path = '/api'): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
http.get(`http://localhost:${port}${path}`, (res) => {
|
||||
expect(res.statusCode).toEqual(200);
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.once('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('Node Applications', () => {
|
||||
beforeAll(() => {
|
||||
originalEnvPort = process.env.PORT;
|
||||
newProject({
|
||||
packages: [
|
||||
'@nx/node',
|
||||
'@nx/express',
|
||||
'@nx/nest',
|
||||
'@nx/webpack',
|
||||
'@nx/jest',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.PORT = originalEnvPort;
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it('should be able to generate an empty application', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow();
|
||||
expect(() => runCLI(`test ${nodeapp}`)).not.toThrow();
|
||||
|
||||
updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
|
||||
await runCLIAsync(`build ${nodeapp}`);
|
||||
|
||||
checkFilesExist(`dist/apps/${nodeapp}/main.js`);
|
||||
const result = execSync(`node dist/apps/${nodeapp}/main.js`, {
|
||||
cwd: tmpProjPath(),
|
||||
}).toString();
|
||||
expect(result).toContain('Hello World!');
|
||||
await killPorts(port);
|
||||
}, 300000);
|
||||
|
||||
// TODO(crystal, @ndcunningham): This does not work because NxWebpackPlugin({}) outputFilename does not work.
|
||||
xit('should be able to generate the correct outputFileName in options', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${nodeapp} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
updateJson(join('apps', nodeapp, 'project.json'), (config) => {
|
||||
config.targets.build.options.outputFileName = 'index.js';
|
||||
return config;
|
||||
});
|
||||
|
||||
await runCLIAsync(`build ${nodeapp}`);
|
||||
checkFilesExist(`dist/apps/${nodeapp}/index.js`);
|
||||
}, 300000);
|
||||
|
||||
it('should be able to generate an empty application with additional entries', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --bundler=webpack --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
const lintResults = runCLI(`lint ${nodeapp}`);
|
||||
expect(lintResults).toContain('Successfully ran target lint');
|
||||
|
||||
updateFile(
|
||||
`apps/${nodeapp}/webpack.config.js`,
|
||||
`
|
||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, '../../dist/apps/${nodeapp}'),
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
target: 'node',
|
||||
compiler: 'tsc',
|
||||
main: './src/main.ts',
|
||||
tsConfig: './tsconfig.app.json',
|
||||
assets: ['./src/assets'],
|
||||
additionalEntryPoints: [
|
||||
{
|
||||
entryPath: 'apps/${nodeapp}/src/additional-main.ts',
|
||||
entryName: 'additional-main',
|
||||
}
|
||||
],
|
||||
optimization: false,
|
||||
outputHashing: 'none',
|
||||
}),
|
||||
],
|
||||
};
|
||||
`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/additional-main.ts`,
|
||||
`console.log('Hello Additional World!');`
|
||||
);
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/main.ts`,
|
||||
`console.log('Hello World!');
|
||||
console.log('env: ' + process.env['NODE_ENV']);
|
||||
`
|
||||
);
|
||||
|
||||
await runCLIAsync(`build ${nodeapp}`);
|
||||
|
||||
checkFilesExist(
|
||||
`dist/apps/${nodeapp}/main.js`,
|
||||
`dist/apps/${nodeapp}/additional-main.js`
|
||||
);
|
||||
|
||||
const result = execSync(
|
||||
`NODE_ENV=development && node dist/apps/${nodeapp}/main.js`,
|
||||
{
|
||||
cwd: tmpProjPath(),
|
||||
}
|
||||
).toString();
|
||||
expect(result).toContain('Hello World!');
|
||||
expect(result).toContain('env: development');
|
||||
|
||||
const additionalResult = execSync(
|
||||
`node dist/apps/${nodeapp}/additional-main.js`,
|
||||
{
|
||||
cwd: tmpProjPath(),
|
||||
}
|
||||
).toString();
|
||||
expect(additionalResult).toContain('Hello Additional World!');
|
||||
|
||||
await killPorts(port);
|
||||
}, 300_000);
|
||||
|
||||
it('should be able to generate an empty application with variable in .env file', async () => {
|
||||
const originalEnvPort = process.env.PORT;
|
||||
const port = 3457;
|
||||
process.env.PORT = `${port}`;
|
||||
const nodeapp = uniq('nodeapp');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${nodeapp} --linter=eslint --bundler=webpack --framework=none --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
updateFile('.env', `NX_FOOBAR="test foo bar"`);
|
||||
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/main.ts`,
|
||||
`console.log('foobar: ' + process.env['NX_FOOBAR']);`
|
||||
);
|
||||
|
||||
await runCLIAsync(`build ${nodeapp}`);
|
||||
checkFilesExist(`dist/apps/${nodeapp}/main.js`);
|
||||
|
||||
// check serving
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nodeapp} --port=${port} --watch=false`,
|
||||
(output) => {
|
||||
process.stdout.write(output);
|
||||
return output.includes(`foobar: test foo bar`);
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
try {
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
await killPorts(port);
|
||||
} finally {
|
||||
process.env.port = originalEnvPort;
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
it("should exclude 'test' target from e2e project that uses jest", async () => {
|
||||
const appName = uniq('nodeapp');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app ${appName} --no-interactive --unitTestRunner=jest --linter=eslint --e2eTestRunner=jest`
|
||||
);
|
||||
|
||||
const nxJson = JSON.parse(readFile('nx.json'));
|
||||
expect(nxJson.plugins).toBeDefined();
|
||||
|
||||
const jestPlugin = nxJson.plugins.find(
|
||||
(p) => p.plugin === '@nx/jest/plugin'
|
||||
);
|
||||
expect(jestPlugin).toBeDefined();
|
||||
expect(jestPlugin.exclude).toContain(`${appName}-e2e/**/*`);
|
||||
});
|
||||
|
||||
it('should be able to generate an express application', async () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const originalEnvPort = process.env.PORT;
|
||||
const port = 3499;
|
||||
process.env.PORT = `${port}`;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/express:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
const lintResults = runCLI(`lint ${nodeapp}`);
|
||||
expect(lintResults).toContain('Successfully ran target lint');
|
||||
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/app/test.spec.ts`,
|
||||
`
|
||||
describe('test', () => {
|
||||
it('should work', () => {
|
||||
expect(true).toEqual(true);
|
||||
})
|
||||
})
|
||||
`
|
||||
);
|
||||
|
||||
const jestResult = runCLI(`test ${nodeapp}`);
|
||||
expect(jestResult).toContain('Successfully ran target test');
|
||||
|
||||
// checking serve
|
||||
updateFile(`apps/${nodeapp}/src/assets/file.txt`, `Test`);
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nodeapp}`,
|
||||
(output) => output.includes(`Listening at http://localhost:${port}`),
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let result = await getData(port);
|
||||
expect(result.message).toMatch(`Welcome to ${nodeapp}!`);
|
||||
|
||||
result = await getData(port, '/assets/file.txt');
|
||||
expect(result).toMatch(`Test`);
|
||||
|
||||
try {
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
await killPorts(port);
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
it('should be able to generate a nest application', async () => {
|
||||
const nestapp = uniq('nestapp');
|
||||
const port = 3335;
|
||||
runCLI(
|
||||
`generate @nx/nest:app apps/${nestapp} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
const lintResults = runCLI(`lint ${nestapp}`);
|
||||
expect(lintResults).toContain('Successfully ran target lint');
|
||||
|
||||
updateFile(`apps/${nestapp}/src/assets/file.txt`, ``);
|
||||
const jestResult = await runCLIAsync(`test ${nestapp}`);
|
||||
expect(jestResult.combinedOutput).toContain(
|
||||
'Test Suites: 2 passed, 2 total'
|
||||
);
|
||||
|
||||
const buildResult = runCLI(`build ${nestapp}`);
|
||||
|
||||
checkFilesExist(
|
||||
`dist/apps/${nestapp}/main.js`,
|
||||
`dist/apps/${nestapp}/assets/file.txt`
|
||||
);
|
||||
|
||||
expect(buildResult).toContain(
|
||||
`Successfully ran target build for project ${nestapp}`
|
||||
);
|
||||
|
||||
// checking serve
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nestapp} --port=${port}`,
|
||||
(output) => {
|
||||
process.stdout.write(output);
|
||||
return output.includes(`listening on ws://localhost:${port}`);
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
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');
|
||||
}, 120000);
|
||||
|
||||
it('should generate a nest application with docker', async () => {
|
||||
const nestapp = 'node-nest-docker-test';
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app ${nestapp} --bundler=webpack --framework=nest --docker --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
checkFilesExist(`${nestapp}/Dockerfile`);
|
||||
});
|
||||
|
||||
// TODO(crystal, @ndcunningham): how do we handle this now?
|
||||
// Revisit when NxWebpackPlugin({}) outputFilename is working.
|
||||
xit('should be able to run ESM applications', async () => {
|
||||
const esmapp = uniq('esmapp');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app ${esmapp} --linter=eslint --framework=none --bundler=webpack --unitTestRunner=jest`
|
||||
);
|
||||
updateJson(`apps/${esmapp}/tsconfig.app.json`, (config) => {
|
||||
config.module = 'esnext';
|
||||
config.target = 'es2020';
|
||||
return config;
|
||||
});
|
||||
updateJson(join('apps', esmapp, 'project.json'), (config) => {
|
||||
config.targets.build.options.outputFileName = 'main.mjs';
|
||||
config.targets.build.options.assets = [];
|
||||
return config;
|
||||
});
|
||||
updateFile(
|
||||
`apps/${esmapp}/webpack.config.js`,
|
||||
`
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
module.exports = composePlugins(withNx(), (config) => {
|
||||
config.experiments = {
|
||||
...config.experiments,
|
||||
outputModule: true,
|
||||
topLevelAwait: true,
|
||||
};
|
||||
config.output = {
|
||||
path: config.output.path,
|
||||
chunkFormat: 'module',
|
||||
library: { type: 'module' }
|
||||
}
|
||||
return config;
|
||||
});
|
||||
`
|
||||
);
|
||||
await runCLIAsync(`build ${esmapp}`);
|
||||
const p = await runCommandUntil(
|
||||
`serve ${esmapp}`,
|
||||
(output) => {
|
||||
return output.includes('Hello World');
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
}
|
||||
);
|
||||
await promisifiedTreeKill(p.pid, 'SIGKILL');
|
||||
}, 300000);
|
||||
});
|
||||
|
||||
describe('Build Node apps', () => {
|
||||
let scope: string;
|
||||
beforeAll(() => {
|
||||
originalEnvPort = process.env.PORT;
|
||||
scope = newProject({
|
||||
packages: [
|
||||
'@nx/node',
|
||||
'@nx/express',
|
||||
'@nx/nest',
|
||||
'@nx/webpack',
|
||||
'@nx/jest',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.PORT = originalEnvPort;
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
// TODO(crystal, @ndcunningham): What is the alternative here?
|
||||
xit('should generate a package.json with the `--generatePackageJson` flag', async () => {
|
||||
const packageManager = detectPackageManager(tmpProjPath());
|
||||
const nestapp = uniq('nestapp');
|
||||
runCLI(
|
||||
`generate @nx/nest:app apps/${nestapp} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
await runCLIAsync(`build ${nestapp} --generatePackageJson`);
|
||||
|
||||
checkFilesExist(`dist/apps/${nestapp}/package.json`);
|
||||
checkFilesExist(
|
||||
`dist/apps/${nestapp}/${getLockFileName(
|
||||
detectPackageManager(tmpProjPath())
|
||||
)}`
|
||||
);
|
||||
const rootPackageJson = JSON.parse(readFile(`package.json`));
|
||||
const packageJson = JSON.parse(
|
||||
readFile(`dist/apps/${nestapp}/package.json`)
|
||||
);
|
||||
expect(packageJson).toEqual(
|
||||
expect.objectContaining({
|
||||
main: 'main.js',
|
||||
name: expect.any(String),
|
||||
version: '0.0.1',
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
satisfies(
|
||||
packageJson.dependencies['@nestjs/common'],
|
||||
rootPackageJson.dependencies['@nestjs/common']
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
satisfies(
|
||||
packageJson.dependencies['@nestjs/core'],
|
||||
rootPackageJson.dependencies['@nestjs/core']
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
satisfies(
|
||||
packageJson.dependencies['reflect-metadata'],
|
||||
rootPackageJson.dependencies['reflect-metadata']
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
satisfies(
|
||||
packageJson.dependencies['rxjs'],
|
||||
rootPackageJson.dependencies['rxjs']
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
satisfies(
|
||||
packageJson.dependencies['tslib'],
|
||||
rootPackageJson.dependencies['tslib']
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
checkFilesExist(
|
||||
`dist/apps/${nestapp}/${packageManagerLockFile[packageManager]}`
|
||||
);
|
||||
runCommand(`${getPackageManagerCommand().ciInstall}`, {
|
||||
cwd: joinPathFragments(tmpProjPath(), 'dist/apps', nestapp),
|
||||
});
|
||||
|
||||
const nodeapp = uniq('nodeapp');
|
||||
runCLI(
|
||||
`generate @nx/node:app ${nodeapp} --bundler=webpack --unitTestRunner=jest --linter=eslint`
|
||||
);
|
||||
|
||||
const jslib = uniq('jslib');
|
||||
runCLI(`generate @nx/js:lib ${jslib} --bundler=tsc`);
|
||||
|
||||
updateFile(
|
||||
`apps/${nodeapp}/src/main.ts`,
|
||||
`
|
||||
import { ${jslib} } from '@${scope}/${jslib}';
|
||||
console.log('Hello World!');
|
||||
${jslib}();
|
||||
`
|
||||
);
|
||||
|
||||
const { combinedOutput: nodeCombinedOutput } = await runCLIAsync(
|
||||
`build ${nodeapp} --generate-package-json`
|
||||
);
|
||||
expect(nodeCombinedOutput).not.toMatch(/Graph is not consistent/);
|
||||
checkFilesExist(`dist/apps/${nodeapp}/package.json`);
|
||||
checkFilesExist(
|
||||
`dist/apps/${nodeapp}/${packageManagerLockFile[packageManager]}`
|
||||
);
|
||||
const nodeAppPackageJson = JSON.parse(
|
||||
readFile(`dist/apps/${nodeapp}/package.json`)
|
||||
);
|
||||
|
||||
expect(nodeAppPackageJson['dependencies']['tslib']).toBeTruthy();
|
||||
|
||||
runCommand(`${getPackageManagerCommand().ciInstall}`, {
|
||||
cwd: joinPathFragments(tmpProjPath(), 'dist/apps', nestapp),
|
||||
});
|
||||
|
||||
runCommand(`${getPackageManagerCommand().ciInstall}`, {
|
||||
cwd: joinPathFragments(tmpProjPath(), 'dist/apps', nodeapp),
|
||||
});
|
||||
}, 1_000_000);
|
||||
|
||||
it('should remove previous output before building with the --deleteOutputPath option set', async () => {
|
||||
const appName = uniq('app');
|
||||
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app apps/${appName} --port=${port} --no-interactive --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
// deleteOutputPath should default to true
|
||||
createFile(`dist/apps/${appName}/_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`); // no explicit deleteOutputPath option set
|
||||
checkFilesDoNotExist(`dist/apps/${appName}/_should_remove.txt`);
|
||||
checkFilesExist(`dist/apps/_should_not_remove.txt`);
|
||||
}, 120000);
|
||||
|
||||
it('should support generating projects with the new name and root format', async () => {
|
||||
const appName = uniq('app1');
|
||||
const libName = uniq('@my-org/lib1');
|
||||
|
||||
const port = await reservePort();
|
||||
process.env.PORT = `${port}`;
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app ${appName} --port=${port} --no-interactive --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
// check files are generated without the layout directory ("apps/") and
|
||||
// using the project name as the directory when no directory is provided
|
||||
checkFilesExist(`${appName}/src/main.ts`);
|
||||
// check build works
|
||||
expect(runCLI(`build ${appName}`)).toContain(
|
||||
`Successfully ran target build for project ${appName}`
|
||||
);
|
||||
// check tests pass
|
||||
const appTestResult = runCLI(`test ${appName} --passWithNoTests`);
|
||||
expect(appTestResult).toContain(
|
||||
`Successfully ran target test for project ${appName}`
|
||||
);
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:lib ${libName} --buildable --no-interactive --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
// check files are generated without the layout directory ("libs/") and
|
||||
// using the project name as the directory when no directory is provided
|
||||
checkFilesExist(`${libName}/src/index.ts`);
|
||||
// check build works
|
||||
expect(runCLI(`build ${libName}`)).toContain(
|
||||
`Successfully ran target build for project ${libName}`
|
||||
);
|
||||
// check tests pass
|
||||
const libTestResult = runCLI(`test ${libName}`);
|
||||
expect(libTestResult).toContain(
|
||||
`Successfully ran target test for project ${libName}`
|
||||
);
|
||||
}, 500_000);
|
||||
|
||||
// TODO(crystal, @ndcunningnam): Investigate why these tests are failing
|
||||
xdescribe('NestJS', () => {
|
||||
// TODO(crystal, @ndcunningham): What is the alternative here?
|
||||
xit('should have plugin output if specified in `tsPlugins`', async () => {
|
||||
const nestapp = uniq('nestapp');
|
||||
runCLI(
|
||||
`generate @nx/nest:app ${nestapp} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
packageInstall('@nestjs/swagger', undefined, '^11.0.0');
|
||||
|
||||
updateJson(join('apps', nestapp, 'project.json'), (config) => {
|
||||
config.targets.build.options.tsPlugins = ['@nestjs/swagger/plugin'];
|
||||
return config;
|
||||
});
|
||||
|
||||
updateFile(
|
||||
`apps/${nestapp}/src/app/foo.dto.ts`,
|
||||
`
|
||||
export class FooDto {
|
||||
foo: string;
|
||||
bar: number;
|
||||
}`
|
||||
);
|
||||
updateFile(
|
||||
`apps/${nestapp}/src/app/app.controller.ts`,
|
||||
`
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { FooDto } from './foo.dto';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getData() {
|
||||
return this.appService.getData();
|
||||
}
|
||||
|
||||
@Get('foo')
|
||||
getFoo(): Promise<FooDto> {
|
||||
return Promise.resolve({
|
||||
foo: 'foo',
|
||||
bar: 123
|
||||
})
|
||||
}
|
||||
}`
|
||||
);
|
||||
|
||||
await runCLIAsync(`build ${nestapp}`);
|
||||
|
||||
const mainJs = readFile(`dist/apps/${nestapp}/main.js`);
|
||||
expect(mainJs).toContain('FooDto');
|
||||
expect(mainJs).toContain('_OPENAPI_METADATA_FACTORY');
|
||||
}, 300000);
|
||||
});
|
||||
|
||||
describe('nest libraries', function () {
|
||||
it('should be able to generate a nest library', async () => {
|
||||
const nestlib = uniq('nestlib');
|
||||
runCLI(
|
||||
`generate @nx/nest:lib libs/${nestlib} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
const lintResults = runCLI(`lint ${nestlib}`);
|
||||
expect(lintResults).toContain('Successfully ran target lint');
|
||||
|
||||
const testResults = runCLI(`test ${nestlib} --passWithNoTests`);
|
||||
expect(testResults).toContain(
|
||||
`Successfully ran target test for project ${nestlib}`
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('should be able to generate a nest library w/ service', async () => {
|
||||
const nestlib = uniq('nestlib');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/nest:lib ${nestlib} --service --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
const lintResults = runCLI(`lint ${nestlib}`);
|
||||
expect(lintResults).toContain('Successfully ran target lint');
|
||||
|
||||
const jestResult = await runCLIAsync(`test ${nestlib}`);
|
||||
expect(jestResult.combinedOutput).toContain(
|
||||
'Test Suites: 1 passed, 1 total'
|
||||
);
|
||||
}, 200000);
|
||||
|
||||
it('should be able to generate a nest library w/ controller', async () => {
|
||||
const nestlib = uniq('nestlib');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/nest:lib ${nestlib} --controller --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
const lintResults = runCLI(`lint ${nestlib}`);
|
||||
expect(lintResults).toContain('Successfully ran target lint');
|
||||
|
||||
const jestResult = await runCLIAsync(`test ${nestlib}`);
|
||||
expect(jestResult.combinedOutput).toContain(
|
||||
'Test Suites: 1 passed, 1 total'
|
||||
);
|
||||
}, 200000);
|
||||
|
||||
it('should be able to generate a nest library w/ controller and service', async () => {
|
||||
const nestlib = uniq('nestlib');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/nest:lib ${nestlib} --controller --service --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
const lintResults = runCLI(`lint ${nestlib}`);
|
||||
expect(lintResults).toContain('Successfully ran target lint');
|
||||
|
||||
const jestResult = await runCLIAsync(`test ${nestlib}`);
|
||||
expect(jestResult.combinedOutput).toContain(
|
||||
'Test Suites: 2 passed, 2 total'
|
||||
);
|
||||
}, 200000);
|
||||
|
||||
it('should have plugin output if specified in `transformers`', async () => {
|
||||
const nestlib = uniq('nestlib');
|
||||
runCLI(
|
||||
`generate @nx/nest:lib libs/${nestlib} --buildable --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
packageInstall('@nestjs/swagger', undefined, '^11.0.0');
|
||||
|
||||
updateJson(join('libs', nestlib, 'project.json'), (config) => {
|
||||
config.targets.build.options.transformers = [
|
||||
{
|
||||
name: '@nestjs/swagger/plugin',
|
||||
options: {
|
||||
dtoFileNameSuffix: ['.model.ts'],
|
||||
},
|
||||
},
|
||||
];
|
||||
return config;
|
||||
});
|
||||
|
||||
updateFile(
|
||||
`libs/${nestlib}/src/lib/foo.model.ts`,
|
||||
`
|
||||
export class FooModel {
|
||||
foo?: string;
|
||||
bar?: number;
|
||||
}`
|
||||
);
|
||||
|
||||
await runCLIAsync(`build ${nestlib}`);
|
||||
|
||||
const fooModelJs = readFile(`dist/libs/${nestlib}/src/lib/foo.model.js`);
|
||||
expect(stripIndents`${fooModelJs}`).toContain(
|
||||
stripIndents`
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.FooModel = void 0;
|
||||
const openapi = require("@nestjs/swagger");
|
||||
class FooModel {
|
||||
static _OPENAPI_METADATA_FACTORY() {
|
||||
return { foo: { required: false, type: () => String }, bar: { required: false, type: () => Number } };
|
||||
}
|
||||
}
|
||||
exports.FooModel = FooModel;
|
||||
//# sourceMappingURL=foo.model.js.map
|
||||
`
|
||||
);
|
||||
}, 300000);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user