cleanup(angular): add @angular-devkit/build-angular:ng-packagr builder migrator (#12682)

This commit is contained in:
Leosvel Pérez Espinosa 2022-10-24 12:13:33 +02:00 committed by GitHub
parent 2f46797535
commit f4288e6bc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 484 additions and 284 deletions

View File

@ -1,7 +1,7 @@
import type { Tree } from '@nrwl/devkit';
import { import {
readJson, readJson,
readProjectConfiguration, readProjectConfiguration,
Tree,
updateJson, updateJson,
writeJson, writeJson,
} from '@nrwl/devkit'; } from '@nrwl/devkit';

View File

@ -1,34 +1,33 @@
import type { Tree } from '@nrwl/devkit';
import { import {
addDependenciesToPackageJson, addDependenciesToPackageJson,
formatFiles, formatFiles,
installPackagesTask, installPackagesTask,
readJson, readJson,
readWorkspaceConfiguration, readWorkspaceConfiguration,
Tree,
updateJson, updateJson,
updateWorkspaceConfiguration, updateWorkspaceConfiguration,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { nxVersion } from '../../utils/versions'; import { nxVersion } from '../../utils/versions';
import type { ProjectMigrator } from './migrators';
import { AppMigrator, LibMigrator } from './migrators';
import type { GeneratorOptions } from './schema'; 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 { import {
cleanupEsLintPackages, cleanupEsLintPackages,
createNxJson, createNxJson,
createRootKarmaConfig, createRootKarmaConfig,
createWorkspaceFiles, createWorkspaceFiles,
decorateAngularCli, decorateAngularCli,
getAllProjects,
getWorkspaceCapabilities, getWorkspaceCapabilities,
normalizeOptions,
updatePackageJson, updatePackageJson,
updateRootEsLintConfig, updateRootEsLintConfig,
updateRootTsConfig, updateRootTsConfig,
updateWorkspaceConfigDefaults, updateWorkspaceConfigDefaults,
validateProjects,
validateWorkspace, validateWorkspace,
} from './utilities/workspace'; } from './utilities';
export async function migrateFromAngularCli( export async function migrateFromAngularCli(
tree: Tree, tree: Tree,

View File

@ -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;
});
}
}

View File

@ -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;
};

View File

@ -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<string, TargetConfiguration> = 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);
}
}
}
}

View File

@ -0,0 +1,3 @@
export * from './angular-devkit-ng-packagr.migrator';
export * from './builder-migrator-class.type';
export * from './builder.migrator';

View File

@ -0,0 +1,2 @@
export * from './builders';
export * from './projects';

View File

@ -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> | 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;
});
}
}

View File

