From 48d3d179e61fb092bf7103170ff3362995b48572 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Fri, 6 Jan 2023 09:13:08 -0700 Subject: [PATCH] feat(node): support nodejs frameworks to handle scaffolding using --framework --- .../packages/node/generators/application.json | 16 ++ packages/node/package.json | 1 + .../application/application.spec.ts | 12 -- .../src/generators/application/application.ts | 174 +++++++++++++++--- .../environments/environment.prod.ts__tmpl__ | 3 - .../src/environments/environment.ts__tmpl__ | 3 - .../application/files/app/src/main.ts__tmpl__ | 1 - .../files/{app => common}/src/app/.gitkeep | 0 .../files/{app => common}/src/assets/.gitkeep | 0 .../files/common/src/main.ts__tmpl__ | 1 + .../files/{app => common}/tsconfig.app.json | 0 .../files/{app => common}/tsconfig.json | 0 .../files/express/src/main.ts__tmpl__ | 11 ++ .../src/generators/application/schema.d.ts | 5 + .../src/generators/application/schema.json | 16 ++ packages/node/src/utils/versions.ts | 13 ++ 16 files changed, 213 insertions(+), 43 deletions(-) delete mode 100644 packages/node/src/generators/application/files/app/src/environments/environment.prod.ts__tmpl__ delete mode 100644 packages/node/src/generators/application/files/app/src/environments/environment.ts__tmpl__ delete mode 100644 packages/node/src/generators/application/files/app/src/main.ts__tmpl__ rename packages/node/src/generators/application/files/{app => common}/src/app/.gitkeep (100%) rename packages/node/src/generators/application/files/{app => common}/src/assets/.gitkeep (100%) create mode 100644 packages/node/src/generators/application/files/common/src/main.ts__tmpl__ rename packages/node/src/generators/application/files/{app => common}/tsconfig.app.json (100%) rename packages/node/src/generators/application/files/{app => common}/tsconfig.json (100%) create mode 100644 packages/node/src/generators/application/files/express/src/main.ts__tmpl__ diff --git a/docs/generated/packages/node/generators/application.json b/docs/generated/packages/node/generators/application.json index acf273ceb34d7..9f0a21054d22e 100644 --- a/docs/generated/packages/node/generators/application.json +++ b/docs/generated/packages/node/generators/application.json @@ -73,6 +73,22 @@ "standaloneConfig": { "description": "Split the project configuration into `/project.json` rather than including it inside `workspace.json`.", "type": "boolean" + }, + "bundler": { + "description": "Bundler which is used to package the application", + "type": "string", + "enum": ["esbuild", "webpack"], + "default": "esbuild" + }, + "framework": { + "description": "Generate the node application using a framework", + "type": "string", + "enum": ["express", "koa", "fastify", "connect"] + }, + "port": { + "description": "The port which the server will be run on", + "type": "number", + "default": 3000 } }, "required": [], diff --git a/packages/node/package.json b/packages/node/package.json index 9e1937d58231b..f0e9886d246f5 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -38,6 +38,7 @@ "@nrwl/webpack": "file:../webpack", "@nrwl/workspace": "file:../workspace", "chalk": "4.1.0", + "enquirer": "~2.3.6", "tslib": "^2.3.0" }, "publishConfig": { diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index 237ec5f5fa16a..0d8199c85c499 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -59,12 +59,6 @@ describe('app', () => { optimization: true, extractLicenses: true, inspect: false, - fileReplacements: [ - { - replace: 'apps/my-node-app/src/environments/environment.ts', - with: 'apps/my-node-app/src/environments/environment.prod.ts', - }, - ], }, }, }, @@ -434,12 +428,6 @@ describe('app', () => { const buildTarget = project.architect.build; expect(buildTarget.options.main).toEqual('apps/my-node-app/src/main.js'); - expect(buildTarget.configurations.production.fileReplacements).toEqual([ - { - replace: 'apps/my-node-app/src/environments/environment.js', - with: 'apps/my-node-app/src/environments/environment.prod.js', - }, - ]); }); it('should generate js files for nested libs as well', async () => { diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 4d51745a09777..07293e4615320 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -1,4 +1,5 @@ import { + addDependenciesToPackageJson, addProjectConfiguration, convertNxGenerator, extractLayoutDirectory, @@ -15,6 +16,7 @@ import { TargetConfiguration, toJS, Tree, + updateJson, updateProjectConfiguration, updateTsConfigsToJs, } from '@nrwl/devkit'; @@ -28,18 +30,30 @@ import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-ser import { Schema } from './schema'; import { initGenerator } from '../init/init'; import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript'; +import { + connectTypingsVersion, + connectVersion, + esbuildVersion, + expressTypingsVersion, + expressVersion, + fastifyVersion, + koaTypingsVersion, + koaVersion, + nxVersion, +} from '../../utils/versions'; +import { prompt } from 'enquirer'; export interface NormalizedSchema extends Schema { appProjectRoot: string; parsedTags: string[]; } -function getBuildConfig( +function getWebpackBuildConfig( project: ProjectConfiguration, options: NormalizedSchema ): TargetConfiguration { return { - executor: '@nrwl/webpack:webpack', + executor: `@nrwl/webpack:webpack`, outputs: ['{options.outputPath}'], options: { target: 'node', @@ -57,23 +71,31 @@ function getBuildConfig( optimization: true, extractLicenses: true, inspect: false, - fileReplacements: [ - { - replace: joinPathFragments( - project.sourceRoot, - 'environments/environment' + (options.js ? '.js' : '.ts') - ), - with: joinPathFragments( - project.sourceRoot, - 'environments/environment.prod' + (options.js ? '.js' : '.ts') - ), - }, - ], }, }, }; } +function getEsBuildConfig( + project: ProjectConfiguration, + options: NormalizedSchema +): TargetConfiguration { + return { + executor: '@nrwl/esbuild:esbuild', + outputs: ['{options.outputPath}'], + options: { + outputPath: joinPathFragments('dist', options.appProjectRoot), + format: ['cjs'], + main: joinPathFragments( + project.sourceRoot, + 'main' + (options.js ? '.js' : '.ts') + ), + tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), + assets: [joinPathFragments(project.sourceRoot, 'assets')], + }, + }; +} + function getServeConfig(options: NormalizedSchema): TargetConfiguration { return { executor: '@nrwl/js:node', @@ -96,7 +118,10 @@ function addProject(tree: Tree, options: NormalizedSchema) { targets: {}, tags: options.parsedTags, }; - project.targets.build = getBuildConfig(project, options); + project.targets.build = + options.bundler === 'esbuild' + ? getEsBuildConfig(project, options) + : getWebpackBuildConfig(project, options); project.targets.serve = getServeConfig(options); addProjectConfiguration( @@ -108,16 +133,41 @@ function addProject(tree: Tree, options: NormalizedSchema) { } function addAppFiles(tree: Tree, options: NormalizedSchema) { - generateFiles(tree, join(__dirname, './files/app'), options.appProjectRoot, { - tmpl: '', - name: options.name, - root: options.appProjectRoot, - offset: offsetFromRoot(options.appProjectRoot), - rootTsConfigPath: getRelativePathToRootTsConfig( + generateFiles( + tree, + join(__dirname, './files/common'), + options.appProjectRoot, + { + ...options, + tmpl: '', + name: options.name, + root: options.appProjectRoot, + offset: offsetFromRoot(options.appProjectRoot), + rootTsConfigPath: getRelativePathToRootTsConfig( + tree, + options.appProjectRoot + ), + } + ); + + if (options.framework) { + generateFiles( tree, - options.appProjectRoot - ), - }); + join(__dirname, `./files/${options.framework}`), + options.appProjectRoot, + { + ...options, + tmpl: '', + name: options.name, + root: options.appProjectRoot, + offset: offsetFromRoot(options.appProjectRoot), + rootTsConfigPath: getRelativePathToRootTsConfig( + tree, + options.appProjectRoot + ), + } + ); + } if (options.js) { toJS(tree); } @@ -189,7 +239,73 @@ export async function addLintingToApplication( return lintTask; } +function addProjectDependencies( + tree: Tree, + options: NormalizedSchema +): GeneratorCallback { + const bundlers = { + webpack: { + '@nrwl/webpack': nxVersion, + }, + esbuild: { + '@nrwl/esbuild': nxVersion, + esbuild: esbuildVersion, + }, + }; + + const frameworkDependencies = { + express: { + express: expressVersion, + '@types/express': expressTypingsVersion, + }, + koa: { + koa: koaVersion, + '@types/koa': koaTypingsVersion, + }, + fastify: { + fastify: fastifyVersion, + }, + connect: { + connect: connectVersion, + '@types/connect': connectTypingsVersion, + }, + }; + return addDependenciesToPackageJson( + tree, + {}, + { + ...frameworkDependencies[options.framework], + ...bundlers[options.bundler], + } + ); +} + +function updateTsConfigOptions(tree: Tree, options: NormalizedSchema) { + // updatae tsconfig.app.json to typecheck default exports https://www.typescriptlang.org/tsconfig#esModuleInterop + updateJson(tree, `${options.appProjectRoot}/tsconfig.app.json`, (json) => ({ + ...json, + compilerOptions: { + ...json.compilerOptions, + esModuleInterop: true, + }, + })); +} + export async function applicationGenerator(tree: Tree, schema: Schema) { + // Prompt for bundler webpack / esbuild + if (schema.framework) { + schema.bundler = ( + await prompt<{ bundler: 'esbuild' | 'webpack' }>([ + { + message: 'What bundler would you like to use?', + type: 'select', + name: 'bundler', + choices: ['esbuild', 'webpack'], + }, + ]) + ).bundler; + } + const options = normalizeOptions(tree, schema); const tasks: GeneratorCallback[] = []; @@ -199,8 +315,12 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { }); tasks.push(initTask); + addProjectDependencies(tree, options); addAppFiles(tree, options); addProject(tree, options); + if (options.framework && options?.bundler === 'esbuild') { + updateTsConfigOptions(tree, options); + } if (options.linter !== Linter.None) { const lintTask = await addLintingToApplication(tree, { @@ -251,6 +371,11 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-'); const appProjectRoot = joinPathFragments(appsDir, appDirectory); + if (options.framework) { + options.bundler = options.bundler ?? 'esbuild'; + } else { + options.bundler = 'webpack'; + } const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) @@ -266,6 +391,7 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { parsedTags, linter: options.linter ?? Linter.EsLint, unitTestRunner: options.unitTestRunner ?? 'jest', + port: options.port ?? 3000, }; } diff --git a/packages/node/src/generators/application/files/app/src/environments/environment.prod.ts__tmpl__ b/packages/node/src/generators/application/files/app/src/environments/environment.prod.ts__tmpl__ deleted file mode 100644 index 3612073bc31cd..0000000000000 --- a/packages/node/src/generators/application/files/app/src/environments/environment.prod.ts__tmpl__ +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true -}; diff --git a/packages/node/src/generators/application/files/app/src/environments/environment.ts__tmpl__ b/packages/node/src/generators/application/files/app/src/environments/environment.ts__tmpl__ deleted file mode 100644 index ffe8aed766425..0000000000000 --- a/packages/node/src/generators/application/files/app/src/environments/environment.ts__tmpl__ +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: false -}; diff --git a/packages/node/src/generators/application/files/app/src/main.ts__tmpl__ b/packages/node/src/generators/application/files/app/src/main.ts__tmpl__ deleted file mode 100644 index a420803c09eec..0000000000000 --- a/packages/node/src/generators/application/files/app/src/main.ts__tmpl__ +++ /dev/null @@ -1 +0,0 @@ -console.log('Hello World!'); diff --git a/packages/node/src/generators/application/files/app/src/app/.gitkeep b/packages/node/src/generators/application/files/common/src/app/.gitkeep similarity index 100% rename from packages/node/src/generators/application/files/app/src/app/.gitkeep rename to packages/node/src/generators/application/files/common/src/app/.gitkeep diff --git a/packages/node/src/generators/application/files/app/src/assets/.gitkeep b/packages/node/src/generators/application/files/common/src/assets/.gitkeep similarity index 100% rename from packages/node/src/generators/application/files/app/src/assets/.gitkeep rename to packages/node/src/generators/application/files/common/src/assets/.gitkeep diff --git a/packages/node/src/generators/application/files/common/src/main.ts__tmpl__ b/packages/node/src/generators/application/files/common/src/main.ts__tmpl__ new file mode 100644 index 0000000000000..73c02658c48d9 --- /dev/null +++ b/packages/node/src/generators/application/files/common/src/main.ts__tmpl__ @@ -0,0 +1 @@ +console.log('Hello World'); \ No newline at end of file diff --git a/packages/node/src/generators/application/files/app/tsconfig.app.json b/packages/node/src/generators/application/files/common/tsconfig.app.json similarity index 100% rename from packages/node/src/generators/application/files/app/tsconfig.app.json rename to packages/node/src/generators/application/files/common/tsconfig.app.json diff --git a/packages/node/src/generators/application/files/app/tsconfig.json b/packages/node/src/generators/application/files/common/tsconfig.json similarity index 100% rename from packages/node/src/generators/application/files/app/tsconfig.json rename to packages/node/src/generators/application/files/common/tsconfig.json diff --git a/packages/node/src/generators/application/files/express/src/main.ts__tmpl__ b/packages/node/src/generators/application/files/express/src/main.ts__tmpl__ new file mode 100644 index 0000000000000..8c7daef188233 --- /dev/null +++ b/packages/node/src/generators/application/files/express/src/main.ts__tmpl__ @@ -0,0 +1,11 @@ +import express from 'express'; +const app = express(); + + +app.get('/', (req, res) => { + res.send('Hello from Nrwl 🐳 API'); +}); + +app.listen(<%= port %>, () => { + // Server is running +}); \ No newline at end of file diff --git a/packages/node/src/generators/application/schema.d.ts b/packages/node/src/generators/application/schema.d.ts index 4564398f11aad..5ebffdc73a4bf 100644 --- a/packages/node/src/generators/application/schema.d.ts +++ b/packages/node/src/generators/application/schema.d.ts @@ -14,4 +14,9 @@ export interface Schema { pascalCaseFiles?: boolean; setParserOptionsProject?: boolean; standaloneConfig?: boolean; + bundler?: 'esbuild' | 'webpack'; + framework?: NodeJsFrameWorks; + port?: number; } + +export type NodeJsFrameWorks = 'express' | 'koa' | 'fastify' | 'connect'; diff --git a/packages/node/src/generators/application/schema.json b/packages/node/src/generators/application/schema.json index 46b617010860e..580505921c02f 100644 --- a/packages/node/src/generators/application/schema.json +++ b/packages/node/src/generators/application/schema.json @@ -73,6 +73,22 @@ "standaloneConfig": { "description": "Split the project configuration into `/project.json` rather than including it inside `workspace.json`.", "type": "boolean" + }, + "bundler": { + "description": "Bundler which is used to package the application", + "type": "string", + "enum": ["esbuild", "webpack"], + "default": "esbuild" + }, + "framework": { + "description": "Generate the node application using a framework", + "type": "string", + "enum": ["express", "koa", "fastify", "connect"] + }, + "port": { + "description": "The port which the server will be run on", + "type": "number", + "default": 3000 } }, "required": [] diff --git a/packages/node/src/utils/versions.ts b/packages/node/src/utils/versions.ts index 87baad502a439..049060134f7da 100644 --- a/packages/node/src/utils/versions.ts +++ b/packages/node/src/utils/versions.ts @@ -3,3 +3,16 @@ export const nxVersion = require('../../package.json').version; export const tslibVersion = '^2.3.0'; export const typesNodeVersion = '18.7.1'; + +export const esbuildVersion = '^0.15.7'; + +export const expressVersion = '^4.18.1'; +export const expressTypingsVersion = '4.17.13'; + +export const koaVersion = '2.14.1'; +export const koaTypingsVersion = '2.13.5'; + +export const fastifyVersion = '4.11.0'; + +export const connectVersion = '3.7.0'; +export const connectTypingsVersion = '3.4.35';