diff --git a/packages/angular/src/generators/ng-add/migrate-from-angular-cli.spec.ts b/packages/angular/src/generators/ng-add/migrate-from-angular-cli.spec.ts index b98c9c1679..4aaa49c017 100644 --- a/packages/angular/src/generators/ng-add/migrate-from-angular-cli.spec.ts +++ b/packages/angular/src/generators/ng-add/migrate-from-angular-cli.spec.ts @@ -1,7 +1,7 @@ +import type { Tree } from '@nrwl/devkit'; import { readJson, readProjectConfiguration, - Tree, updateJson, writeJson, } from '@nrwl/devkit'; diff --git a/packages/angular/src/generators/ng-add/migrate-from-angular-cli.ts b/packages/angular/src/generators/ng-add/migrate-from-angular-cli.ts index 14ccb3ea39..ddf126b21f 100755 --- a/packages/angular/src/generators/ng-add/migrate-from-angular-cli.ts +++ b/packages/angular/src/generators/ng-add/migrate-from-angular-cli.ts @@ -1,34 +1,33 @@ +import type { Tree } from '@nrwl/devkit'; import { addDependenciesToPackageJson, formatFiles, installPackagesTask, readJson, readWorkspaceConfiguration, - Tree, updateJson, updateWorkspaceConfiguration, } from '@nrwl/devkit'; import { nxVersion } from '../../utils/versions'; +import type { ProjectMigrator } from './migrators'; +import { AppMigrator, LibMigrator } from './migrators'; import type { GeneratorOptions } from './schema'; -import { AppMigrator } from './utilities/app.migrator'; -import { getAllProjects } from './utilities/get-all-projects'; -import { LibMigrator } from './utilities/lib.migrator'; -import { normalizeOptions } from './utilities/normalize-options'; -import { ProjectMigrator } from './utilities/project.migrator'; -import { validateProjects } from './utilities/validate-projects'; import { cleanupEsLintPackages, createNxJson, createRootKarmaConfig, createWorkspaceFiles, decorateAngularCli, + getAllProjects, getWorkspaceCapabilities, + normalizeOptions, updatePackageJson, updateRootEsLintConfig, updateRootTsConfig, updateWorkspaceConfigDefaults, + validateProjects, validateWorkspace, -} from './utilities/workspace'; +} from './utilities'; export async function migrateFromAngularCli( tree: Tree, diff --git a/packages/angular/src/generators/ng-add/migrators/builders/angular-devkit-ng-packagr.migrator.ts b/packages/angular/src/generators/ng-add/migrators/builders/angular-devkit-ng-packagr.migrator.ts new file mode 100644 index 0000000000..4566313a63 --- /dev/null +++ b/packages/angular/src/generators/ng-add/migrators/builders/angular-devkit-ng-packagr.migrator.ts @@ -0,0 +1,179 @@ +import type { + ProjectConfiguration, + TargetConfiguration, + Tree, +} from '@nrwl/devkit'; +import { + joinPathFragments, + offsetFromRoot, + updateJson, + updateProjectConfiguration, +} from '@nrwl/devkit'; +import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; +import { basename } from 'path'; +import { addBuildableLibrariesPostCssDependencies } from '../../../utils/dependencies'; +import type { + Logger, + ProjectMigrationInfo, + ValidationError, + ValidationResult, +} from '../../utilities'; +import { arrayToString } from '../../utilities'; +import { BuilderMigrator } from './builder.migrator'; + +export class AngularDevkitNgPackagrMigrator extends BuilderMigrator { + constructor( + tree: Tree, + project: ProjectMigrationInfo, + projectConfig: ProjectConfiguration, + logger: Logger + ) { + super( + tree, + '@angular-devkit/build-angular:ng-packagr', + project, + projectConfig, + logger + ); + } + + override migrate(): void { + if (!this.targets.size) { + this.logger.warn( + `There is no target in the project configuration using the ${this.builderName} builder. This might not be an issue. ` + + `Skipping updating the build configuration.` + ); + return; + } + + for (const [name, target] of this.targets) { + this.updateTargetConfiguration(name, target); + this.updateNgPackageJson(name, target); + this.updateTsConfigs(name, target); + this.updateCacheableOperations([name]); + addBuildableLibrariesPostCssDependencies(this.tree); + } + } + + override validate(): ValidationResult { + const errors: ValidationError[] = []; + // TODO(leo): keeping restriction until the full refactor is done and we start + // expanding what's supported. + if (this.targets.size > 1) { + errors.push({ + message: `There is more than one target using a builder that is used to build the project (${arrayToString( + [...this.targets.keys()] + )}).`, + hint: `Make sure the project only has one target with a builder that is used to build the project.`, + }); + } + + return errors.length ? errors : null; + } + + private updateTargetConfiguration( + targetName: string, + target: TargetConfiguration + ): void { + target.executor = '@nrwl/angular:package'; + + if ( + !target.options && + (!target.configurations || !Object.keys(target.configurations).length) + ) { + this.logger.warn( + `The target "${targetName}" is not specifying any options or configurations. Skipping updating the target configuration.` + ); + return; + } + + ['project', 'tsConfig'].forEach((option) => { + if (target.options?.[option]) { + target.options[option] = joinPathFragments( + this.project.newRoot, + basename(target.options[option]) + ); + } + + for (const configuration of Object.values(target.configurations ?? {})) { + configuration[option] = + configuration[option] && + joinPathFragments( + this.project.newRoot, + basename(configuration[option]) + ); + } + }); + + updateProjectConfiguration(this.tree, this.project.name, { + ...this.projectConfig, + }); + } + + private updateNgPackageJson( + targetName: string, + target: TargetConfiguration + ): void { + if (!target.options?.project) { + this.logger.warn( + `The "${targetName}" target does not have the "project" option configured. Skipping updating the ng-packagr project file ("ng-package.json").` + ); + return; + } else if (!this.tree.exists(target.options.project)) { + this.logger.warn( + `The ng-packagr project file "${this.originalProjectConfig.targets[targetName].options.project}" specified in the "${targetName}" ` + + `target could not be found. Skipping updating the ng-packagr project file.` + ); + return; + } + + updateJson(this.tree, target.options.project, (ngPackageJson) => { + const offset = offsetFromRoot(this.project.newRoot); + ngPackageJson.$schema = + ngPackageJson.$schema && + `${offset}node_modules/ng-packagr/ng-package.schema.json`; + ngPackageJson.dest = `${offset}dist/${this.project.name}`; + + return ngPackageJson; + }); + } + + private updateTsConfigs( + targetName: string, + target: TargetConfiguration + ): void { + const tsConfigPath = + target.options?.tsConfig ?? target.configurations?.development?.tsConfig; + if (!tsConfigPath) { + this.logger.warn( + `The "${targetName}" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.` + ); + return; + } else if (!this.tree.exists(tsConfigPath)) { + const originalTsConfigPath = target.options?.tsConfig + ? this.originalProjectConfig.targets[targetName].options.tsConfig + : this.originalProjectConfig.targets[targetName].configurations + ?.development?.tsConfig; + this.logger.warn( + `The tsConfig file "${originalTsConfigPath}" specified in the "${targetName}" target could not be found. Skipping updating the tsConfig file.` + ); + return; + } + + const rootTsConfigFile = getRootTsConfigPathInTree(this.tree); + const projectOffsetFromRoot = offsetFromRoot(this.projectConfig.root); + this.updateTsConfigFile( + tsConfigPath, + rootTsConfigFile, + projectOffsetFromRoot + ); + + updateJson(this.tree, tsConfigPath, (json) => { + if (!json.include?.length && !json.files?.length) { + json.include = ['**/*.ts']; + } + + return json; + }); + } +} diff --git a/packages/angular/src/generators/ng-add/migrators/builders/builder-migrator-class.type.ts b/packages/angular/src/generators/ng-add/migrators/builders/builder-migrator-class.type.ts new file mode 100644 index 0000000000..9db6fcbc38 --- /dev/null +++ b/packages/angular/src/generators/ng-add/migrators/builders/builder-migrator-class.type.ts @@ -0,0 +1,12 @@ +import type { ProjectConfiguration, Tree } from '@nrwl/devkit'; +import type { Logger, ProjectMigrationInfo } from '../../utilities'; +import type { BuilderMigrator } from './builder.migrator'; + +export type BuilderMigratorClassType = { + new ( + tree: Tree, + project: ProjectMigrationInfo, + projectConfig: ProjectConfiguration, + logger: Logger + ): BuilderMigrator; +}; diff --git a/packages/angular/src/generators/ng-add/migrators/builders/builder.migrator.ts b/packages/angular/src/generators/ng-add/migrators/builders/builder.migrator.ts new file mode 100644 index 0000000000..ad945e4b1a --- /dev/null +++ b/packages/angular/src/generators/ng-add/migrators/builders/builder.migrator.ts @@ -0,0 +1,36 @@ +import type { + ProjectConfiguration, + TargetConfiguration, + Tree, +} from '@nrwl/devkit'; +import type { Logger, ProjectMigrationInfo } from '../../utilities'; +import { Migrator } from '../migrator'; + +export abstract class BuilderMigrator extends Migrator { + protected targets: Map = new Map(); + + constructor( + tree: Tree, + public readonly builderName: string, + project: ProjectMigrationInfo, + projectConfig: ProjectConfiguration, + logger: Logger + ) { + super(tree, projectConfig, logger); + + this.project = project; + this.projectConfig = projectConfig; + + this.collectBuilderTargets(); + } + + protected collectBuilderTargets(): void { + for (const [name, target] of Object.entries( + this.projectConfig.targets ?? {} + )) { + if (target.executor === this.builderName) { + this.targets.set(name, target); + } + } + } +} diff --git a/packages/angular/src/generators/ng-add/migrators/builders/index.ts b/packages/angular/src/generators/ng-add/migrators/builders/index.ts new file mode 100644 index 0000000000..c3fb0d3d8f --- /dev/null +++ b/packages/angular/src/generators/ng-add/migrators/builders/index.ts @@ -0,0 +1,3 @@ +export * from './angular-devkit-ng-packagr.migrator'; +export * from './builder-migrator-class.type'; +export * from './builder.migrator'; diff --git a/packages/angular/src/generators/ng-add/migrators/index.ts b/packages/angular/src/generators/ng-add/migrators/index.ts new file mode 100644 index 0000000000..64fb862628 --- /dev/null +++ b/packages/angular/src/generators/ng-add/migrators/index.ts @@ -0,0 +1,2 @@ +export * from './builders'; +export * from './projects'; diff --git a/packages/angular/src/generators/ng-add/migrators/migrator.ts b/packages/angular/src/generators/ng-add/migrators/migrator.ts new file mode 100644 index 0000000000..81f1365d26 --- /dev/null +++ b/packages/angular/src/generators/ng-add/migrators/migrator.ts @@ -0,0 +1,66 @@ +import type { ProjectConfiguration, Tree } from '@nrwl/devkit'; +import { + readWorkspaceConfiguration, + updateJson, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import type { Logger } from '../utilities/logger'; +import type { + ProjectMigrationInfo, + ValidationResult, +} from '../utilities/types'; + +export abstract class Migrator { + protected project: ProjectMigrationInfo; + protected readonly originalProjectConfig: ProjectConfiguration; + + constructor( + protected readonly tree: Tree, + protected projectConfig: ProjectConfiguration, + protected readonly logger: Logger + ) { + this.originalProjectConfig = Object.freeze( + JSON.parse(JSON.stringify(this.projectConfig)) + ); + } + + abstract migrate(): Promise | void; + abstract validate(): ValidationResult; + + // TODO(leo): This should be moved to BuilderMigrator once everything is split into builder migrators. + protected updateCacheableOperations(targetNames: string[]): void { + if (!targetNames.length) { + return; + } + + const workspaceConfig = readWorkspaceConfiguration(this.tree); + + Object.keys(workspaceConfig.tasksRunnerOptions ?? {}).forEach( + (taskRunnerName) => { + const taskRunner = workspaceConfig.tasksRunnerOptions[taskRunnerName]; + taskRunner.options.cacheableOperations = Array.from( + new Set([ + ...(taskRunner.options.cacheableOperations ?? []), + ...targetNames, + ]) + ); + } + ); + + updateWorkspaceConfiguration(this.tree, workspaceConfig); + } + + // TODO(leo): This should be moved to BuilderMigrator once everything is split into builder migrators. + protected updateTsConfigFile( + tsConfigPath: string, + rootTsConfigFile: string, + projectOffsetFromRoot: string + ): void { + updateJson(this.tree, tsConfigPath, (json) => { + json.extends = `${projectOffsetFromRoot}${rootTsConfigFile}`; + json.compilerOptions = json.compilerOptions ?? {}; + json.compilerOptions.outDir = `${projectOffsetFromRoot}dist/out-tsc`; + return json; + }); + } +} diff --git a/packages/angular/src/generators/ng-add/utilities/__snapshots__/e2e.migrator.spec.ts.snap b/packages/angular/src/generators/ng-add/migrators/projects/__snapshots__/e2e.migrator.spec.ts.snap similarity index 100% rename from packages/angular/src/generators/ng-add/utilities/__snapshots__/e2e.migrator.spec.ts.snap rename to packages/angular/src/generators/ng-add/migrators/projects/__snapshots__/e2e.migrator.spec.ts.snap diff --git a/packages/angular/src/generators/ng-add/utilities/app.migrator.spec.ts b/packages/angular/src/generators/ng-add/migrators/projects/app.migrator.spec.ts similarity index 99% rename from packages/angular/src/generators/ng-add/utilities/app.migrator.spec.ts rename to packages/angular/src/generators/ng-add/migrators/projects/app.migrator.spec.ts index 6a4dfe1adf..3ad58f0ef0 100644 --- a/packages/angular/src/generators/ng-add/utilities/app.migrator.spec.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/app.migrator.spec.ts @@ -1,15 +1,17 @@ -import { +import type { ProjectConfiguration, + TargetConfiguration, + Tree, +} from '@nrwl/devkit'; +import { readJson, readProjectConfiguration, readWorkspaceConfiguration, - TargetConfiguration, - Tree, writeJson, } from '@nrwl/devkit'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import type { MigrationProjectConfiguration } from '../../utilities/types'; import { AppMigrator } from './app.migrator'; -import { MigrationProjectConfiguration } from './types'; type AngularCliProjectConfiguration = Omit & { architect?: { diff --git a/packages/angular/src/generators/ng-add/utilities/app.migrator.ts b/packages/angular/src/generators/ng-add/migrators/projects/app.migrator.ts similarity index 98% rename from packages/angular/src/generators/ng-add/utilities/app.migrator.ts rename to packages/angular/src/generators/ng-add/migrators/projects/app.migrator.ts index f82910a408..b0c85f440f 100644 --- a/packages/angular/src/generators/ng-add/utilities/app.migrator.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/app.migrator.ts @@ -1,9 +1,8 @@ +import type { TargetConfiguration, Tree } from '@nrwl/devkit'; import { joinPathFragments, offsetFromRoot, readJson, - TargetConfiguration, - Tree, updateJson, updateProjectConfiguration, } from '@nrwl/devkit'; @@ -11,15 +10,15 @@ import { hasRulesRequiringTypeChecking } from '@nrwl/linter'; import { convertToNxProjectGenerator } from '@nrwl/workspace/generators'; import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; import { basename } from 'path'; -import { GeneratorOptions } from '../schema'; -import { E2eMigrator } from './e2e.migrator'; -import { Logger } from './logger'; -import { ProjectMigrator } from './project.migrator'; -import { +import type { GeneratorOptions } from '../../schema'; +import type { + Logger, MigrationProjectConfiguration, Target, ValidationResult, -} from './types'; +} from '../../utilities'; +import { E2eMigrator } from './e2e.migrator'; +import { ProjectMigrator } from './project.migrator'; type SupportedTargets = | 'build' @@ -78,7 +77,7 @@ export class AppMigrator extends ProjectMigrator { } } - async migrate(): Promise { + override async migrate(): Promise { await this.e2eMigrator.migrate(); this.moveProjectFiles(); diff --git a/packages/angular/src/generators/ng-add/utilities/e2e.migrator.spec.ts b/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.spec.ts similarity index 99% rename from packages/angular/src/generators/ng-add/utilities/e2e.migrator.spec.ts rename to packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.spec.ts index 5a6dfaa46d..ab95f6ddf6 100644 --- a/packages/angular/src/generators/ng-add/utilities/e2e.migrator.spec.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.spec.ts @@ -9,21 +9,23 @@ jest.mock('fs', () => { })), }; }); + import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version'; +import type { + ProjectConfiguration, + TargetConfiguration, + Tree, +} from '@nrwl/devkit'; import { joinPathFragments, offsetFromRoot, - ProjectConfiguration, readJson, readProjectConfiguration, - TargetConfiguration, - Tree, writeJson, } from '@nrwl/devkit'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; -import { lstatSync } from 'fs'; +import type { MigrationProjectConfiguration } from '../../utilities'; import { E2eMigrator } from './e2e.migrator'; -import { MigrationProjectConfiguration } from './types'; type AngularCliProjectConfiguration = Omit & { architect?: { diff --git a/packages/angular/src/generators/ng-add/utilities/e2e.migrator.ts b/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.ts similarity index 99% rename from packages/angular/src/generators/ng-add/utilities/e2e.migrator.ts rename to packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.ts index 65dccb4be2..6ca001f536 100644 --- a/packages/angular/src/generators/ng-add/utilities/e2e.migrator.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/e2e.migrator.ts @@ -1,18 +1,20 @@ import { cypressProjectGenerator } from '@nrwl/cypress'; import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset'; import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version'; +import type { + ProjectConfiguration, + TargetConfiguration, + Tree, +} from '@nrwl/devkit'; import { addProjectConfiguration, joinPathFragments, names, offsetFromRoot, - ProjectConfiguration, readJson, readProjectConfiguration, removeProjectConfiguration, stripIndents, - TargetConfiguration, - Tree, updateJson, updateProjectConfiguration, visitNotIgnoredFiles, @@ -23,25 +25,27 @@ import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils'; import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; import { tsquery } from '@phenomnomnominal/tsquery'; import { basename, relative } from 'path'; +import type { + Node, + ObjectLiteralExpression, + PropertyAssignment, +} from 'typescript'; import { isObjectLiteralExpression, isPropertyAssignment, isStringLiteralLike, isTemplateExpression, - Node, - ObjectLiteralExpression, - PropertyAssignment, SyntaxKind, } from 'typescript'; -import { GeneratorOptions } from '../schema'; -import { FileChangeRecorder } from './file-change-recorder'; -import { Logger } from './logger'; -import { ProjectMigrator } from './project.migrator'; -import { +import type { GeneratorOptions } from '../../schema'; +import type { + Logger, MigrationProjectConfiguration, Target, ValidationResult, -} from './types'; +} from '../../utilities'; +import { FileChangeRecorder } from '../../utilities'; +import { ProjectMigrator } from './project.migrator'; type SupportedTargets = 'e2e'; const supportedTargets: Record = { @@ -101,7 +105,7 @@ export class E2eMigrator extends ProjectMigrator { this.initialize(); } - async migrate(): Promise { + override async migrate(): Promise { if (!this.targetNames.e2e) { this.logger.info( 'No e2e project was migrated because there was no "e2e" target declared in the "angular.json".' diff --git a/packages/angular/src/generators/ng-add/migrators/projects/index.ts b/packages/angular/src/generators/ng-add/migrators/projects/index.ts new file mode 100644 index 0000000000..61e754fa92 --- /dev/null +++ b/packages/angular/src/generators/ng-add/migrators/projects/index.ts @@ -0,0 +1,4 @@ +export * from './app.migrator'; +export * from './e2e.migrator'; +export * from './lib.migrator'; +export * from './project.migrator'; diff --git a/packages/angular/src/generators/ng-add/utilities/lib.migrator.spec.ts b/packages/angular/src/generators/ng-add/migrators/projects/lib.migrator.spec.ts similarity index 97% rename from packages/angular/src/generators/ng-add/utilities/lib.migrator.spec.ts rename to packages/angular/src/generators/ng-add/migrators/projects/lib.migrator.spec.ts index 610641d53f..b2cc8e2c18 100644 --- a/packages/angular/src/generators/ng-add/utilities/lib.migrator.spec.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/lib.migrator.spec.ts @@ -1,15 +1,17 @@ -import { +import type { ProjectConfiguration, + TargetConfiguration, + Tree, +} from '@nrwl/devkit'; +import { readJson, readProjectConfiguration, readWorkspaceConfiguration, - TargetConfiguration, - Tree, writeJson, } from '@nrwl/devkit'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import type { MigrationProjectConfiguration } from '../../utilities'; import { LibMigrator } from './lib.migrator'; -import { MigrationProjectConfiguration } from './types'; type AngularCliProjectConfiguration = Omit & { architect?: { @@ -126,8 +128,8 @@ describe('lib migrator', () => { expect(result[0].messageGroup.messages).toStrictEqual([ 'The "build" target is using an unsupported builder "@not/supported:builder".', ]); - expect(result[0].hint).toBe( - 'The supported builders for libraries are: "@angular-devkit/build-angular:ng-packagr", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint".' + expect(result[0].hint).toMatchInlineSnapshot( + `"The supported builders for libraries are: \\"@angular-devkit/build-angular:karma\\", \\"@angular-eslint/builder:lint\\" and \\"@angular-devkit/build-angular:ng-packagr\\"."` ); }); @@ -149,8 +151,8 @@ describe('lib migrator', () => { 'The "build" target is using an unsupported builder "@not/supported:builder".', 'The "test" target is using an unsupported builder "@other/not-supported:builder".', ]); - expect(result[0].hint).toBe( - 'The supported builders for libraries are: "@angular-devkit/build-angular:ng-packagr", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint".' + expect(result[0].hint).toMatchInlineSnapshot( + `"The supported builders for libraries are: \\"@angular-devkit/build-angular:karma\\", \\"@angular-eslint/builder:lint\\" and \\"@angular-devkit/build-angular:ng-packagr\\"."` ); }); @@ -168,8 +170,8 @@ describe('lib migrator', () => { expect(result[0].messageGroup.messages).toStrictEqual([ 'The "my-build" target is using an unsupported builder "@not/supported:builder".', ]); - expect(result[0].hint).toBe( - 'The supported builders for libraries are: "@angular-devkit/build-angular:ng-packagr", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint".' + expect(result[0].hint).toMatchInlineSnapshot( + `"The supported builders for libraries are: \\"@angular-devkit/build-angular:karma\\", \\"@angular-eslint/builder:lint\\" and \\"@angular-devkit/build-angular:ng-packagr\\"."` ); }); @@ -210,17 +212,17 @@ describe('lib migrator', () => { expect(result).toHaveLength(2); expect(result[0].message).toBe( - 'There is more than one target using a builder that is used to build the project ("build1" and "build2").' - ); - expect(result[0].hint).toBe( - 'Make sure the project only has one target with a builder that is used to build the project.' - ); - expect(result[1].message).toBe( 'There is more than one target using a builder that is used to lint the project ("lint1" and "lint2").' ); - expect(result[1].hint).toBe( + expect(result[0].hint).toBe( 'Make sure the project only has one target with a builder that is used to lint the project.' ); + expect(result[1].message).toBe( + 'There is more than one target using a builder that is used to build the project ("build1" and "build2").' + ); + expect(result[1].hint).toBe( + 'Make sure the project only has one target with a builder that is used to build the project.' + ); }); it('should succeed validation', async () => { @@ -274,7 +276,7 @@ describe('lib migrator', () => { await expect(migrator.migrate()).resolves.not.toThrow(); expect(mockedLogger.warn).toHaveBeenCalledWith( - 'There is no build target in the project configuration. This might not be an issue. Skipping updating the build configuration.' + 'There is no target in the project configuration using the @angular-devkit/build-angular:ng-packagr builder. This might not be an issue. Skipping updating the build configuration.' ); }); diff --git a/packages/angular/src/generators/ng-add/utilities/lib.migrator.ts b/packages/angular/src/generators/ng-add/migrators/projects/lib.migrator.ts similarity index 63% rename from packages/angular/src/generators/ng-add/utilities/lib.migrator.ts rename to packages/angular/src/generators/ng-add/migrators/projects/lib.migrator.ts index 59786c3080..f889e6546d 100644 --- a/packages/angular/src/generators/ng-add/utilities/lib.migrator.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/lib.migrator.ts @@ -1,32 +1,36 @@ +import type { Tree } from '@nrwl/devkit'; import { joinPathFragments, offsetFromRoot, readJson, - Tree, updateJson, updateProjectConfiguration, - writeJson, } from '@nrwl/devkit'; import { hasRulesRequiringTypeChecking } from '@nrwl/linter'; import { convertToNxProjectGenerator } from '@nrwl/workspace/generators'; import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; import { basename } from 'path'; -import { addBuildableLibrariesPostCssDependencies } from '../../utils/dependencies'; -import { GeneratorOptions } from '../schema'; -import { Logger } from './logger'; -import { ProjectMigrator } from './project.migrator'; -import { +import type { GeneratorOptions } from '../../schema'; +import type { + Logger, MigrationProjectConfiguration, Target, + ValidationError, ValidationResult, -} from './types'; +} from '../../utilities'; +import type { BuilderMigratorClassType } from '../builders'; +import { AngularDevkitNgPackagrMigrator } from '../builders'; +import { ProjectMigrator } from './project.migrator'; -type SupportedTargets = 'build' | 'test' | 'lint'; +type SupportedTargets = 'test' | 'lint'; const supportedTargets: Record = { - build: { builders: ['@angular-devkit/build-angular:ng-packagr'] }, test: { builders: ['@angular-devkit/build-angular:karma'] }, lint: { builders: ['@angular-eslint/builder:lint'] }, }; +// TODO(leo): this will replace `supportedTargets` once the full refactor is done. +const supportedBuilderMigrators: BuilderMigratorClassType[] = [ + AngularDevkitNgPackagrMigrator, +]; export class LibMigrator extends ProjectMigrator { private oldEsLintConfigPath: string; @@ -38,7 +42,15 @@ export class LibMigrator extends ProjectMigrator { project: MigrationProjectConfiguration, logger?: Logger ) { - super(tree, options, supportedTargets, project, 'libs', logger); + super( + tree, + options, + supportedTargets, + project, + 'libs', + logger, + supportedBuilderMigrators + ); if (this.targetNames.lint) { this.oldEsLintConfigPath = @@ -49,24 +61,29 @@ export class LibMigrator extends ProjectMigrator { } } - async migrate(): Promise { + override async migrate(): Promise { await this.updateProjectConfiguration(); this.moveProjectFiles(); - this.updateNgPackageJson(); + + for (const builderMigrator of this.builderMigrators ?? []) { + await builderMigrator.migrate(); + } + this.updateTsConfigs(); this.updateEsLintConfig(); this.updateCacheableOperations( - [ - this.targetNames.build, - this.targetNames.lint, - this.targetNames.test, - ].filter(Boolean) + [this.targetNames.lint, this.targetNames.test].filter(Boolean) ); - addBuildableLibrariesPostCssDependencies(this.tree); } override validate(): ValidationResult { - return super.validate(); + const errors: ValidationError[] = [...(super.validate() ?? [])]; + + for (const builderMigrator of this.builderMigrators) { + errors.push(...(builderMigrator.validate() ?? [])); + } + + return errors.length ? errors : null; } private moveProjectFiles(): void { @@ -85,7 +102,6 @@ export class LibMigrator extends ProjectMigrator { 'The project does not have any targets configured. This might not be an issue. Skipping updating targets.' ); } else { - this.updateBuildTargetConfiguration(); this.updateLintTargetConfiguration(); this.updateTestTargetConfiguration(); } @@ -100,33 +116,10 @@ export class LibMigrator extends ProjectMigrator { }); } - private updateNgPackageJson(): void { - const buildTarget = this.projectConfig.targets?.[this.targetNames.build]; - if ( - !buildTarget?.options?.project || - !this.tree.exists(buildTarget.options.project) - ) { - // we already logged a warning for these cases, so just return - return; - } - - const ngPackageJson = readJson(this.tree, buildTarget.options.project); - const offset = offsetFromRoot(this.project.newRoot); - ngPackageJson.$schema = - ngPackageJson.$schema && - `${offset}node_modules/ng-packagr/ng-package.schema.json`; - ngPackageJson.dest = `${offset}dist/${this.project.name}`; - writeJson(this.tree, buildTarget.options.project, ngPackageJson); - } - private updateTsConfigs(): void { const rootTsConfigFile = getRootTsConfigPathInTree(this.tree); const projectOffsetFromRoot = offsetFromRoot(this.projectConfig.root); - this.updateTsConfigFileUsedByBuildTarget( - rootTsConfigFile, - projectOffsetFromRoot - ); this.updateTsConfigFileUsedByTestTarget( rootTsConfigFile, projectOffsetFromRoot @@ -176,72 +169,6 @@ export class LibMigrator extends ProjectMigrator { }); } - private updateBuildTargetConfiguration(): void { - if (!this.targetNames.build) { - this.logger.warn( - 'There is no build target in the project configuration. This might not be an issue. Skipping updating the build configuration.' - ); - return; - } - - const buildTarget = this.projectConfig.targets[this.targetNames.build]; - buildTarget.executor = '@nrwl/angular:package'; - - if ( - !buildTarget.options && - (!buildTarget.configurations || - !Object.keys(buildTarget.configurations).length) - ) { - this.logger.warn( - `The target "${this.targetNames.build}" is not specifying any options or configurations. Skipping updating the target configuration.` - ); - return; - } - - const buildDevTsConfig = - buildTarget.options?.tsConfig ?? - buildTarget.configurations?.development?.tsConfig; - if (!buildDevTsConfig) { - this.logger.warn( - `The "${this.targetNames.build}" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.` - ); - } else if (!this.tree.exists(buildDevTsConfig)) { - this.logger.warn( - `The tsConfig file "${buildDevTsConfig}" specified in the "${this.targetNames.build}" target could not be found. Skipping updating the tsConfig file.` - ); - } - - if (!buildTarget.options?.project) { - this.logger.warn( - `The "${this.targetNames.build}" target does not have the "project" option configured. Skipping updating the ng-packagr project file ("ng-package.json").` - ); - } else if (!this.tree.exists(buildTarget.options.project)) { - this.logger.warn( - `The ng-packagr project file "${buildTarget.options.project}" specified in the "${this.targetNames.build}" target could not be found. Skipping updating the ng-packagr project file.` - ); - } - - ['project', 'tsConfig'].forEach((option) => { - if (buildTarget.options?.[option]) { - buildTarget.options[option] = joinPathFragments( - this.project.newRoot, - basename(buildTarget.options[option]) - ); - } - - for (const configuration of Object.values( - buildTarget.configurations ?? {} - )) { - configuration[option] = - configuration[option] && - joinPathFragments( - this.project.newRoot, - basename(configuration[option]) - ); - } - }); - } - private updateLintTargetConfiguration(): void { if (!this.targetNames.lint) { return; @@ -353,39 +280,6 @@ export class LibMigrator extends ProjectMigrator { testOptions.scripts.map((script) => this.convertAsset(script)); } - private updateTsConfigFileUsedByBuildTarget( - rootTsConfigFile: string, - projectOffsetFromRoot: string - ): void { - if (!this.targetNames.build) { - return; - } - - const tsConfigPath = - this.projectConfig.targets[this.targetNames.build].options?.tsConfig ?? - this.projectConfig.targets[this.targetNames.build].configurations - ?.development?.tsConfig; - - if (!tsConfigPath || !this.tree.exists(tsConfigPath)) { - // we already logged a warning for these cases, so just return - return; - } - - this.updateTsConfigFile( - tsConfigPath, - rootTsConfigFile, - projectOffsetFromRoot - ); - - updateJson(this.tree, tsConfigPath, (json) => { - if (!json.include?.length && !json.files?.length) { - json.include = ['**/*.ts']; - } - - return json; - }); - } - private updateTsConfigFileUsedByTestTarget( rootTsConfigFile: string, projectOffsetFromRoot: string diff --git a/packages/angular/src/generators/ng-add/utilities/project.migrator.ts b/packages/angular/src/generators/ng-add/migrators/projects/project.migrator.ts similarity index 81% rename from packages/angular/src/generators/ng-add/utilities/project.migrator.ts rename to packages/angular/src/generators/ng-add/migrators/projects/project.migrator.ts index 930bcfc675..1f930b83a4 100644 --- a/packages/angular/src/generators/ng-add/utilities/project.migrator.ts +++ b/packages/angular/src/generators/ng-add/migrators/projects/project.migrator.ts @@ -1,50 +1,46 @@ +import type { TargetConfiguration, Tree } from '@nrwl/devkit'; import { joinPathFragments, normalizePath, offsetFromRoot, - ProjectConfiguration, - readWorkspaceConfiguration, - TargetConfiguration, - Tree, - updateJson, - updateWorkspaceConfiguration, visitNotIgnoredFiles, } from '@nrwl/devkit'; import { basename, dirname } from 'path'; -import { GeneratorOptions } from '../schema'; -import { Logger } from './logger'; -import { +import type { GeneratorOptions } from '../../schema'; +import type { MigrationProjectConfiguration, Target, + ValidationError, ValidationResult, -} from './types'; -import { arrayToString } from './validation-logging'; +} from '../../utilities'; +import { arrayToString, Logger } from '../../utilities'; +import type { BuilderMigratorClassType } from '../builders'; +import { BuilderMigrator } from '../builders'; +import { Migrator } from '../migrator'; -export abstract class ProjectMigrator { +export abstract class ProjectMigrator< + TargetType extends string = string +> extends Migrator { public get projectName(): string { return this.project.name; } - protected projectConfig: ProjectConfiguration; - protected project: { - name: string; - oldRoot: string; - oldSourceRoot: string; - newRoot: string; - newSourceRoot: string; - }; - protected logger: Logger; + protected builderMigrators: BuilderMigrator[]; protected readonly targetNames: Partial> = {}; constructor( - protected readonly tree: Tree, + tree: Tree, protected readonly options: GeneratorOptions, protected readonly targets: Record, project: MigrationProjectConfiguration, rootDir: string, - logger?: Logger + logger?: Logger, + // TODO(leo): this will replace `targets` and become required once the full + // refactor is done. + supportedBuilderMigrators?: BuilderMigratorClassType[] ) { - this.projectConfig = project.config; + super(tree, project.config, logger ?? new Logger(project.name)); + this.project = { name: project.name, oldRoot: this.projectConfig.root ?? '', @@ -55,22 +51,19 @@ export abstract class ProjectMigrator { newSourceRoot: `${rootDir}/${project.name}/src`, }; - this.logger = logger ?? new Logger(this.project.name); - this.collectTargetNames(); + this.createBuilderMigrators(supportedBuilderMigrators); } - abstract migrate(): Promise; - - validate(): ValidationResult { - const result: ValidationResult = []; + override validate(): ValidationResult { + const errors: ValidationError[] = []; // check project root if ( this.projectConfig.root === undefined || this.projectConfig.root === null ) { - result.push({ + errors.push({ message: 'The project root is not defined in the project configuration.', hint: @@ -81,7 +74,7 @@ export abstract class ProjectMigrator { this.projectConfig.root !== '' && !this.tree.exists(this.projectConfig.root) ) { - result.push({ + errors.push({ message: `The project root "${this.project.oldRoot}" could not be found.`, hint: `Make sure the value for "projects.${this.project.name}.root" is correct ` + @@ -94,7 +87,7 @@ export abstract class ProjectMigrator { this.projectConfig.sourceRoot && !this.tree.exists(this.projectConfig.sourceRoot) ) { - result.push({ + errors.push({ message: `The project source root "${this.project.oldSourceRoot}" could not be found.`, hint: `Make sure the value for "projects.${this.project.name}.sourceRoot" is correct ` + @@ -108,6 +101,9 @@ export abstract class ProjectMigrator { .map((x) => x.builders) .flat(), ]; + allSupportedBuilders.push( + ...this.builderMigrators.map((migrator) => migrator.builderName) + ); const unsupportedBuilders: [target: string, builder: string][] = []; Object.entries(this.projectConfig.targets ?? {}).forEach( @@ -119,7 +115,7 @@ export abstract class ProjectMigrator { ); if (unsupportedBuilders.length) { - result.push({ + errors.push({ messageGroup: { title: 'Unsupported builders', messages: unsupportedBuilders.map( @@ -163,7 +159,7 @@ export abstract class ProjectMigrator { return; } - result.push({ + errors.push({ message: `There is more than one target using a builder that is used to ${targetType} the project (${arrayToString( targetsByType[targetType] )}).`, @@ -171,7 +167,7 @@ export abstract class ProjectMigrator { }); }); - return result.length ? result : null; + return errors.length ? errors : null; } protected convertAsset(asset: string | any): string | any { @@ -310,41 +306,6 @@ export abstract class ProjectMigrator { } } - protected updateCacheableOperations(targetNames: string[]): void { - if (!targetNames.length) { - return; - } - - const workspaceConfig = readWorkspaceConfiguration(this.tree); - - Object.keys(workspaceConfig.tasksRunnerOptions ?? {}).forEach( - (taskRunnerName) => { - const taskRunner = workspaceConfig.tasksRunnerOptions[taskRunnerName]; - taskRunner.options.cacheableOperations = Array.from( - new Set([ - ...(taskRunner.options.cacheableOperations ?? []), - ...targetNames, - ]) - ); - } - ); - - updateWorkspaceConfiguration(this.tree, workspaceConfig); - } - - protected updateTsConfigFile( - tsConfigPath: string, - rootTsConfigFile: string, - projectOffsetFromRoot: string - ): void { - updateJson(this.tree, tsConfigPath, (json) => { - json.extends = `${projectOffsetFromRoot}${rootTsConfigFile}`; - json.compilerOptions = json.compilerOptions ?? {}; - json.compilerOptions.outDir = `${projectOffsetFromRoot}dist/out-tsc`; - return json; - }); - } - private collectTargetNames(): void { const targetTypes = Object.keys(this.targets) as TargetType[]; @@ -361,4 +322,23 @@ export abstract class ProjectMigrator { } ); } + + private createBuilderMigrators( + supportedBuilderMigrators?: BuilderMigratorClassType[] + ): void { + if (!supportedBuilderMigrators) { + this.builderMigrators = []; + return; + } + + this.builderMigrators = supportedBuilderMigrators.map( + (migratorClass) => + new migratorClass( + this.tree, + this.project, + this.projectConfig, + this.logger + ) + ); + } } diff --git a/packages/angular/src/generators/ng-add/ng-add.spec.ts b/packages/angular/src/generators/ng-add/ng-add.spec.ts index 4530d2056e..df195462c2 100644 --- a/packages/angular/src/generators/ng-add/ng-add.spec.ts +++ b/packages/angular/src/generators/ng-add/ng-add.spec.ts @@ -1,7 +1,7 @@ import * as angularCliMigrator from './migrate-from-angular-cli'; import * as initGenerator from '../init/init'; import { ngAddGenerator } from './ng-add'; -import { Tree } from '@nrwl/devkit'; +import type { Tree } from '@nrwl/devkit'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; describe('ngAdd generator', () => { diff --git a/packages/angular/src/generators/ng-add/utilities/get-all-projects.ts b/packages/angular/src/generators/ng-add/utilities/get-all-projects.ts index abb4689c89..2ba49677c5 100644 --- a/packages/angular/src/generators/ng-add/utilities/get-all-projects.ts +++ b/packages/angular/src/generators/ng-add/utilities/get-all-projects.ts @@ -1,5 +1,5 @@ import { getProjects, Tree } from '@nrwl/devkit'; -import { MigrationProjectConfiguration, WorkspaceProjects } from './types'; +import type { MigrationProjectConfiguration, WorkspaceProjects } from './types'; export function getAllProjects(tree: Tree): WorkspaceProjects { const projects = getProjects(tree); diff --git a/packages/angular/src/generators/ng-add/utilities/index.ts b/packages/angular/src/generators/ng-add/utilities/index.ts new file mode 100644 index 0000000000..470975b4f8 --- /dev/null +++ b/packages/angular/src/generators/ng-add/utilities/index.ts @@ -0,0 +1,8 @@ +export * from './file-change-recorder'; +export * from './get-all-projects'; +export * from './logger'; +export * from './normalize-options'; +export * from './types'; +export * from './validate-projects'; +export * from './validation-logging'; +export * from './workspace'; diff --git a/packages/angular/src/generators/ng-add/utilities/normalize-options.ts b/packages/angular/src/generators/ng-add/utilities/normalize-options.ts index 716c48f261..90d3998a4a 100644 --- a/packages/angular/src/generators/ng-add/utilities/normalize-options.ts +++ b/packages/angular/src/generators/ng-add/utilities/normalize-options.ts @@ -1,12 +1,12 @@ +import type { Tree } from '@nrwl/devkit'; import { detectWorkspaceScope, joinPathFragments, names, readJson, - Tree, } from '@nrwl/devkit'; -import { GeneratorOptions } from '../schema'; -import { WorkspaceProjects } from './types'; +import type { GeneratorOptions } from '../schema'; +import type { WorkspaceProjects } from './types'; export function normalizeOptions( tree: Tree, diff --git a/packages/angular/src/generators/ng-add/utilities/types.ts b/packages/angular/src/generators/ng-add/utilities/types.ts index 509d669c64..c5d703d86e 100644 --- a/packages/angular/src/generators/ng-add/utilities/types.ts +++ b/packages/angular/src/generators/ng-add/utilities/types.ts @@ -1,4 +1,4 @@ -import { ProjectConfiguration } from '@nrwl/devkit'; +import type { ProjectConfiguration } from '@nrwl/devkit'; export type MigrationProjectConfiguration = { config: ProjectConfiguration; @@ -10,6 +10,14 @@ export type WorkspaceProjects = { libs: MigrationProjectConfiguration[]; }; +export type ProjectMigrationInfo = { + name: string; + oldRoot: string; + oldSourceRoot: string; + newRoot: string; + newSourceRoot: string; +}; + export type WorkspaceCapabilities = { karma: boolean; eslint: boolean; diff --git a/packages/angular/src/generators/ng-add/utilities/validate-projects.spec.ts b/packages/angular/src/generators/ng-add/utilities/validate-projects.spec.ts index a0b2e278dc..213b4afae9 100644 --- a/packages/angular/src/generators/ng-add/utilities/validate-projects.spec.ts +++ b/packages/angular/src/generators/ng-add/utilities/validate-projects.spec.ts @@ -1,5 +1,5 @@ import * as chalk from 'chalk'; -import { ProjectMigrator } from './project.migrator'; +import type { ProjectMigrator } from '../migrators'; import { validateProjects } from './validate-projects'; describe('validateProjects', () => { diff --git a/packages/angular/src/generators/ng-add/utilities/validate-projects.ts b/packages/angular/src/generators/ng-add/utilities/validate-projects.ts index 6bd5e67834..1ab7915226 100644 --- a/packages/angular/src/generators/ng-add/utilities/validate-projects.ts +++ b/packages/angular/src/generators/ng-add/utilities/validate-projects.ts @@ -1,5 +1,6 @@ +// TODO(leo): this should probably move into a workspace-wide migrator import * as chalk from 'chalk'; -import { ProjectMigrator } from './project.migrator'; +import type { ProjectMigrator } from '../migrators'; import { ValidationError } from './types'; import { workspaceMigrationErrorHeading } from './validation-logging'; diff --git a/packages/angular/src/generators/ng-add/utilities/workspace.ts b/packages/angular/src/generators/ng-add/utilities/workspace.ts index 7749472727..b3d14e9c00 100644 --- a/packages/angular/src/generators/ng-add/utilities/workspace.ts +++ b/packages/angular/src/generators/ng-add/utilities/workspace.ts @@ -1,10 +1,9 @@ +import type { NxJsonConfiguration, Tree } from '@nrwl/devkit'; import { generateFiles, joinPathFragments, - NxJsonConfiguration, readJson, readWorkspaceConfiguration, - Tree, updateJson, updateWorkspaceConfiguration, writeJson, @@ -19,8 +18,8 @@ import { readFileSync } from 'fs'; import { readModulePackageJson } from 'nx/src/utils/package-json'; import { dirname, join } from 'path'; import { angularDevkitVersion, nxVersion } from '../../../utils/versions'; -import { GeneratorOptions } from '../schema'; -import { WorkspaceCapabilities, WorkspaceProjects } from './types'; +import type { GeneratorOptions } from '../schema'; +import type { WorkspaceCapabilities, WorkspaceProjects } from './types'; import { workspaceMigrationErrorHeading } from './validation-logging'; export function validateWorkspace(tree: Tree): void {