@ -1,15 +1,17 @@
import { import type {
ProjectConfiguration, ProjectConfiguration,
TargetConfiguration,
Tree,
} from '@nrwl/devkit';
import {
readJson, readJson,
readProjectConfiguration, readProjectConfiguration,
readWorkspaceConfiguration, readWorkspaceConfiguration,
TargetConfiguration,
Tree,
writeJson, writeJson,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import type { MigrationProjectConfiguration } from '../../utilities/types';
import { AppMigrator } from './app.migrator'; import { AppMigrator } from './app.migrator';
import { MigrationProjectConfiguration } from './types';
type AngularCliProjectConfiguration = Omit<ProjectConfiguration, 'targets'> & { type AngularCliProjectConfiguration = Omit<ProjectConfiguration, 'targets'> & {
architect?: { architect?: {

View File

@ -1,9 +1,8 @@
import type { TargetConfiguration, Tree } from '@nrwl/devkit';
import { import {
joinPathFragments, joinPathFragments,
offsetFromRoot, offsetFromRoot,
readJson, readJson,
TargetConfiguration,
Tree,
updateJson, updateJson,
updateProjectConfiguration, updateProjectConfiguration,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
@ -11,15 +10,15 @@ import { hasRulesRequiringTypeChecking } from '@nrwl/linter';
import { convertToNxProjectGenerator } from '@nrwl/workspace/generators'; import { convertToNxProjectGenerator } from '@nrwl/workspace/generators';
import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript';
import { basename } from 'path'; import { basename } from 'path';
import { GeneratorOptions } from '../schema'; import type { GeneratorOptions } from '../../schema';
import { E2eMigrator } from './e2e.migrator'; import type {
import { Logger } from './logger'; Logger,
import { ProjectMigrator } from './project.migrator';
import {
MigrationProjectConfiguration, MigrationProjectConfiguration,
Target, Target,
ValidationResult, ValidationResult,
} from './types'; } from '../../utilities';
import { E2eMigrator } from './e2e.migrator';
import { ProjectMigrator } from './project.migrator';
type SupportedTargets = type SupportedTargets =
| 'build' | 'build'
@ -78,7 +77,7 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
} }
} }
async migrate(): Promise<void> { override async migrate(): Promise<void> {
await this.e2eMigrator.migrate(); await this.e2eMigrator.migrate();
this.moveProjectFiles(); this.moveProjectFiles();

View File

@ -9,21 +9,23 @@ jest.mock('fs', () => {
})), })),
}; };
}); });
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version'; import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import type {
ProjectConfiguration,
TargetConfiguration,
Tree,
} from '@nrwl/devkit';
import { import {
joinPathFragments, joinPathFragments,
offsetFromRoot, offsetFromRoot,
ProjectConfiguration,
readJson, readJson,
readProjectConfiguration, readProjectConfiguration,
TargetConfiguration,
Tree,
writeJson, writeJson,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import { lstatSync } from 'fs'; import type { MigrationProjectConfiguration } from '../../utilities';
import { E2eMigrator } from './e2e.migrator'; import { E2eMigrator } from './e2e.migrator';
import { MigrationProjectConfiguration } from './types';
type AngularCliProjectConfiguration = Omit<ProjectConfiguration, 'targets'> & { type AngularCliProjectConfiguration = Omit<ProjectConfiguration, 'targets'> & {
architect?: { architect?: {

View File

@ -1,18 +1,20 @@
import { cypressProjectGenerator } from '@nrwl/cypress'; import { cypressProjectGenerator } from '@nrwl/cypress';
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset'; import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version'; import { installedCypressVersion } from '@nrwl/cypress/src/utils/cypress-version';
import type {
ProjectConfiguration,
TargetConfiguration,
Tree,
} from '@nrwl/devkit';
import { import {
addProjectConfiguration, addProjectConfiguration,
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot, offsetFromRoot,
ProjectConfiguration,
readJson, readJson,
readProjectConfiguration, readProjectConfiguration,
removeProjectConfiguration, removeProjectConfiguration,
stripIndents, stripIndents,
TargetConfiguration,
Tree,
updateJson, updateJson,
updateProjectConfiguration, updateProjectConfiguration,
visitNotIgnoredFiles, visitNotIgnoredFiles,
@ -23,25 +25,27 @@ import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils';
import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript';
import { tsquery } from '@phenomnomnominal/tsquery'; import { tsquery } from '@phenomnomnominal/tsquery';
import { basename, relative } from 'path'; import { basename, relative } from 'path';
import type {
Node,
ObjectLiteralExpression,
PropertyAssignment,
} from 'typescript';
import { import {
isObjectLiteralExpression, isObjectLiteralExpression,
isPropertyAssignment, isPropertyAssignment,
isStringLiteralLike, isStringLiteralLike,
isTemplateExpression, isTemplateExpression,
Node,
ObjectLiteralExpression,
PropertyAssignment,
SyntaxKind, SyntaxKind,
} from 'typescript'; } from 'typescript';
import { GeneratorOptions } from '../schema'; import type { GeneratorOptions } from '../../schema';
import { FileChangeRecorder } from './file-change-recorder'; import type {
import { Logger } from './logger'; Logger,
import { ProjectMigrator } from './project.migrator';
import {
MigrationProjectConfiguration, MigrationProjectConfiguration,
Target, Target,
ValidationResult, ValidationResult,
} from './types'; } from '../../utilities';
import { FileChangeRecorder } from '../../utilities';
import { ProjectMigrator } from './project.migrator';
type SupportedTargets = 'e2e'; type SupportedTargets = 'e2e';
const supportedTargets: Record<SupportedTargets, Target> = { const supportedTargets: Record<SupportedTargets, Target> = {
@ -101,7 +105,7 @@ export class E2eMigrator extends ProjectMigrator<SupportedTargets> {
this.initialize(); this.initialize();
} }
async migrate(): Promise<void> { override async migrate(): Promise<void> {
if (!this.targetNames.e2e) { if (!this.targetNames.e2e) {
this.logger.info( this.logger.info(
'No e2e project was migrated because there was no "e2e" target declared in the "angular.json".' 'No e2e project was migrated because there was no "e2e" target declared in the "angular.json".'

View File

@ -0,0 +1,4 @@
export * from './app.migrator';
export * from './e2e.migrator';
export * from './lib.migrator';
export * from './project.migrator';

View File

@ -1,15 +1,17 @@
import { import type {
ProjectConfiguration, ProjectConfiguration,
TargetConfiguration,
Tree,
} from '@nrwl/devkit';
import {
readJson, readJson,
readProjectConfiguration, readProjectConfiguration,
readWorkspaceConfiguration, readWorkspaceConfiguration,
TargetConfiguration,
Tree,
writeJson, writeJson,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import type { MigrationProjectConfiguration } from '../../utilities';
import { LibMigrator } from './lib.migrator'; import { LibMigrator } from './lib.migrator';
import { MigrationProjectConfiguration } from './types';
type AngularCliProjectConfiguration = Omit<ProjectConfiguration, 'targets'> & { type AngularCliProjectConfiguration = Omit<ProjectConfiguration, 'targets'> & {
architect?: { architect?: {
@ -126,8 +128,8 @@ describe('lib migrator', () => {
expect(result[0].messageGroup.messages).toStrictEqual([ expect(result[0].messageGroup.messages).toStrictEqual([
'The "build" target is using an unsupported builder "@not/supported:builder".', 'The "build" target is using an unsupported builder "@not/supported:builder".',
]); ]);
expect(result[0].hint).toBe( expect(result[0].hint).toMatchInlineSnapshot(
'The supported builders for libraries are: "@angular-devkit/build-angular:ng-packagr", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint".' `"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 "build" target is using an unsupported builder "@not/supported:builder".',
'The "test" target is using an unsupported builder "@other/not-supported:builder".', 'The "test" target is using an unsupported builder "@other/not-supported:builder".',
]); ]);
expect(result[0].hint).toBe( expect(result[0].hint).toMatchInlineSnapshot(
'The supported builders for libraries are: "@angular-devkit/build-angular:ng-packagr", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint".' `"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([ expect(result[0].messageGroup.messages).toStrictEqual([
'The "my-build" target is using an unsupported builder "@not/supported:builder".', 'The "my-build" target is using an unsupported builder "@not/supported:builder".',
]); ]);
expect(result[0].hint).toBe( expect(result[0].hint).toMatchInlineSnapshot(
'The supported builders for libraries are: "@angular-devkit/build-angular:ng-packagr", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint".' `"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).toHaveLength(2);
expect(result[0].message).toBe( 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").' '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.' '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 () => { it('should succeed validation', async () => {
@ -274,7 +276,7 @@ describe('lib migrator', () => {
await expect(migrator.migrate()).resolves.not.toThrow(); await expect(migrator.migrate()).resolves.not.toThrow();
expect(mockedLogger.warn).toHaveBeenCalledWith( 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.'
); );
}); });

View File

@ -1,32 +1,36 @@
import type { Tree } from '@nrwl/devkit';
import { import {
joinPathFragments, joinPathFragments,
offsetFromRoot, offsetFromRoot,
readJson, readJson,
Tree,
updateJson, updateJson,
updateProjectConfiguration, updateProjectConfiguration,
writeJson,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { hasRulesRequiringTypeChecking } from '@nrwl/linter'; import { hasRulesRequiringTypeChecking } from '@nrwl/linter';
import { convertToNxProjectGenerator } from '@nrwl/workspace/generators'; import { convertToNxProjectGenerator } from '@nrwl/workspace/generators';
import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript'; import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript';
import { basename } from 'path'; import { basename } from 'path';
import { addBuildableLibrariesPostCssDependencies } from '../../utils/dependencies'; import type { GeneratorOptions } from '../../schema';
import { GeneratorOptions } from '../schema'; import type {
import { Logger } from './logger'; Logger,
import { ProjectMigrator } from './project.migrator';
import {
MigrationProjectConfiguration, MigrationProjectConfiguration,
Target, Target,
ValidationError,
ValidationResult, 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<SupportedTargets, Target> = { const supportedTargets: Record<SupportedTargets, Target> = {
build: { builders: ['@angular-devkit/build-angular:ng-packagr'] },
test: { builders: ['@angular-devkit/build-angular:karma'] }, test: { builders: ['@angular-devkit/build-angular:karma'] },
lint: { builders: ['@angular-eslint/builder:lint'] }, 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<SupportedTargets> { export class LibMigrator extends ProjectMigrator<SupportedTargets> {
private oldEsLintConfigPath: string; private oldEsLintConfigPath: string;
@ -38,7 +42,15 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
project: MigrationProjectConfiguration, project: MigrationProjectConfiguration,
logger?: Logger logger?: Logger
) { ) {
super(tree, options, supportedTargets, project, 'libs', logger); super(
tree,
options,
supportedTargets,
project,
'libs',
logger,
supportedBuilderMigrators
);
if (this.targetNames.lint) { if (this.targetNames.lint) {
this.oldEsLintConfigPath = this.oldEsLintConfigPath =
@ -49,24 +61,29 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
} }
} }
async migrate(): Promise<void> { override async migrate(): Promise<void> {
await this.updateProjectConfiguration(); await this.updateProjectConfiguration();
this.moveProjectFiles(); this.moveProjectFiles();
this.updateNgPackageJson();
for (const builderMigrator of this.builderMigrators ?? []) {
await builderMigrator.migrate();
}
this.updateTsConfigs(); this.updateTsConfigs();
this.updateEsLintConfig(); this.updateEsLintConfig();
this.updateCacheableOperations( this.updateCacheableOperations(
[ [this.targetNames.lint, this.targetNames.test].filter(Boolean)
this.targetNames.build,
this.targetNames.lint,
this.targetNames.test,
].filter(Boolean)
); );
addBuildableLibrariesPostCssDependencies(this.tree);
} }
override validate(): ValidationResult { 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 { private moveProjectFiles(): void {
@ -85,7 +102,6 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
'The project does not have any targets configured. This might not be an issue. Skipping updating targets.' 'The project does not have any targets configured. This might not be an issue. Skipping updating targets.'
); );
} else { } else {
this.updateBuildTargetConfiguration();
this.updateLintTargetConfiguration(); this.updateLintTargetConfiguration();
this.updateTestTargetConfiguration(); this.updateTestTargetConfiguration();
} }
@ -100,33 +116,10 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
}); });
} }
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 { private updateTsConfigs(): void {
const rootTsConfigFile = getRootTsConfigPathInTree(this.tree); const rootTsConfigFile = getRootTsConfigPathInTree(this.tree);
const projectOffsetFromRoot = offsetFromRoot(this.projectConfig.root); const projectOffsetFromRoot = offsetFromRoot(this.projectConfig.root);
this.updateTsConfigFileUsedByBuildTarget(
rootTsConfigFile,
projectOffsetFromRoot
);
this.updateTsConfigFileUsedByTestTarget( this.updateTsConfigFileUsedByTestTarget(
rootTsConfigFile, rootTsConfigFile,
projectOffsetFromRoot projectOffsetFromRoot
@ -176,72 +169,6 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
}); });
} }
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 { private updateLintTargetConfiguration(): void {
if (!this.targetNames.lint) { if (!this.targetNames.lint) {
return; return;
@ -353,39 +280,6 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
testOptions.scripts.map((script) => this.convertAsset(script)); 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( private updateTsConfigFileUsedByTestTarget(
rootTsConfigFile: string, rootTsConfigFile: string,
projectOffsetFromRoot: string projectOffsetFromRoot: string

View File

@ -1,50 +1,46 @@
import type { TargetConfiguration, Tree } from '@nrwl/devkit';
import { import {
joinPathFragments, joinPathFragments,
normalizePath, normalizePath,
offsetFromRoot, offsetFromRoot,
ProjectConfiguration,
readWorkspaceConfiguration,
TargetConfiguration,
Tree,
updateJson,
updateWorkspaceConfiguration,
visitNotIgnoredFiles, visitNotIgnoredFiles,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { basename, dirname } from 'path'; import { basename, dirname } from 'path';
import { GeneratorOptions } from '../schema'; import type { GeneratorOptions } from '../../schema';
import { Logger } from './logger'; import type {
import {
MigrationProjectConfiguration, MigrationProjectConfiguration,
Target, Target,
ValidationError,
ValidationResult, ValidationResult,
} from './types'; } from '../../utilities';
import { arrayToString } from './validation-logging'; import { arrayToString, Logger } from '../../utilities';
import type { BuilderMigratorClassType } from '../builders';
import { BuilderMigrator } from '../builders';
import { Migrator } from '../migrator';
export abstract class ProjectMigrator<TargetType extends string = any> { export abstract class ProjectMigrator<
TargetType extends string = string
> extends Migrator {
public get projectName(): string { public get projectName(): string {
return this.project.name; return this.project.name;
} }
protected projectConfig: ProjectConfiguration; protected builderMigrators: BuilderMigrator[];
protected project: {
name: string;
oldRoot: string;
oldSourceRoot: string;
newRoot: string;
newSourceRoot: string;
};
protected logger: Logger;
protected readonly targetNames: Partial<Record<TargetType, string>> = {}; protected readonly targetNames: Partial<Record<TargetType, string>> = {};
constructor( constructor(
protected readonly tree: Tree, tree: Tree,
protected readonly options: GeneratorOptions, protected readonly options: GeneratorOptions,
protected readonly targets: Record<TargetType, Target>, protected readonly targets: Record<TargetType, Target>,
project: MigrationProjectConfiguration, project: MigrationProjectConfiguration,
rootDir: string, 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 = { this.project = {
name: project.name, name: project.name,
oldRoot: this.projectConfig.root ?? '', oldRoot: this.projectConfig.root ?? '',
@ -55,22 +51,19 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
newSourceRoot: `${rootDir}/${project.name}/src`, newSourceRoot: `${rootDir}/${project.name}/src`,
}; };
this.logger = logger ?? new Logger(this.project.name);
this.collectTargetNames(); this.collectTargetNames();
this.createBuilderMigrators(supportedBuilderMigrators);
} }
abstract migrate(): Promise<void>; override validate(): ValidationResult {
const errors: ValidationError[] = [];
validate(): ValidationResult {
const result: ValidationResult = [];
// check project root // check project root
if ( if (
this.projectConfig.root === undefined || this.projectConfig.root === undefined ||
this.projectConfig.root === null this.projectConfig.root === null
) { ) {
result.push({ errors.push({
message: message:
'The project root is not defined in the project configuration.', 'The project root is not defined in the project configuration.',
hint: hint:
@ -81,7 +74,7 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
this.projectConfig.root !== '' && this.projectConfig.root !== '' &&
!this.tree.exists(this.projectConfig.root) !this.tree.exists(this.projectConfig.root)
) { ) {
result.push({ errors.push({
message: `The project root "${this.project.oldRoot}" could not be found.`, message: `The project root "${this.project.oldRoot}" could not be found.`,
hint: hint:
`Make sure the value for "projects.${this.project.name}.root" is correct ` + `Make sure the value for "projects.${this.project.name}.root" is correct ` +
@ -94,7 +87,7 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
this.projectConfig.sourceRoot && this.projectConfig.sourceRoot &&
!this.tree.exists(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.`, message: `The project source root "${this.project.oldSourceRoot}" could not be found.`,
hint: hint:
`Make sure the value for "projects.${this.project.name}.sourceRoot" is correct ` + `Make sure the value for "projects.${this.project.name}.sourceRoot" is correct ` +
@ -108,6 +101,9 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
.map((x) => x.builders) .map((x) => x.builders)
.flat(), .flat(),
]; ];
allSupportedBuilders.push(
...this.builderMigrators.map((migrator) => migrator.builderName)
);
const unsupportedBuilders: [target: string, builder: string][] = []; const unsupportedBuilders: [target: string, builder: string][] = [];
Object.entries(this.projectConfig.targets ?? {}).forEach( Object.entries(this.projectConfig.targets ?? {}).forEach(
@ -119,7 +115,7 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
); );
if (unsupportedBuilders.length) { if (unsupportedBuilders.length) {
result.push({ errors.push({
messageGroup: { messageGroup: {
title: 'Unsupported builders', title: 'Unsupported builders',
messages: unsupportedBuilders.map( messages: unsupportedBuilders.map(
@ -163,7 +159,7 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
return; return;
} }
result.push({ errors.push({
message: `There is more than one target using a builder that is used to ${targetType} the project (${arrayToString( message: `There is more than one target using a builder that is used to ${targetType} the project (${arrayToString(
targetsByType[targetType] targetsByType[targetType]
)}).`, )}).`,
@ -171,7 +167,7 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
}); });
}); });
return result.length ? result : null; return errors.length ? errors : null;
} }
protected convertAsset(asset: string | any): string | any { protected convertAsset(asset: string | any): string | any {
@ -310,41 +306,6 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
} }
} }
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 { private collectTargetNames(): void {
const targetTypes = Object.keys(this.targets) as TargetType[]; const targetTypes = Object.keys(this.targets) as TargetType[];
@ -361,4 +322,23 @@ export abstract class ProjectMigrator<TargetType extends string = any> {
} }
); );
} }
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
)
);
}
} }

View File

@ -1,7 +1,7 @@
import * as angularCliMigrator from './migrate-from-angular-cli'; import * as angularCliMigrator from './migrate-from-angular-cli';
import * as initGenerator from '../init/init'; import * as initGenerator from '../init/init';
import { ngAddGenerator } from './ng-add'; import { ngAddGenerator } from './ng-add';
import { Tree } from '@nrwl/devkit'; import type { Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
describe('ngAdd generator', () => { describe('ngAdd generator', () => {

View File

@ -1,5 +1,5 @@
import { getProjects, Tree } from '@nrwl/devkit'; import { getProjects, Tree } from '@nrwl/devkit';
import { MigrationProjectConfiguration, WorkspaceProjects } from './types'; import type { MigrationProjectConfiguration, WorkspaceProjects } from './types';
export function getAllProjects(tree: Tree): WorkspaceProjects { export function getAllProjects(tree: Tree): WorkspaceProjects {
const projects = getProjects(tree); const projects = getProjects(tree);

View File

@ -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';

View File

@ -1,12 +1,12 @@
import type { Tree } from '@nrwl/devkit';
import { import {
detectWorkspaceScope, detectWorkspaceScope,
joinPathFragments, joinPathFragments,
names, names,
readJson, readJson,
Tree,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { GeneratorOptions } from '../schema'; import type { GeneratorOptions } from '../schema';
import { WorkspaceProjects } from './types'; import type { WorkspaceProjects } from './types';
export function normalizeOptions( export function normalizeOptions(
tree: Tree, tree: Tree,

View File

@ -1,4 +1,4 @@
import { ProjectConfiguration } from '@nrwl/devkit'; import type { ProjectConfiguration } from '@nrwl/devkit';
export type MigrationProjectConfiguration = { export type MigrationProjectConfiguration = {
config: ProjectConfiguration; config: ProjectConfiguration;
@ -10,6 +10,14 @@ export type WorkspaceProjects = {
libs: MigrationProjectConfiguration[]; libs: MigrationProjectConfiguration[];
}; };
export type ProjectMigrationInfo = {
name: string;
oldRoot: string;
oldSourceRoot: string;
newRoot: string;
newSourceRoot: string;
};
export type WorkspaceCapabilities = { export type WorkspaceCapabilities = {
karma: boolean; karma: boolean;
eslint: boolean; eslint: boolean;

View File

@ -1,5 +1,5 @@
import * as chalk from 'chalk'; import * as chalk from 'chalk';
import { ProjectMigrator } from './project.migrator'; import type { ProjectMigrator } from '../migrators';
import { validateProjects } from './validate-projects'; import { validateProjects } from './validate-projects';
describe('validateProjects', () => { describe('validateProjects', () => {

View File

@ -1,5 +1,6 @@
// TODO(leo): this should probably move into a workspace-wide migrator
import * as chalk from 'chalk'; import * as chalk from 'chalk';
import { ProjectMigrator } from './project.migrator'; import type { ProjectMigrator } from '../migrators';
import { ValidationError } from './types'; import { ValidationError } from './types';
import { workspaceMigrationErrorHeading } from './validation-logging'; import { workspaceMigrationErrorHeading } from './validation-logging';

View File

@ -1,10 +1,9 @@
import type { NxJsonConfiguration, Tree } from '@nrwl/devkit';
import { import {
generateFiles, generateFiles,
joinPathFragments, joinPathFragments,
NxJsonConfiguration,
readJson, readJson,
readWorkspaceConfiguration, readWorkspaceConfiguration,
Tree,
updateJson, updateJson,
updateWorkspaceConfiguration, updateWorkspaceConfiguration,
writeJson, writeJson,
@ -19,8 +18,8 @@ import { readFileSync } from 'fs';
import { readModulePackageJson } from 'nx/src/utils/package-json'; import { readModulePackageJson } from 'nx/src/utils/package-json';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { angularDevkitVersion, nxVersion } from '../../../utils/versions'; import { angularDevkitVersion, nxVersion } from '../../../utils/versions';
import { GeneratorOptions } from '../schema'; import type { GeneratorOptions } from '../schema';
import { WorkspaceCapabilities, WorkspaceProjects } from './types'; import type { WorkspaceCapabilities, WorkspaceProjects } from './types';
import { workspaceMigrationErrorHeading } from './validation-logging'; import { workspaceMigrationErrorHeading } from './validation-logging';
export function validateWorkspace(tree: Tree): void { export function validateWorkspace(tree: Tree): void {