diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index b004b08625f1c..7aed9ea63c73b 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -75,6 +75,12 @@ Default: `main` Default base to use for new projects +### framework + +Type: `string` + +Framework option to be used when the node-server preset is selected + ### help Type: `boolean` @@ -113,7 +119,7 @@ Package manager to use Type: `string` -Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "react-native", "expo", "next", "nest", "express", "react", "angular"]. To build your own see https://nx.dev/packages/nx-plugin#preset +Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "react-native", "expo", "next", "nest", "express", "react", "angular", "node-server"]. To build your own see https://nx.dev/packages/nx-plugin#preset ### skipGit diff --git a/docs/generated/packages/node/generators/application.json b/docs/generated/packages/node/generators/application.json index 9f0a21054d22e..b50e1dd1408b8 100644 --- a/docs/generated/packages/node/generators/application.json +++ b/docs/generated/packages/node/generators/application.json @@ -89,6 +89,12 @@ "description": "The port which the server will be run on", "type": "number", "default": 3000 + }, + "rootProject": { + "description": "Create node application at the root of the workspace", + "type": "boolean", + "default": false, + "hidden": true } }, "required": [], diff --git a/docs/generated/packages/nx/documents/create-nx-workspace.md b/docs/generated/packages/nx/documents/create-nx-workspace.md index b004b08625f1c..7aed9ea63c73b 100644 --- a/docs/generated/packages/nx/documents/create-nx-workspace.md +++ b/docs/generated/packages/nx/documents/create-nx-workspace.md @@ -75,6 +75,12 @@ Default: `main` Default base to use for new projects +### framework + +Type: `string` + +Framework option to be used when the node-server preset is selected + ### help Type: `boolean` @@ -113,7 +119,7 @@ Package manager to use Type: `string` -Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "react-native", "expo", "next", "nest", "express", "react", "angular"]. To build your own see https://nx.dev/packages/nx-plugin#preset +Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "react-native", "expo", "next", "nest", "express", "react", "angular", "node-server"]. To build your own see https://nx.dev/packages/nx-plugin#preset ### skipGit diff --git a/docs/generated/packages/workspace/generators/new.json b/docs/generated/packages/workspace/generators/new.json index 00645254b131a..e4656a210b817 100644 --- a/docs/generated/packages/workspace/generators/new.json +++ b/docs/generated/packages/workspace/generators/new.json @@ -59,6 +59,11 @@ "description": "The package manager used to install dependencies.", "type": "string", "enum": ["npm", "yarn", "pnpm"] + }, + "framework": { + "description": "The framework which the application is using", + "type": "string", + "enum": ["express", "koa", "fastify", "connect"] } }, "additionalProperties": true, diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index 48214543d03ad..8d8290f855434 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -57,6 +57,11 @@ "description": "The package manager used to install dependencies.", "type": "string", "enum": ["npm", "yarn", "pnpm"] + }, + "framework": { + "description": "The framework which the application is using", + "type": "string", + "enum": ["express", "koa", "fastify", "connect"] } }, "presets": [] diff --git a/e2e/node/src/node.test.ts b/e2e/node/src/node.test.ts index 68ea8f4cd734c..eb2ab9dede6a1 100644 --- a/e2e/node/src/node.test.ts +++ b/e2e/node/src/node.test.ts @@ -126,9 +126,9 @@ describe('Node Applications', () => { it('should be able to generate an express application', async () => { const nodeapp = uniq('nodeapp'); - const originalEnvPort = process.env.port; + const originalEnvPort = process.env.PORT; const port = 3333; - process.env.port = `${port}`; + process.env.PORT = `${port}`; runCLI(`generate @nrwl/express:app ${nodeapp} --linter=eslint`); const lintResults = runCLI(`lint ${nodeapp}`); diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 1ec707d161d6f..0f3f638b4e3a3 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -31,6 +31,7 @@ type Arguments = { appName: string; cli: string; style: string; + framework: string; nxCloud: boolean; allPrompts: boolean; packageManager: PackageManager; @@ -62,6 +63,7 @@ enum Preset { Express = 'express', React = 'react', Angular = 'angular', + NodeServer = 'node-server', } const presetOptions: { name: Preset; message: string }[] = [ @@ -96,6 +98,11 @@ const presetOptions: { name: Preset; message: string }[] = [ message: 'react-native [a monorepo with a single React Native application]', }, + { + name: Preset.NodeServer, + message: + 'node [a standalone repo with a single Node Server e.g. Express]', + }, ]; const nxVersion = require('../package.json').version; @@ -145,6 +152,10 @@ export const commandsObject: yargs.Argv = yargs describe: chalk.dim`Style option to be used when a preset with pregenerated app is selected`, type: 'string', }) + .option('framework', { + describe: chalk.dim`Framework option to be used when the node-server preset is selected`, + type: 'string', + }) .option('nxCloud', { describe: chalk.dim(messages.getPromptMessage('nxCloudCreation')), type: 'boolean', @@ -224,6 +235,7 @@ async function main(parsedArgs: yargs.Arguments) { ci, skipGit, commit, + framework, } = parsedArgs; output.log({ @@ -248,6 +260,7 @@ async function main(parsedArgs: yargs.Arguments) { style, nxCloud, defaultBase, + framework, } ); @@ -300,7 +313,7 @@ async function getConfiguration( argv: yargs.Arguments ): Promise { try { - let name, appName, style, preset; + let name, appName, style, preset, framework; output.log({ title: @@ -343,6 +356,9 @@ async function getConfiguration( } else { name = await determineRepoName(argv); appName = await determineAppName(preset, argv); + if (preset === Preset.NodeServer) { + framework = await determineFramework(preset, argv); + } } style = await determineStyle(preset, argv); } @@ -358,6 +374,7 @@ async function getConfiguration( preset, appName, style, + framework, cli, nxCloud, packageManager, @@ -643,6 +660,47 @@ async function determineAppName( }); } +async function determineFramework( + preset: Preset, + parsedArgs: yargs.Arguments +): Promise { + if (preset !== Preset.NodeServer) { + return Promise.resolve(''); + } + + const frameworkChoices = ['express', 'koa', 'fastify', 'connect']; + + if (!parsedArgs.framework) { + return enquirer + .prompt([ + { + message: 'What framework should be used?', + type: 'select', + name: 'framework', + choices: frameworkChoices, + }, + ]) + .then((a: { framework: string }) => a.framework); + } + + const foundFramework = frameworkChoices.indexOf(parsedArgs.framework); + + if (foundFramework < 0) { + output.error({ + title: 'Invalid framwork', + bodyLines: [ + `It must be one of the following:`, + '', + ...frameworkChoices.map((choice) => choice), + ], + }); + + process.exit(1); + } + + return Promise.resolve(parsedArgs.framework); +} + function isValidCli(cli: string): cli is 'angular' | 'nx' { return ['nx', 'angular'].indexOf(cli) !== -1; } @@ -685,7 +743,8 @@ async function determineStyle( preset === Preset.Nest || preset === Preset.Express || preset === Preset.ReactNative || - preset === Preset.Expo + preset === Preset.Expo || + preset === Preset.NodeServer ) { return Promise.resolve(null); } @@ -1182,6 +1241,7 @@ function pointToTutorialAndCourse(preset: Preset) { }); break; case Preset.Express: + case Preset.NodeServer: output.addVerticalSeparator(); output.note({ title, diff --git a/packages/express/src/generators/application/application.spec.ts b/packages/express/src/generators/application/application.spec.ts index 45ce6ec1e12cb..0843593f2677b 100644 --- a/packages/express/src/generators/application/application.spec.ts +++ b/packages/express/src/generators/application/application.spec.ts @@ -16,11 +16,14 @@ describe('app', () => { } as Schema); const mainFile = appTree.read('apps/my-node-app/src/main.ts').toString(); - expect(mainFile).toContain(`import * as express from 'express';`); + expect(mainFile).toContain(`import express from 'express';`); const tsconfig = readJson(appTree, 'apps/my-node-app/tsconfig.json'); expect(tsconfig).toMatchInlineSnapshot(` Object { + "compilerOptions": Object { + "esModuleInterop": true, + }, "extends": "../../tsconfig.base.json", "files": Array [], "include": Array [], @@ -111,12 +114,13 @@ Object { expect(appTree.exists('apps/my-node-app/src/main.js')).toBeTruthy(); expect(appTree.read('apps/my-node-app/src/main.js').toString()).toContain( - `import * as express from 'express';` + `import express from 'express';` ); const tsConfig = readJson(appTree, 'apps/my-node-app/tsconfig.json'); expect(tsConfig.compilerOptions).toEqual({ allowJs: true, + esModuleInterop: true, }); const tsConfigApp = readJson( diff --git a/packages/express/src/generators/application/application.ts b/packages/express/src/generators/application/application.ts index 57dec0160b290..c805f97e09322 100644 --- a/packages/express/src/generators/application/application.ts +++ b/packages/express/src/generators/application/application.ts @@ -37,7 +37,7 @@ function addMainFile(tree: Tree, options: NormalizedSchema) { * This is only a minimal backend to get started. */ -import * as express from 'express'; +import express from 'express'; import * as path from 'path'; const app = express(); @@ -48,7 +48,7 @@ app.get('/api', (req, res) => { res.send({ message: 'Welcome to ${options.name}!' }); }); -const port = process.env.port || 3333; +const port = process.env.PORT || 3333; const server = app.listen(port, () => { console.log(\`Listening at http://localhost:\${port}/api\`); }); diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index 9da9d5779fd56..f1c4c81f24f11 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -41,6 +41,7 @@ describe('app', () => { await applicationGenerator(tree, { name: 'myNodeApp', standaloneConfig: false, + bundler: 'webpack', }); const project = readProjectConfiguration(tree, 'my-node-app'); expect(project.root).toEqual('my-node-app'); @@ -116,6 +117,9 @@ describe('app', () => { const tsconfig = readJson(tree, 'my-node-app/tsconfig.json'); expect(tsconfig).toMatchInlineSnapshot(` Object { + "compilerOptions": Object { + "esModuleInterop": true, + }, "extends": "../tsconfig.base.json", "files": Array [], "include": Array [], @@ -401,6 +405,7 @@ describe('app', () => { const tsConfig = readJson(tree, 'my-node-app/tsconfig.json'); expect(tsConfig.compilerOptions).toEqual({ allowJs: true, + esModuleInterop: true, }); const tsConfigApp = readJson(tree, 'my-node-app/tsconfig.app.json'); diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 5c9722f3fb7bd..23c641a2b64c0 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -27,7 +27,7 @@ import { Linter, lintProjectGenerator } from '@nrwl/linter'; import { jestProjectGenerator } from '@nrwl/jest'; import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; -import { Schema } from './schema'; +import { NodeJsFrameWorks, Schema } from './schema'; import { initGenerator } from '../init/init'; import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript'; import { @@ -43,6 +43,8 @@ import { } from '../../utils/versions'; import { prompt } from 'enquirer'; +import * as shared from '@nrwl/workspace/src/utils/create-ts-config'; + export interface NormalizedSchema extends Schema { appProjectRoot: string; parsedTags: string[]; @@ -290,14 +292,27 @@ function addProjectDependencies( } 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, - }, - })); + updateJson(tree, `${options.appProjectRoot}/tsconfig.json`, (json) => { + if (options.rootProject) { + return { + compilerOptions: { + ...shared.tsConfigBaseOptions, + ...json.compilerOptions, + }, + ...json, + extends: undefined, + exclude: ['node_modules', 'tmp'], + }; + } else { + return { + ...json, + compilerOptions: { + ...json.compilerOptions, + esModuleInterop: true, + }, + }; + } + }); } export async function applicationGenerator(tree: Tree, schema: Schema) { @@ -313,9 +328,8 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { addProjectDependencies(tree, options); addAppFiles(tree, options); addProject(tree, options); - if (options.framework && options?.bundler === 'esbuild') { - updateTsConfigOptions(tree, options); - } + + updateTsConfigOptions(tree, options); if (options.linter !== Linter.None) { const lintTask = await addLintingToApplication(tree, { @@ -365,7 +379,11 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-'); - const appProjectRoot = joinPathFragments(appsDir, appDirectory); + const appProjectRoot = options.rootProject + ? '.' + : joinPathFragments(appsDir, appDirectory); + + options.bundler = options.bundler ?? 'esbuild'; const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) diff --git a/packages/node/src/generators/application/schema.d.ts b/packages/node/src/generators/application/schema.d.ts index 5ebffdc73a4bf..497abd5db2915 100644 --- a/packages/node/src/generators/application/schema.d.ts +++ b/packages/node/src/generators/application/schema.d.ts @@ -17,6 +17,7 @@ export interface Schema { bundler?: 'esbuild' | 'webpack'; framework?: NodeJsFrameWorks; port?: number; + rootProject?: boolean; } 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 580505921c02f..ee86c6830ef46 100644 --- a/packages/node/src/generators/application/schema.json +++ b/packages/node/src/generators/application/schema.json @@ -89,6 +89,12 @@ "description": "The port which the server will be run on", "type": "number", "default": 3000 + }, + "rootProject": { + "description": "Create node application at the root of the workspace", + "type": "boolean", + "default": false, + "hidden": true } }, "required": [] diff --git a/packages/workspace/src/generators/new/__snapshots__/generate-workspace-files.spec.ts.snap b/packages/workspace/src/generators/new/__snapshots__/generate-workspace-files.spec.ts.snap index 997ebb9aacbf1..8ab5adae0fcf1 100644 --- a/packages/workspace/src/generators/new/__snapshots__/generate-workspace-files.spec.ts.snap +++ b/packages/workspace/src/generators/new/__snapshots__/generate-workspace-files.spec.ts.snap @@ -230,6 +230,31 @@ Visit the [Nx Documentation](https://nx.dev) to learn more. " `; +exports[`@nrwl/workspace:generateWorkspaceFiles README.md should be created for NodeServer preset 1`] = ` +"# Proj + + + +✨ **This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)** ✨ + +## Development server + +Run \`nx serve app1\` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files. + +## Understand this workspace + +Run \`nx graph\` to see a diagram of the dependencies of the projects. + +## Remote caching + +Run \`npx nx connect-to-nx-cloud\` to enable [remote caching](https://nx.app) and make CI faster. + +## Further help + +Visit the [Nx Documentation](https://nx.dev) to learn more. +" +`; + exports[`@nrwl/workspace:generateWorkspaceFiles README.md should be created for ReactMonorepo preset 1`] = ` "# Proj diff --git a/packages/workspace/src/generators/new/generate-preset.ts b/packages/workspace/src/generators/new/generate-preset.ts index 21acc0dcd5362..f00c6781f00b6 100644 --- a/packages/workspace/src/generators/new/generate-preset.ts +++ b/packages/workspace/src/generators/new/generate-preset.ts @@ -73,6 +73,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) { opts.linter ? `--linter=${opts.linter}` : null, opts.npmScope ? `--npmScope=${opts.npmScope}` : `--npmScope=${opts.name}`, opts.preset ? `--preset=${opts.preset}` : null, + opts.framework ? `--framework=${opts.framework}` : null, opts.packageManager ? `--packageManager=${opts.packageManager}` : null, parsedArgs.interactive ? '--interactive=true' : '--interactive=false', ].filter((e) => !!e); @@ -112,6 +113,9 @@ function getPresetDependencies(preset: string, version?: string) { case Preset.WebComponents: return { dependencies: {}, dev: { '@nrwl/web': nxVersion } }; + case Preset.NodeServer: + return { dependencies: {}, dev: { '@nrwl/node': nxVersion } }; + default: { return { dev: {}, diff --git a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts index 3f4f81d97a979..f582f355c6d19 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts @@ -42,6 +42,7 @@ describe('@nrwl/workspace:generateWorkspaceFiles', () => { Preset.NextJs, Preset.WebComponents, Preset.Express, + Preset.NodeServer, ].includes(Preset[preset]) ) { appName = 'app1'; diff --git a/packages/workspace/src/generators/new/generate-workspace-files.ts b/packages/workspace/src/generators/new/generate-workspace-files.ts index 0d97d656e0550..52f9f3824a9b0 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.ts @@ -76,7 +76,8 @@ function createAppsAndLibsFolders(tree: Tree, options: NormalizedSchema) { tree.write(join(options.directory, 'packages/.gitkeep'), ''); } else if ( options.preset === Preset.AngularStandalone || - options.preset === Preset.ReactStandalone + options.preset === Preset.ReactStandalone || + options.preset === Preset.NodeServer ) { // don't generate any folders } else { @@ -134,7 +135,8 @@ function createFiles(tree: Tree, options: NormalizedSchema) { const formattedNames = names(options.name); const filesDirName = options.preset === Preset.AngularStandalone || - options.preset === Preset.ReactStandalone + options.preset === Preset.ReactStandalone || + options.preset === Preset.NodeServer ? './files-root-app' : options.preset === Preset.NPM || options.preset === Preset.Core ? './files-package-based-repo' @@ -196,7 +198,8 @@ function createYarnrcYml(tree: Tree, options: NormalizedSchema) { function addNpmScripts(tree: Tree, options: NormalizedSchema) { if ( options.preset === Preset.AngularStandalone || - options.preset === Preset.ReactStandalone + options.preset === Preset.ReactStandalone || + options.preset === Preset.NodeServer ) { updateJson(tree, join(options.directory, 'package.json'), (json) => { Object.assign(json.scripts, { diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index 688b923a09e3f..fabdd87ddbc56 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -24,6 +24,7 @@ interface Schema { nxCloud?: boolean; preset: string; defaultBase: string; + framework?: string; linter?: Linter; packageManager?: PackageManager; } @@ -74,6 +75,12 @@ function validateOptions(options: Schema, host: Tree) { throw new Error(`Cannot select nxCloud when skipInstall is set to true.`); } + if (options.preset === Preset.NodeServer && !options.framework) { + throw new Error( + `Cannot generate ${options.preset} without selecting a framework` + ); + } + if (devkitGetWorkspacePath(host)) { throw new Error( 'Cannot generate a new workspace within an existing workspace' diff --git a/packages/workspace/src/generators/new/schema.json b/packages/workspace/src/generators/new/schema.json index 224497bfc511a..6e4f895785333 100644 --- a/packages/workspace/src/generators/new/schema.json +++ b/packages/workspace/src/generators/new/schema.json @@ -62,6 +62,11 @@ "description": "The package manager used to install dependencies.", "type": "string", "enum": ["npm", "yarn", "pnpm"] + }, + "framework": { + "description": "The framework which the application is using", + "type": "string", + "enum": ["express", "koa", "fastify", "connect"] } }, "additionalProperties": true diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index beb5bf9f28482..1662c3924d747 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -134,6 +134,16 @@ async function createPreset(tree: Tree, options: Schema) { libsDir: 'packages', }; updateNxJson(tree, c); + } else if (options.preset === Preset.NodeServer) { + const { applicationGenerator: nodeApplicationGenerator } = require('@nrwl' + + '/node'); + await nodeApplicationGenerator(tree, { + name: options.name, + linter: options.linter, + standaloneConfig: options.standaloneConfig, + framework: options.framework, + rootProject: true, + }); } else { throw new Error(`Invalid preset ${options.preset}`); } diff --git a/packages/workspace/src/generators/preset/schema.d.ts b/packages/workspace/src/generators/preset/schema.d.ts index 50be2f7e3dd27..ea7ed2958b4ad 100644 --- a/packages/workspace/src/generators/preset/schema.d.ts +++ b/packages/workspace/src/generators/preset/schema.d.ts @@ -9,5 +9,6 @@ export interface Schema { linter?: string; preset: Preset; standaloneConfig?: boolean; + framework?: string; packageManager?: PackageManager; } diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index eadec1541cf8e..846d1202501a7 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -63,6 +63,11 @@ "description": "The package manager used to install dependencies.", "type": "string", "enum": ["npm", "yarn", "pnpm"] + }, + "framework": { + "description": "The framework which the application is using", + "type": "string", + "enum": ["express", "koa", "fastify", "connect"] } } } diff --git a/packages/workspace/src/generators/utils/presets.ts b/packages/workspace/src/generators/utils/presets.ts index a1b89bc8573bc..9f24aa394e17b 100644 --- a/packages/workspace/src/generators/utils/presets.ts +++ b/packages/workspace/src/generators/utils/presets.ts @@ -14,4 +14,5 @@ export enum Preset { NextJs = 'next', Nest = 'nest', Express = 'express', + NodeServer = 'node-server', }