nx/e2e/node/src/node.test.ts
2024-02-05 10:16:08 -07:00

738 lines
22 KiB
TypeScript

import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { joinPathFragments } from '@nx/devkit';
import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
createFile,
detectPackageManager,
expectJestTestsToPass,
getPackageManagerCommand,
killPorts,
newProject,
packageInstall,
packageManagerLockFile,
promisifiedTreeKill,
readFile,
runCLI,
runCLIAsync,
runCommand,
runCommandUntil,
tmpProjPath,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { exec, 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 getRandomPort() {
return Math.floor(1000 + Math.random() * 9000);
}
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'],
});
});
afterAll(() => {
process.env.PORT = originalEnvPort;
cleanupProject();
});
it('should be able to generate an empty application', async () => {
const nodeapp = uniq('nodeapp');
const port = getRandomPort();
process.env.PORT = `${port}`;
runCLI(`generate @nx/node:app ${nodeapp} --port=${port} --linter=eslint`);
const lintResults = runCLI(`lint ${nodeapp}`);
expect(lintResults).toContain('Successfully ran target lint');
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 ${nodeapp} --linter=eslint`);
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 = getRandomPort();
process.env.PORT = `${port}`;
runCLI(
`generate @nx/node:app ${nodeapp} --port=${port} --linter=eslint --bundler=webpack`
);
const lintResults = runCLI(`lint ${nodeapp}`);
expect(lintResults).toContain('Successfully ran target lint');
updateFile(
`apps/${nodeapp}/webpack.config.js`,
`
const { NxWebpackPlugin } = require('@nx/webpack');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, '../../dist/apps/${nodeapp}'),
},
plugins: [
new NxWebpackPlugin({
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 ${nodeapp} --linter=eslint --bundler=webpack --framework=none`
);
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`);
}
);
try {
await promisifiedTreeKill(p.pid, 'SIGKILL');
await killPorts(port);
} finally {
process.env.port = originalEnvPort;
}
}, 60000);
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 ${nodeapp} --port=${port} --linter=eslint`
);
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}`)
);
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) {
expect(err).toBeFalsy();
}
}, 120_000);
xit('should be able to generate a nest application', async () => {
const nestapp = uniq('nestapp');
const port = 3335;
runCLI(`generate @nx/nest:app ${nestapp} --linter=eslint`);
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'
);
await runCLIAsync(`build ${nestapp}`);
checkFilesExist(
`dist/apps/${nestapp}/main.js`,
`dist/apps/${nestapp}/assets/file.txt`,
`dist/apps/${nestapp}/main.js.map`
);
const server = exec(`node ./dist/apps/${nestapp}/main.js`, {
cwd: tmpProjPath(),
});
expect(server).toBeTruthy();
// checking build
await new Promise((resolve) => {
server.stdout.on('data', async (data) => {
const message = data.toString();
if (message.includes(`Listening at http://localhost:${port}`)) {
const result = await getData(port);
expect(result.message).toEqual(`Welcome to ${nestapp}!`);
server.kill();
resolve(null);
}
});
});
// checking serve
const p = await runCommandUntil(
`serve ${nestapp} --port=${port}`,
(output) => {
process.stdout.write(output);
return output.includes(`Listening at http://localhost:${port}`);
}
);
const result = await getData(port);
expect(result.message).toEqual(`Welcome to ${nestapp}!`);
try {
await promisifiedTreeKill(p.pid, 'SIGKILL');
expect(await killPorts(port)).toBeTruthy();
} catch (err) {
expect(err).toBeFalsy();
}
}, 120000);
// 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 @nrwl/node:app ${esmapp} --linter=eslint --framework=none --bundler=webpack`
);
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');
});
p.kill();
}, 300000);
});
describe('Build Node apps', () => {
let scope: string;
beforeAll(() => {
originalEnvPort = process.env.PORT;
scope = newProject({
packages: ['@nx/node', '@nx/express', '@nx/nest'],
});
});
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 ${nestapp} --linter=eslint`);
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`);
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 = getRandomPort();
process.env.PORT = `${port}`;
runCLI(`generate @nx/node:app ${appName} --port=${port} --no-interactive`);
// 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`);
// Explicitly set `deleteOutputPath` 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 --deleteOutputPath`);
checkFilesDoNotExist(`dist/apps/${appName}/_should_remove.txt`);
checkFilesExist(`dist/apps/_should_not_remove.txt`);
// Explicitly set `deleteOutputPath` to false
createFile(`dist/apps/${appName}/_should_keep.txt`);
createFile(`dist/apps/_should_keep.txt`);
runCLI(`build ${appName} --deleteOutputPath=false --outputHashing none`);
checkFilesExist(`dist/apps/${appName}/_should_keep.txt`);
checkFilesExist(`dist/apps/_should_keep.txt`);
}, 120000);
it('should support generating projects with the new name and root format', () => {
const appName = uniq('app1');
const libName = uniq('@my-org/lib1');
const port = getRandomPort();
process.env.PORT = `${port}`;
runCLI(
`generate @nx/node:app ${appName} --project-name-and-root-format=as-provided --port=${port} --no-interactive`
);
// 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}`
);
// assert scoped project names are not supported when --project-name-and-root-format=derived
expect(() =>
runCLI(
`generate @nx/node:lib ${libName} --buildable --project-name-and-root-format=derived --no-interactive`
)
).toThrow();
runCLI(
`generate @nx/node:lib ${libName} --buildable --project-name-and-root-format=as-provided --no-interactive`
);
// 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`);
packageInstall('@nestjs/swagger', undefined, '^7.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 ${nestlib}`);
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`);
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`);
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`);
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 ${nestlib} --buildable`);
packageInstall('@nestjs/swagger', undefined, '^7.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);
it('should run default jest tests', async () => {
await expectJestTestsToPass('@nx/node:lib');
}, 100000);
});
});