diff --git a/docs/generated/packages/web/generators/application.json b/docs/generated/packages/web/generators/application.json index 7ddf805e10833f..3f9633366df50a 100644 --- a/docs/generated/packages/web/generators/application.json +++ b/docs/generated/packages/web/generators/application.json @@ -66,6 +66,11 @@ "enum": ["eslint", "none"], "default": "eslint" }, + "strict": { + "type": "boolean", + "description": "Creates an application with strict mode and strict type checking.", + "default": true + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean", diff --git a/packages/web/src/generators/application/application.spec.ts b/packages/web/src/generators/application/application.spec.ts index 6d7a0f0de4479d..0dd8ae5064d012 100644 --- a/packages/web/src/generators/application/application.spec.ts +++ b/packages/web/src/generators/application/application.spec.ts @@ -549,6 +549,18 @@ describe('app', () => { expect(tree.exists('my-app/.babelrc')).toBeFalsy(); expect(tree.exists('my-app/.swcrc')).toBeTruthy(); }); + + it('should be strict by default', async () => { + await applicationGenerator(tree, { + name: 'my-app', + compiler: 'swc', + projectNameAndRootFormat: 'as-provided', + addPlugin: true, + } as Schema); + + const tsconfig = readJson(tree, 'my-app/tsconfig.json'); + expect(tsconfig.compilerOptions.strict).toBeTruthy(); + }); }); describe('setup web app with --bundler=vite', () => { diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index cac278d7705255..85636c6bf4be27 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -3,29 +3,15 @@ import { addProjectConfiguration, ensurePackage, formatFiles, - generateFiles, GeneratorCallback, getPackageManagerCommand, joinPathFragments, - names, - offsetFromRoot, - readNxJson, - readProjectConfiguration, runTasksInSerial, - TargetConfiguration, Tree, - updateNxJson, - updateProjectConfiguration, writeJson, } from '@nx/devkit'; -import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { - getRelativePathToRootTsConfig, - initGenerator as jsInitGenerator, -} from '@nx/js'; +import { initGenerator as jsInitGenerator } from '@nx/js'; import { swcCoreVersion } from '@nx/js/src/utils/versions'; -import type { Linter } from '@nx/eslint'; -import { join } from 'path'; import { nxVersion, swcLoaderVersion, @@ -34,201 +20,13 @@ import { } from '../../utils/versions'; import { webInitGenerator } from '../init/init'; import { Schema } from './schema'; -import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; -import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; -import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; -import { VitePluginOptions } from '@nx/vite/src/plugins/plugin'; -import { WebpackPluginOptions } from '@nx/webpack/src/plugins/plugin'; - -interface NormalizedSchema extends Schema { - projectName: string; - appProjectRoot: string; - e2eProjectName: string; - e2eProjectRoot: string; - e2eWebServerAddress: string; - e2eWebServerTarget: string; - e2ePort: number; - parsedTags: string[]; -} - -function createApplicationFiles(tree: Tree, options: NormalizedSchema) { - if (options.bundler === 'vite') { - generateFiles( - tree, - join(__dirname, './files/app-vite'), - options.appProjectRoot, - { - ...options, - ...names(options.name), - tmpl: '', - offsetFromRoot: offsetFromRoot(options.appProjectRoot), - rootTsConfigPath: getRelativePathToRootTsConfig( - tree, - options.appProjectRoot - ), - } - ); - } else { - generateFiles( - tree, - join(__dirname, './files/app-webpack'), - options.appProjectRoot, - { - ...options, - ...names(options.name), - tmpl: '', - offsetFromRoot: offsetFromRoot(options.appProjectRoot), - rootTsConfigPath: getRelativePathToRootTsConfig( - tree, - options.appProjectRoot - ), - webpackPluginOptions: hasWebpackPlugin(tree) - ? { - compiler: options.compiler, - target: 'web', - outputPath: joinPathFragments( - 'dist', - options.appProjectRoot != '.' - ? options.appProjectRoot - : options.projectName - ), - tsConfig: './tsconfig.app.json', - main: './src/main.ts', - assets: ['./src/favicon.ico', './src/assets'], - index: './src/index.html', - baseHref: '/', - styles: [`./src/styles.${options.style}`], - } - : null, - } - ); - if (options.unitTestRunner === 'none') { - tree.delete( - join(options.appProjectRoot, './src/app/app.element.spec.ts') - ); - } - } -} - -async function setupBundler(tree: Tree, options: NormalizedSchema) { - const main = joinPathFragments(options.appProjectRoot, 'src/main.ts'); - const tsConfig = joinPathFragments( - options.appProjectRoot, - 'tsconfig.app.json' - ); - const assets = [ - joinPathFragments(options.appProjectRoot, 'src/favicon.ico'), - joinPathFragments(options.appProjectRoot, 'src/assets'), - ]; - if (options.bundler === 'webpack') { - const { configurationGenerator } = ensurePackage< - typeof import('@nx/webpack') - >('@nx/webpack', nxVersion); - await configurationGenerator(tree, { - target: 'web', - project: options.projectName, - main, - tsConfig, - compiler: options.compiler ?? 'babel', - devServer: true, - webpackConfig: joinPathFragments( - options.appProjectRoot, - 'webpack.config.js' - ), - skipFormat: true, - addPlugin: options.addPlugin, - }); - const project = readProjectConfiguration(tree, options.projectName); - if (project.targets.build) { - const prodConfig = project.targets.build.configurations.production; - const buildOptions = project.targets.build.options; - buildOptions.assets = assets; - buildOptions.index = joinPathFragments( - options.appProjectRoot, - 'src/index.html' - ); - buildOptions.baseHref = '/'; - buildOptions.styles = [ - joinPathFragments( - options.appProjectRoot, - `src/styles.${options.style}` - ), - ]; - // We can delete that, because this projest is an application - // and applications have a .babelrc file in their root dir. - // So Nx will find it and use it - delete buildOptions.babelUpwardRootMode; - buildOptions.scripts = []; - prodConfig.fileReplacements = [ - { - replace: joinPathFragments( - options.appProjectRoot, - `src/environments/environment.ts` - ), - with: joinPathFragments( - options.appProjectRoot, - `src/environments/environment.prod.ts` - ), - }, - ]; - prodConfig.optimization = true; - prodConfig.outputHashing = 'all'; - prodConfig.sourceMap = false; - prodConfig.namedChunks = false; - prodConfig.extractLicenses = true; - prodConfig.vendorChunk = false; - updateProjectConfiguration(tree, options.projectName, project); - } - // TODO(jack): Flush this out... no bundler should be possible for web but the experience isn't holistic due to missing features (e.g. writing index.html). - } else if (options.bundler === 'none') { - const project = readProjectConfiguration(tree, options.projectName); - addBuildTargetDefaults(tree, `@nx/js:${options.compiler}`); - project.targets.build = { - executor: `@nx/js:${options.compiler}`, - outputs: ['{options.outputPath}'], - options: { - main, - outputPath: joinPathFragments('dist', options.appProjectRoot), - tsConfig, - }, - }; - updateProjectConfiguration(tree, options.projectName, project); - } else { - throw new Error('Unsupported bundler type'); - } -} - -async function addProject(tree: Tree, options: NormalizedSchema) { - const targets: Record = {}; - - addProjectConfiguration( - tree, - options.projectName, - { - projectType: 'application', - root: options.appProjectRoot, - sourceRoot: joinPathFragments(options.appProjectRoot, 'src'), - tags: options.parsedTags, - targets, - }, - options.standaloneConfig - ); -} - -function setDefaults(tree: Tree, options: NormalizedSchema) { - const nxJson = readNxJson(tree); - nxJson.generators = nxJson.generators || {}; - nxJson.generators['@nx/web:application'] = { - style: options.style, - linter: options.linter, - unitTestRunner: options.unitTestRunner, - e2eTestRunner: options.e2eTestRunner, - ...nxJson.generators['@nx/web:application'], - }; - updateNxJson(tree, nxJson); -} +import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; +import { addProject } from './lib/add-project'; +import { setupBundler } from './lib/setup-bundler'; +import { createApplicationFiles } from './lib/create-application-files'; +import { setDefaults } from './lib/set-defaults'; +import { normalizeOptions } from './lib/normalize-options'; export async function applicationGenerator(host: Tree, schema: Schema) { return await applicationGeneratorInternal(host, { @@ -457,95 +255,4 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { return runTasksInSerial(...tasks); } -async function normalizeOptions( - host: Tree, - options: Schema -): Promise { - const { - projectName: appProjectName, - projectRoot: appProjectRoot, - projectNameAndRootFormat, - } = await determineProjectNameAndRootOptions(host, { - name: options.name, - projectType: 'application', - directory: options.directory, - projectNameAndRootFormat: options.projectNameAndRootFormat, - callingGenerator: '@nx/web:application', - }); - options.projectNameAndRootFormat = projectNameAndRootFormat; - const nxJson = readNxJson(host); - const addPluginDefault = - process.env.NX_ADD_PLUGINS !== 'false' && - nxJson.useInferencePlugins !== false; - options.addPlugin ??= addPluginDefault; - - let e2eWebServerTarget = 'serve'; - if (options.addPlugin) { - if (nxJson.plugins) { - for (const plugin of nxJson.plugins) { - if ( - options.bundler === 'vite' && - typeof plugin === 'object' && - plugin.plugin === '@nx/vite/plugin' && - (plugin.options as VitePluginOptions).serveTargetName - ) { - e2eWebServerTarget = (plugin.options as VitePluginOptions) - .serveTargetName; - } else if ( - options.bundler === 'webpack' && - typeof plugin === 'object' && - plugin.plugin === '@nx/webpack/plugin' && - (plugin.options as WebpackPluginOptions).serveTargetName - ) { - e2eWebServerTarget = (plugin.options as WebpackPluginOptions) - .serveTargetName; - } - } - } - } - - let e2ePort = 4200; - if ( - nxJson.targetDefaults?.[e2eWebServerTarget] && - nxJson.targetDefaults?.[e2eWebServerTarget].options?.port - ) { - e2ePort = nxJson.targetDefaults?.[e2eWebServerTarget].options?.port; - } - - const e2eProjectName = `${appProjectName}-e2e`; - const e2eProjectRoot = `${appProjectRoot}-e2e`; - const e2eWebServerAddress = `http://localhost:${e2ePort}`; - - const npmScope = getNpmScope(host); - - const parsedTags = options.tags - ? options.tags.split(',').map((s) => s.trim()) - : []; - - if (options.bundler === 'vite' && options.unitTestRunner !== 'none') { - options.unitTestRunner = 'vitest'; - } - - options.style = options.style || 'css'; - options.linter = options.linter || ('eslint' as Linter.EsLint); - options.unitTestRunner = options.unitTestRunner || 'jest'; - options.e2eTestRunner = options.e2eTestRunner || 'playwright'; - - return { - ...options, - prefix: options.prefix ?? npmScope ?? 'app', - name: names(options.name).fileName, - compiler: options.compiler ?? 'babel', - bundler: options.bundler ?? 'webpack', - projectName: appProjectName, - appProjectRoot, - e2eProjectRoot, - e2eProjectName, - e2eWebServerAddress, - e2eWebServerTarget, - e2ePort, - parsedTags, - }; -} - export default applicationGenerator; diff --git a/packages/web/src/generators/application/lib/add-project.ts b/packages/web/src/generators/application/lib/add-project.ts new file mode 100644 index 00000000000000..d702bca0cbba5f --- /dev/null +++ b/packages/web/src/generators/application/lib/add-project.ts @@ -0,0 +1,24 @@ +import { + TargetConfiguration, + Tree, + addProjectConfiguration, + joinPathFragments, +} from '@nx/devkit'; +import { NormalizedSchema } from '../schema'; + +export async function addProject(tree: Tree, options: NormalizedSchema) { + const targets: Record = {}; + + addProjectConfiguration( + tree, + options.projectName, + { + projectType: 'application', + root: options.appProjectRoot, + sourceRoot: joinPathFragments(options.appProjectRoot, 'src'), + tags: options.parsedTags, + targets, + }, + options.standaloneConfig + ); +} diff --git a/packages/web/src/generators/application/lib/create-application-files.ts b/packages/web/src/generators/application/lib/create-application-files.ts new file mode 100644 index 00000000000000..c31a2f4eff95cc --- /dev/null +++ b/packages/web/src/generators/application/lib/create-application-files.ts @@ -0,0 +1,85 @@ +import { join } from 'path'; +import { + Tree, + generateFiles, + names, + offsetFromRoot, + joinPathFragments, + updateJson, +} from '@nx/devkit'; +import { getRelativePathToRootTsConfig } from '@nx/js'; +import { NormalizedSchema } from '../schema'; +import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; + +export function createApplicationFiles(tree: Tree, options: NormalizedSchema) { + if (options.bundler === 'vite') { + generateFiles( + tree, + join(__dirname, '../files/app-vite'), + options.appProjectRoot, + { + ...options, + ...names(options.name), + tmpl: '', + offsetFromRoot: offsetFromRoot(options.appProjectRoot), + rootTsConfigPath: getRelativePathToRootTsConfig( + tree, + options.appProjectRoot + ), + } + ); + } else { + generateFiles( + tree, + join(__dirname, '../files/app-webpack'), + options.appProjectRoot, + { + ...options, + ...names(options.name), + tmpl: '', + offsetFromRoot: offsetFromRoot(options.appProjectRoot), + rootTsConfigPath: getRelativePathToRootTsConfig( + tree, + options.appProjectRoot + ), + webpackPluginOptions: hasWebpackPlugin(tree) + ? { + compiler: options.compiler, + target: 'web', + outputPath: joinPathFragments( + 'dist', + options.appProjectRoot != '.' + ? options.appProjectRoot + : options.projectName + ), + tsConfig: './tsconfig.app.json', + main: './src/main.ts', + assets: ['./src/favicon.ico', './src/assets'], + index: './src/index.html', + baseHref: '/', + styles: [`./src/styles.${options.style}`], + } + : null, + } + ); + if (options.unitTestRunner === 'none') { + tree.delete( + join(options.appProjectRoot, './src/app/app.element.spec.ts') + ); + } + } + + updateJson( + tree, + joinPathFragments(options.appProjectRoot, 'tsconfig.json'), + (json) => { + return { + ...json, + compilerOptions: { + ...(json.compilerOptions || {}), + strict: options.strict, + }, + }; + } + ); +} diff --git a/packages/web/src/generators/application/lib/normalize-options.ts b/packages/web/src/generators/application/lib/normalize-options.ts new file mode 100644 index 00000000000000..8a93f5fe4068c6 --- /dev/null +++ b/packages/web/src/generators/application/lib/normalize-options.ts @@ -0,0 +1,99 @@ +import { Tree, names, readNxJson } from '@nx/devkit'; +import { Schema, NormalizedSchema } from '../schema'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { VitePluginOptions } from '@nx/vite/src/plugins/plugin'; +import { WebpackPluginOptions } from '@nx/webpack/src/plugins/plugin'; +import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; +import type { Linter } from '@nx/eslint'; + +export async function normalizeOptions( + host: Tree, + options: Schema +): Promise { + const { + projectName: appProjectName, + projectRoot: appProjectRoot, + projectNameAndRootFormat, + } = await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/web:application', + }); + options.projectNameAndRootFormat = projectNameAndRootFormat; + const nxJson = readNxJson(host); + const addPluginDefault = + process.env.NX_ADD_PLUGINS !== 'false' && + nxJson.useInferencePlugins !== false; + options.addPlugin ??= addPluginDefault; + + let e2eWebServerTarget = 'serve'; + if (options.addPlugin) { + if (nxJson.plugins) { + for (const plugin of nxJson.plugins) { + if ( + options.bundler === 'vite' && + typeof plugin === 'object' && + plugin.plugin === '@nx/vite/plugin' && + (plugin.options as VitePluginOptions).serveTargetName + ) { + e2eWebServerTarget = (plugin.options as VitePluginOptions) + .serveTargetName; + } else if ( + options.bundler === 'webpack' && + typeof plugin === 'object' && + plugin.plugin === '@nx/webpack/plugin' && + (plugin.options as WebpackPluginOptions).serveTargetName + ) { + e2eWebServerTarget = (plugin.options as WebpackPluginOptions) + .serveTargetName; + } + } + } + } + + let e2ePort = 4200; + if ( + nxJson.targetDefaults?.[e2eWebServerTarget] && + nxJson.targetDefaults?.[e2eWebServerTarget].options?.port + ) { + e2ePort = nxJson.targetDefaults?.[e2eWebServerTarget].options?.port; + } + + const e2eProjectName = `${appProjectName}-e2e`; + const e2eProjectRoot = `${appProjectRoot}-e2e`; + const e2eWebServerAddress = `http://localhost:${e2ePort}`; + + const npmScope = getNpmScope(host); + + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + if (options.bundler === 'vite' && options.unitTestRunner !== 'none') { + options.unitTestRunner = 'vitest'; + } + + options.style = options.style || 'css'; + options.linter = options.linter || ('eslint' as Linter.EsLint); + options.unitTestRunner = options.unitTestRunner || 'jest'; + options.e2eTestRunner = options.e2eTestRunner || 'playwright'; + + return { + ...options, + prefix: options.prefix ?? npmScope ?? 'app', + name: names(options.name).fileName, + compiler: options.compiler ?? 'babel', + bundler: options.bundler ?? 'webpack', + strict: options.strict ?? true, + projectName: appProjectName, + appProjectRoot, + e2eProjectRoot, + e2eProjectName, + e2eWebServerAddress, + e2eWebServerTarget, + e2ePort, + parsedTags, + }; +} diff --git a/packages/web/src/generators/application/lib/set-defaults.ts b/packages/web/src/generators/application/lib/set-defaults.ts new file mode 100644 index 00000000000000..747b38b17d5261 --- /dev/null +++ b/packages/web/src/generators/application/lib/set-defaults.ts @@ -0,0 +1,15 @@ +import { Tree, readNxJson, updateNxJson } from '@nx/devkit'; +import { NormalizedSchema } from '../schema'; + +export function setDefaults(tree: Tree, options: NormalizedSchema) { + const nxJson = readNxJson(tree); + nxJson.generators = nxJson.generators || {}; + nxJson.generators['@nx/web:application'] = { + style: options.style, + linter: options.linter, + unitTestRunner: options.unitTestRunner, + e2eTestRunner: options.e2eTestRunner, + ...nxJson.generators['@nx/web:application'], + }; + updateNxJson(tree, nxJson); +} diff --git a/packages/web/src/generators/application/lib/setup-bundler.ts b/packages/web/src/generators/application/lib/setup-bundler.ts new file mode 100644 index 00000000000000..6517dbdef62dd3 --- /dev/null +++ b/packages/web/src/generators/application/lib/setup-bundler.ts @@ -0,0 +1,99 @@ +import { + Tree, + ensurePackage, + joinPathFragments, + readProjectConfiguration, + updateProjectConfiguration, +} from '@nx/devkit'; +import { NormalizedSchema } from '../schema'; +import { nxVersion } from '@nx/vite'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; + +export async function setupBundler(tree: Tree, options: NormalizedSchema) { + const main = joinPathFragments(options.appProjectRoot, 'src/main.ts'); + const tsConfig = joinPathFragments( + options.appProjectRoot, + 'tsconfig.app.json' + ); + const assets = [ + joinPathFragments(options.appProjectRoot, 'src/favicon.ico'), + joinPathFragments(options.appProjectRoot, 'src/assets'), + ]; + + if (options.bundler === 'webpack') { + const { configurationGenerator } = ensurePackage< + typeof import('@nx/webpack') + >('@nx/webpack', nxVersion); + await configurationGenerator(tree, { + target: 'web', + project: options.projectName, + main, + tsConfig, + compiler: options.compiler ?? 'babel', + devServer: true, + webpackConfig: joinPathFragments( + options.appProjectRoot, + 'webpack.config.js' + ), + skipFormat: true, + addPlugin: options.addPlugin, + }); + const project = readProjectConfiguration(tree, options.projectName); + if (project.targets.build) { + const prodConfig = project.targets.build.configurations.production; + const buildOptions = project.targets.build.options; + buildOptions.assets = assets; + buildOptions.index = joinPathFragments( + options.appProjectRoot, + 'src/index.html' + ); + buildOptions.baseHref = '/'; + buildOptions.styles = [ + joinPathFragments( + options.appProjectRoot, + `src/styles.${options.style}` + ), + ]; + // We can delete that, because this projest is an application + // and applications have a .babelrc file in their root dir. + // So Nx will find it and use it + delete buildOptions.babelUpwardRootMode; + buildOptions.scripts = []; + prodConfig.fileReplacements = [ + { + replace: joinPathFragments( + options.appProjectRoot, + `src/environments/environment.ts` + ), + with: joinPathFragments( + options.appProjectRoot, + `src/environments/environment.prod.ts` + ), + }, + ]; + prodConfig.optimization = true; + prodConfig.outputHashing = 'all'; + prodConfig.sourceMap = false; + prodConfig.namedChunks = false; + prodConfig.extractLicenses = true; + prodConfig.vendorChunk = false; + updateProjectConfiguration(tree, options.projectName, project); + } + // TODO(jack): Flush this out... no bundler should be possible for web but the experience isn't holistic due to missing features (e.g. writing index.html). + } else if (options.bundler === 'none') { + const project = readProjectConfiguration(tree, options.projectName); + addBuildTargetDefaults(tree, `@nx/js:${options.compiler}`); + project.targets.build = { + executor: `@nx/js:${options.compiler}`, + outputs: ['{options.outputPath}'], + options: { + main, + outputPath: joinPathFragments('dist', options.appProjectRoot), + tsConfig, + }, + }; + updateProjectConfiguration(tree, options.projectName, project); + } else { + throw new Error('Unsupported bundler type'); + } +} diff --git a/packages/web/src/generators/application/schema.d.ts b/packages/web/src/generators/application/schema.d.ts index 8b67cced748918..7a9a165f5d3110 100644 --- a/packages/web/src/generators/application/schema.d.ts +++ b/packages/web/src/generators/application/schema.d.ts @@ -17,5 +17,17 @@ export interface Schema { linter?: Linter; standaloneConfig?: boolean; setParserOptionsProject?: boolean; + strict?: boolean; addPlugin?: boolean; } + +export interface NormalizedSchema extends Schema { + projectName: string; + appProjectRoot: string; + e2eProjectName: string; + e2eProjectRoot: string; + e2eWebServerAddress: string; + e2eWebServerTarget: string; + e2ePort: number; + parsedTags: string[]; +} diff --git a/packages/web/src/generators/application/schema.json b/packages/web/src/generators/application/schema.json index 28e29cac5109d5..7f3514e2d61038 100644 --- a/packages/web/src/generators/application/schema.json +++ b/packages/web/src/generators/application/schema.json @@ -69,6 +69,11 @@ "enum": ["eslint", "none"], "default": "eslint" }, + "strict": { + "type": "boolean", + "description": "Creates an application with strict mode and strict type checking.", + "default": true + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean",