From 1987b2f90e712f8eaf20793c96643adc61f80d30 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:30:57 -0400 Subject: [PATCH 1/2] feat(@schematics/angular): generate applications using TypeScript project references When generating a project (either via `ng new` or `ng generate application`), the created TypeScript configuration files (`tsconfig.app.json`/`tsconfig.spec.json`) will be setup as composite projects and added as project references to in the root `tsconfig.json`. This transforms the root `tsconfig.json` into a "solution" style configuration. This allows IDEs to more accurately discover and provide type information for the varying types of files (test, application, etc.) within each project. The Angular build process is otherwise unaffected by these changes. --- .../common-files/tsconfig.app.json.template | 9 +++++---- .../common-files/tsconfig.spec.json.template | 4 ++-- .../schematics/angular/application/index.ts | 20 +++++++++++++++++++ .../angular/application/index_spec.ts | 9 +++++++-- .../workspace/files/tsconfig.json.template | 3 ++- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/schematics/angular/application/files/common-files/tsconfig.app.json.template b/packages/schematics/angular/application/files/common-files/tsconfig.app.json.template index a65978e44714..c9064b22766b 100644 --- a/packages/schematics/angular/application/files/common-files/tsconfig.app.json.template +++ b/packages/schematics/angular/application/files/common-files/tsconfig.app.json.template @@ -3,13 +3,14 @@ { "extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json", "compilerOptions": { + "composite": true, "outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/app", "types": [] }, - "files": [ - "src/main.ts" - ], "include": [ - "src/**/*.d.ts" + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" ] } diff --git a/packages/schematics/angular/application/files/common-files/tsconfig.spec.json.template b/packages/schematics/angular/application/files/common-files/tsconfig.spec.json.template index 3a0a2b43e8f1..40b32b8bc458 100644 --- a/packages/schematics/angular/application/files/common-files/tsconfig.spec.json.template +++ b/packages/schematics/angular/application/files/common-files/tsconfig.spec.json.template @@ -3,13 +3,13 @@ { "extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json", "compilerOptions": { + "composite": true, "outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/spec", "types": [ "jasmine" ] }, "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" + "src/**/*.ts" ] } diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 2273afc311b1..4a8c191d6830 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -26,12 +26,28 @@ import { import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; import { Schema as ComponentOptions } from '../component/schema'; import { NodeDependencyType, addPackageJsonDependency } from '../utility/dependencies'; +import { JSONFile } from '../utility/json-file'; import { latestVersions } from '../utility/latest-versions'; import { relativePathToWorkspaceRoot } from '../utility/paths'; import { getWorkspace, updateWorkspace } from '../utility/workspace'; import { Builders, ProjectType } from '../utility/workspace-models'; import { Schema as ApplicationOptions, Style } from './schema'; +function updateTsConfig(...paths: string[]) { + return (host: Tree) => { + if (!host.exists('tsconfig.json')) { + return host; + } + + const newReferences = paths.map((path) => ({ path })); + + const file = new JSONFile(host, 'tsconfig.json'); + const jsonPath = ['references']; + const value = file.get(jsonPath); + file.modify(jsonPath, Array.isArray(value) ? [...value, ...newReferences] : newReferences); + }; +} + export default function (options: ApplicationOptions): Rule { return async (host: Tree, context: SchematicContext) => { const { appDir, appRootSelector, componentOptions, folderName, sourceDir } = @@ -39,6 +55,10 @@ export default function (options: ApplicationOptions): Rule { return chain([ addAppToWorkspaceFile(options, appDir, folderName), + updateTsConfig( + join(normalize(appDir), 'tsconfig.app.json'), + join(normalize(appDir), 'tsconfig.spec.json'), + ), options.standalone ? noop() : schematic('module', { diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index 31c505a1548a..a09ad02ddc94 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -93,8 +93,13 @@ describe('Application Schematic', () => { it('should set the right paths in the tsconfig.app.json', async () => { const tree = await schematicRunner.runSchematic('application', defaultOptions, workspaceTree); - const { files, extends: _extends } = readJsonFile(tree, '/projects/foo/tsconfig.app.json'); - expect(files).toEqual(['src/main.ts']); + const { + include, + exclude, + extends: _extends, + } = readJsonFile(tree, '/projects/foo/tsconfig.app.json'); + expect(include).toEqual(['src/**/*.ts']); + expect(exclude).toEqual(['src/**/*.spec.ts']); expect(_extends).toBe('../../tsconfig.json'); }); diff --git a/packages/schematics/angular/workspace/files/tsconfig.json.template b/packages/schematics/angular/workspace/files/tsconfig.json.template index 92d84123aeee..8303df1f24ee 100644 --- a/packages/schematics/angular/workspace/files/tsconfig.json.template +++ b/packages/schematics/angular/workspace/files/tsconfig.json.template @@ -22,5 +22,6 @@ "strictInputAccessModifiers": true, "typeCheckHostBindings": true, "strictTemplates": true<% } %> - } + }, + "files": [] } From 937287a9cd106a007769eb51b3fa4f92d0f0b58c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:42:07 -0400 Subject: [PATCH 2/2] fix(@schematics/angular): remove setting files tsconfig field with SSR/Server generation The `files` field within the `tsconfig.app.json` file is no longer used with the "solution" style tsconfig generated with applications. The SSR and server schematics no longer need to modify this field since all TS files within `src` are included by default. --- .../schematics/angular/application/index.ts | 10 +++---- .../angular/application/index_spec.ts | 28 +++++++++++++++++++ packages/schematics/angular/server/index.ts | 4 --- .../schematics/angular/server/index_spec.ts | 1 - packages/schematics/angular/ssr/index.ts | 7 +++++ packages/schematics/angular/ssr/index_spec.ts | 19 +++++++++++-- .../workspace/files/tsconfig.json.template | 3 +- 7 files changed, 58 insertions(+), 14 deletions(-) diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 4a8c191d6830..b85ad2b41d36 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -33,7 +33,7 @@ import { getWorkspace, updateWorkspace } from '../utility/workspace'; import { Builders, ProjectType } from '../utility/workspace-models'; import { Schema as ApplicationOptions, Style } from './schema'; -function updateTsConfig(...paths: string[]) { +function addTsProjectReference(...paths: string[]) { return (host: Tree) => { if (!host.exists('tsconfig.json')) { return host; @@ -55,10 +55,10 @@ export default function (options: ApplicationOptions): Rule { return chain([ addAppToWorkspaceFile(options, appDir, folderName), - updateTsConfig( - join(normalize(appDir), 'tsconfig.app.json'), - join(normalize(appDir), 'tsconfig.spec.json'), - ), + addTsProjectReference(join(normalize(appDir), 'tsconfig.app.json')), + options.skipTests + ? noop() + : addTsProjectReference(join(normalize(appDir), 'tsconfig.spec.json')), options.standalone ? noop() : schematic('module', { diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index a09ad02ddc94..e9b84948db24 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -110,6 +110,34 @@ describe('Application Schematic', () => { expect(_extends).toBe('../../tsconfig.json'); }); + it('should add project references in the root tsconfig.json', async () => { + const tree = await schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + + const { references } = readJsonFile(tree, '/tsconfig.json'); + expect(references).toContain( + jasmine.objectContaining({ path: 'projects/foo/tsconfig.app.json' }), + ); + expect(references).toContain( + jasmine.objectContaining({ path: 'projects/foo/tsconfig.spec.json' }), + ); + }); + + it('should not add spec project reference in the root tsconfig.json with "skipTests" enabled', async () => { + const tree = await schematicRunner.runSchematic( + 'application', + { ...defaultOptions, skipTests: true }, + workspaceTree, + ); + + const { references } = readJsonFile(tree, '/tsconfig.json'); + expect(references).toContain( + jasmine.objectContaining({ path: 'projects/foo/tsconfig.app.json' }), + ); + expect(references).not.toContain( + jasmine.objectContaining({ path: 'projects/foo/tsconfig.spec.json' }), + ); + }); + it('should install npm dependencies when `skipInstall` is false', async () => { await schematicRunner.runSchematic( 'application', diff --git a/packages/schematics/angular/server/index.ts b/packages/schematics/angular/server/index.ts index a8baccf0d503..50f624e078cd 100644 --- a/packages/schematics/angular/server/index.ts +++ b/packages/schematics/angular/server/index.ts @@ -119,10 +119,6 @@ function updateConfigFileApplicationBuilder(options: ServerOptions): Rule { function updateTsConfigFile(tsConfigPath: string): Rule { return (host: Tree) => { const json = new JSONFile(host, tsConfigPath); - const filesPath = ['files']; - const files = new Set((json.get(filesPath) as string[] | undefined) ?? []); - files.add('src/' + serverMainEntryName); - json.modify(filesPath, [...files]); const typePath = ['compilerOptions', 'types']; const types = new Set((json.get(typePath) as string[] | undefined) ?? []); diff --git a/packages/schematics/angular/server/index_spec.ts b/packages/schematics/angular/server/index_spec.ts index 09dfbc73d2a1..f3e5e277d8ef 100644 --- a/packages/schematics/angular/server/index_spec.ts +++ b/packages/schematics/angular/server/index_spec.ts @@ -167,7 +167,6 @@ describe('Server Schematic', () => { const filePath = '/projects/bar/tsconfig.app.json'; const contents = parseJson(tree.readContent(filePath).toString()); expect(contents.compilerOptions.types).toEqual(['node']); - expect(contents.files).toEqual(['src/main.ts', 'src/main.server.ts']); }); it(`should add 'provideClientHydration' to the providers list`, async () => { diff --git a/packages/schematics/angular/ssr/index.ts b/packages/schematics/angular/ssr/index.ts index b81188340f0b..e589395dac73 100644 --- a/packages/schematics/angular/ssr/index.ts +++ b/packages/schematics/angular/ssr/index.ts @@ -154,6 +154,13 @@ function updateApplicationBuilderTsConfigRule(options: SSROptions): Rule { } const json = new JSONFile(host, tsConfigPath); + + // Skip adding the files entry if the server entry would already be included + const include = json.get(['include']); + if (Array.isArray(include) && include.includes('src/**/*.ts')) { + return; + } + const filesPath = ['files']; const files = new Set((json.get(filesPath) as string[] | undefined) ?? []); files.add('src/server.ts'); diff --git a/packages/schematics/angular/ssr/index_spec.ts b/packages/schematics/angular/ssr/index_spec.ts index a9f4eff7ac5e..97b534aba8e1 100644 --- a/packages/schematics/angular/ssr/index_spec.ts +++ b/packages/schematics/angular/ssr/index_spec.ts @@ -70,13 +70,28 @@ describe('SSR Schematic', () => { expect((schematicRunner.tasks[0].options as { command: string }).command).toBe('install'); }); - it(`should update 'tsconfig.app.json' files with Express main file`, async () => { + it(`should not update 'tsconfig.app.json' files with Express main file already included`, async () => { const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); const { files } = tree.readJson('/projects/test-app/tsconfig.app.json') as { files: string[]; }; - expect(files).toEqual(['src/main.ts', 'src/main.server.ts', 'src/server.ts']); + expect(files).toBeUndefined(); + }); + + it(`should update 'tsconfig.app.json' files with Express main file if not included`, async () => { + const appTsConfigContent = appTree.readJson('/projects/test-app/tsconfig.app.json') as { + include?: string[]; + }; + appTsConfigContent.include = []; + appTree.overwrite('/projects/test-app/tsconfig.app.json', JSON.stringify(appTsConfigContent)); + + const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); + const { files } = tree.readJson('/projects/test-app/tsconfig.app.json') as { + files: string[]; + }; + + expect(files).toContain('src/server.ts'); }); }); diff --git a/packages/schematics/angular/workspace/files/tsconfig.json.template b/packages/schematics/angular/workspace/files/tsconfig.json.template index 8303df1f24ee..798ec8305a16 100644 --- a/packages/schematics/angular/workspace/files/tsconfig.json.template +++ b/packages/schematics/angular/workspace/files/tsconfig.json.template @@ -2,8 +2,7 @@ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ { "compileOnSave": false, - "compilerOptions": { - "outDir": "./dist/out-tsc",<% if (strict) { %> + "compilerOptions": {<% if (strict) { %> "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true,