cleanup(angular): add @angular-devkit/build-angular:karma builder migrator (#13101)

This commit is contained in:
Leosvel Pérez Espinosa 2022-11-10 12:28:06 +01:00 committed by GitHub
parent 8bba6d7abf
commit 02c4bf2af8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 349 additions and 327 deletions

View File

@ -19,7 +19,7 @@ import {
createWorkspaceFiles,
decorateAngularCli,
getAllProjects,
getWorkspaceCapabilities,
getWorkspaceRootFileTypesInfo,
normalizeOptions,
updatePackageJson,
updateRootEsLintConfig,
@ -59,7 +59,10 @@ export async function migrateFromAngularCli(
// validate all projects
validateProjects(migrators);
const workspaceCapabilities = getWorkspaceCapabilities(tree, projects);
const workspaceRootFileTypesInfo = getWorkspaceRootFileTypesInfo(
tree,
migrators
);
/**
* Keep a copy of the root eslint config to restore it later. We need to
@ -68,7 +71,7 @@ export async function migrateFromAngularCli(
* the app migration.
*/
let eslintConfig =
workspaceCapabilities.eslint && tree.exists('.eslintrc.json')
workspaceRootFileTypesInfo.eslint && tree.exists('.eslintrc.json')
? readJson(tree, '.eslintrc.json')
: undefined;
@ -95,10 +98,10 @@ export async function migrateFromAngularCli(
* these files in the root for the root application, so we wait until
* those root config files are moved when the projects are migrated.
*/
if (workspaceCapabilities.karma) {
if (workspaceRootFileTypesInfo.karma) {
createRootKarmaConfig(tree);
}
if (workspaceCapabilities.eslint) {
if (workspaceRootFileTypesInfo.eslint) {
updateRootEsLintConfig(tree, eslintConfig, options.unitTestRunner);
cleanupEsLintPackages(tree);
}

View File

@ -0,0 +1,145 @@
import {
joinPathFragments,
ProjectConfiguration,
TargetConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import { offsetFromRoot } from '@nrwl/devkit';
import { getRootTsConfigPathInTree } from '@nrwl/workspace/src/utilities/typescript';
import { basename } from 'path';
import type {
Logger,
ProjectMigrationInfo,
ValidationError,
ValidationResult,
} from '../../utilities';
import { arrayToString } from '../../utilities';
import { BuilderMigrator } from './builder.migrator';
export class AngularDevkitKarmaMigrator extends BuilderMigrator {
constructor(
tree: Tree,
project: ProjectMigrationInfo,
projectConfig: ProjectConfiguration,
logger: Logger
) {
super(
tree,
'@angular-devkit/build-angular:karma',
'karma',
project,
projectConfig,
logger
);
}
override migrate(): void {
for (const [name, target] of this.targets) {
this.moveFilePathsFromTargetToProjectRoot(target, [
'karmaConfig',
'tsConfig',
'webWorkerTsConfig',
]);
this.updateTargetConfiguration(name, target);
this.updateTsConfigFileUsedByTestTarget(name, target);
this.updateCacheableOperations([name]);
}
if (!this.targets.size && this.projectConfig.root === '') {
// there could still be a karma.conf.js file in the root
// so move to new location
const karmaConfig = 'karma.conf.js';
if (this.tree.exists(karmaConfig)) {
this.logger.info(
'No "test" target was found, but a root Karma config file was found in the project root. The file will be moved to the new location.'
);
this.moveProjectRootFile(karmaConfig);
}
}
}
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 {
if (!target.options) {
this.logger.warn(
`The target "${targetName}" is not specifying any options. Skipping updating the target configuration.`
);
return;
}
target.options.main =
target.options.main && this.convertAsset(target.options.main);
target.options.polyfills =
target.options.polyfills && this.convertAsset(target.options.polyfills);
target.options.tsConfig =
target.options.tsConfig &&
joinPathFragments(
this.project.newRoot,
basename(target.options.tsConfig)
);
target.options.karmaConfig =
target.options.karmaConfig &&
joinPathFragments(
this.project.newRoot,
basename(target.options.karmaConfig)
);
target.options.assets =
target.options.assets &&
target.options.assets.map((asset) => this.convertAsset(asset));
target.options.styles =
target.options.styles &&
target.options.styles.map((style) => this.convertAsset(style));
target.options.scripts =
target.options.scripts &&
target.options.scripts.map((script) => this.convertAsset(script));
updateProjectConfiguration(this.tree, this.project.name, {
...this.projectConfig,
});
}
private updateTsConfigFileUsedByTestTarget(
targetName: string,
target: TargetConfiguration
): void {
if (!target.options?.tsConfig) {
this.logger.warn(
`The "${targetName}" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.`
);
return;
}
if (!this.tree.exists(target.options.tsConfig)) {
const originalTsConfigPath =
this.originalProjectConfig.targets[targetName].options.tsConfig;
this.logger.warn(
`The tsConfig file "${originalTsConfigPath}" specified in the "${targetName}" target could not be found. Skipping updating the tsConfig file.`
);
return;
}
this.updateTsConfigFile(
target.options.tsConfig,
getRootTsConfigPathInTree(this.tree),
offsetFromRoot(this.projectConfig.root)
);
}
}

View File

@ -31,6 +31,7 @@ export class AngularDevkitNgPackagrMigrator extends BuilderMigrator {
super(
tree,
'@angular-devkit/build-angular:ng-packagr',
undefined,
project,
projectConfig,
logger

View File

@ -3,7 +3,11 @@ import type {
TargetConfiguration,
Tree,
} from '@nrwl/devkit';
import type { Logger, ProjectMigrationInfo } from '../../utilities';
import type {
Logger,
ProjectMigrationInfo,
WorkspaceRootFileType,
} from '../../utilities';
import { Migrator } from '../migrator';
export abstract class BuilderMigrator extends Migrator {
@ -12,6 +16,7 @@ export abstract class BuilderMigrator extends Migrator {
constructor(
tree: Tree,
public readonly builderName: string,
public readonly rootFileType: WorkspaceRootFileType | undefined,
project: ProjectMigrationInfo,
projectConfig: ProjectConfiguration,
logger: Logger
@ -24,6 +29,10 @@ export abstract class BuilderMigrator extends Migrator {
this.collectBuilderTargets();
}
isBuilderUsed(): boolean {
return this.targets.size > 0;
}
protected collectBuilderTargets(): void {
for (const [name, target] of Object.entries(
this.projectConfig.targets ?? {}

View File

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

View File

@ -1,9 +1,15 @@
import type { ProjectConfiguration, Tree } from '@nrwl/devkit';
import type {
ProjectConfiguration,
TargetConfiguration,
Tree,
} from '@nrwl/devkit';
import {
joinPathFragments,
readWorkspaceConfiguration,
updateJson,
updateWorkspaceConfiguration,
} from '@nrwl/devkit';
import { basename } from 'path';
import type { Logger } from '../utilities/logger';
import type {
ProjectMigrationInfo,
@ -27,6 +33,52 @@ export abstract class Migrator {
abstract migrate(): Promise<void> | void;
abstract validate(): ValidationResult;
protected convertAsset(asset: string | any): string | any {
if (typeof asset === 'string') {
return this.convertSourceRootPath(asset);
} else {
return { ...asset, input: this.convertSourceRootPath(asset.input) };
}
}
protected moveFile(from: string, to: string, required: boolean = true): void {
if (!this.tree.exists(from)) {
if (required) {
this.logger.warn(`The path "${from}" does not exist. Skipping.`);
}
} else if (this.tree.exists(to)) {
if (required) {
this.logger.warn(`The path "${to}" already exists. Skipping.`);
}
} else {
const contents = this.tree.read(from);
this.tree.write(to, contents);
this.tree.delete(from);
}
}
protected moveFilePathsFromTargetToProjectRoot(
target: TargetConfiguration,
options: string[]
) {
options.forEach((option) => {
this.getTargetValuesForOption(target, option).forEach((path) => {
this.moveProjectRootFile(path);
});
});
}
protected moveProjectRootFile(filePath: string, isRequired = true): void {
if (!filePath) {
return;
}
const filename = !!filePath ? basename(filePath) : '';
const from = filePath;
const to = joinPathFragments(this.project.newRoot, filename);
this.moveFile(from, to, isRequired);
}
// TODO(leo): This should be moved to BuilderMigrator once everything is split into builder migrators.
protected updateCacheableOperations(targetNames: string[]): void {
if (!targetNames.length) {
@ -63,4 +115,54 @@ export abstract class Migrator {
return json;
});
}
private convertSourceRootPath(originalPath: string): string {
return originalPath?.startsWith(this.project.oldSourceRoot)
? joinPathFragments(
this.project.newSourceRoot,
originalPath.replace(this.project.oldSourceRoot, '')
)
: originalPath;
}
private getTargetValuesForOption(
target: TargetConfiguration,
optionPath: string
): any[] {
const values = new Set();
const value = this.getValueForOption(target.options, optionPath);
if (value) {
values.add(value);
}
for (const configuration of Object.values(target.configurations ?? {})) {
const value = this.getValueForOption(configuration, optionPath);
if (value) {
values.add(value);
}
}
return Array.from(values);
}
private getValueForOption(
options: Record<string, any> | undefined,
optionPath: string
): any {
if (!options) {
return null;
}
const segments = optionPath.split('.');
let value = options;
for (const segment of segments) {
if (value && value[segment]) {
value = value[segment];
} else {
return null;
}
}
return value;
}
}

View File

@ -1577,9 +1577,9 @@ describe('app migrator', () => {
'lint',
'test',
'e2e',
'myCustomTest',
'myCustomBuild',
'myCustomLint',
'myCustomTest',
]);
});

View File

@ -17,6 +17,8 @@ import type {
Target,
ValidationResult,
} from '../../utilities';
import type { BuilderMigratorClassType } from '../builders';
import { AngularDevkitKarmaMigrator } from '../builders';
import { E2eMigrator } from './e2e.migrator';
import { ProjectMigrator } from './project.migrator';
@ -28,8 +30,7 @@ type SupportedTargets =
| 'prerender'
| 'serve'
| 'server'
| 'serveSsr'
| 'test';
| 'serveSsr';
const supportedTargets: Record<SupportedTargets, Target> = {
build: { builders: ['@angular-devkit/build-angular:browser'] },
e2e: {
@ -45,9 +46,13 @@ const supportedTargets: Record<SupportedTargets, Target> = {
serve: { builders: ['@angular-devkit/build-angular:dev-server'] },
server: { builders: ['@angular-devkit/build-angular:server'] },
serveSsr: { builders: ['@nguniversal/builders:ssr-dev-server'] },
test: { builders: ['@angular-devkit/build-angular:karma'] },
};
// TODO(leo): this will replace `supportedTargets` once the full refactor is done.
const supportedBuilderMigrators: BuilderMigratorClassType[] = [
AngularDevkitKarmaMigrator,
];
export class AppMigrator extends ProjectMigrator<SupportedTargets> {
private e2eMigrator: E2eMigrator;
private newEsLintConfigPath: string;
@ -59,7 +64,15 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
project: MigrationProjectConfiguration,
logger?: Logger
) {
super(tree, options, supportedTargets, project, 'apps', logger);
super(
tree,
options,
supportedTargets,
project,
'apps',
logger,
supportedBuilderMigrators
);
this.e2eMigrator = new E2eMigrator(
tree,
@ -82,25 +95,33 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
this.moveProjectFiles();
await this.updateProjectConfiguration();
for (const builderMigrator of this.builderMigrators ?? []) {
await builderMigrator.migrate();
}
this.updateTsConfigs();
this.updateEsLintConfig();
this.updateCacheableOperations(
[
this.targetNames.build,
this.targetNames.lint,
this.targetNames.test,
this.targetNames.e2e,
].filter(Boolean)
);
}
override validate(): ValidationResult {
const result: ValidationResult = [
const errors: ValidationResult = [
...(super.validate() ?? []),
...(this.e2eMigrator.validate() ?? []),
];
return result.length ? result : null;
for (const builderMigrator of this.builderMigrators) {
errors.push(...(builderMigrator.validate() ?? []));
}
return errors.length ? errors : null;
}
private moveProjectFiles(): void {
@ -123,23 +144,6 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
);
}
if (this.targetNames.test) {
this.moveFilePathsFromTargetToProjectRoot(
this.projectConfig.targets[this.targetNames.test],
['karmaConfig', 'tsConfig', 'webWorkerTsConfig']
);
} else {
// there could still be a karma.conf.js file in the root
// so move to new location
const karmaConfig = 'karma.conf.js';
if (this.tree.exists(karmaConfig)) {
this.logger.info(
'No "test" target was found, but a root Karma config file was found in the project root. The file will be moved to the new location.'
);
this.moveProjectRootFile(karmaConfig);
}
}
if (this.targetNames.server) {
this.moveFilePathsFromTargetToProjectRoot(
this.projectConfig.targets[this.targetNames.server],
@ -178,7 +182,6 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
} else {
this.updateBuildTargetConfiguration();
this.updateLintTargetConfiguration();
this.updateTestTargetConfiguration();
this.updateServerTargetConfiguration();
this.updatePrerenderTargetConfiguration();
this.updateServeSsrTargetConfiguration();
@ -202,10 +205,6 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
rootTsConfigFile,
projectOffsetFromRoot
);
this.updateTsConfigFileUsedByTestTarget(
rootTsConfigFile,
projectOffsetFromRoot
);
this.updateTsConfigFileUsedByServerTarget(projectOffsetFromRoot);
}
@ -303,17 +302,6 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
}));
}
private moveFilePathsFromTargetToProjectRoot(
target: TargetConfiguration,
options: string[]
) {
options.forEach((option) => {
this.getTargetValuesForOption(target, option).forEach((path) => {
this.moveProjectRootFile(path);
});
});
}
private updateBuildTargetConfiguration(): void {
if (!this.targetNames.build) {
this.logger.warn(
@ -521,56 +509,6 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
});
}
private updateTestTargetConfiguration(): void {
if (!this.targetNames.test) {
return;
}
const testOptions =
this.projectConfig.targets[this.targetNames.test].options;
if (!testOptions) {
this.logger.warn(
`The target "${this.targetNames.test}" is not specifying any options. Skipping updating the target configuration.`
);
return;
}
if (!testOptions.tsConfig) {
this.logger.warn(
`The "${this.targetNames.test}" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.`
);
} else {
const newTestTsConfig = this.convertPath(testOptions.tsConfig);
if (!this.tree.exists(newTestTsConfig)) {
this.logger.warn(
`The tsConfig file "${testOptions.tsConfig}" specified in the "${this.targetNames.test}" target could not be found. Skipping updating the tsConfig file.`
);
}
}
testOptions.main = testOptions.main && this.convertAsset(testOptions.main);
testOptions.polyfills =
testOptions.polyfills && this.convertAsset(testOptions.polyfills);
testOptions.tsConfig =
testOptions.tsConfig &&
joinPathFragments(this.project.newRoot, basename(testOptions.tsConfig));
testOptions.karmaConfig =
testOptions.karmaConfig &&
joinPathFragments(
this.project.newRoot,
basename(testOptions.karmaConfig)
);
testOptions.assets =
testOptions.assets &&
testOptions.assets.map((asset) => this.convertAsset(asset));
testOptions.styles =
testOptions.styles &&
testOptions.styles.map((style) => this.convertAsset(style));
testOptions.scripts =
testOptions.scripts &&
testOptions.scripts.map((script) => this.convertAsset(script));
}
private updateTsConfigFileUsedByBuildTarget(
rootTsConfigFile: string,
projectOffsetFromRoot: string
@ -619,22 +557,4 @@ export class AppMigrator extends ProjectMigrator<SupportedTargets> {
return json;
});
}
private updateTsConfigFileUsedByTestTarget(
rootTsConfigFile: string,
projectOffsetFromRoot: string
): void {
if (!this.targetNames.test) {
return;
}
const tsConfig =
this.projectConfig.targets[this.targetNames.test].options?.tsConfig;
if (!tsConfig || !this.tree.exists(tsConfig)) {
// we already logged a warning for these cases, so just return
return;
}
this.updateTsConfigFile(tsConfig, rootTsConfigFile, projectOffsetFromRoot);
}
}

View File

@ -129,7 +129,7 @@ describe('lib migrator', () => {
'The "build" target is using an unsupported builder "@not/supported:builder".',
]);
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\\"."`
`"The supported builders for libraries are: \\"@angular-eslint/builder:lint\\", \\"@angular-devkit/build-angular:ng-packagr\\" and \\"@angular-devkit/build-angular:karma\\"."`
);
});
@ -152,7 +152,7 @@ describe('lib migrator', () => {
'The "test" target is using an unsupported builder "@other/not-supported:builder".',
]);
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\\"."`
`"The supported builders for libraries are: \\"@angular-eslint/builder:lint\\", \\"@angular-devkit/build-angular:ng-packagr\\" and \\"@angular-devkit/build-angular:karma\\"."`
);
});
@ -171,7 +171,7 @@ describe('lib migrator', () => {
'The "my-build" target is using an unsupported builder "@not/supported:builder".',
]);
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\\"."`
`"The supported builders for libraries are: \\"@angular-eslint/builder:lint\\", \\"@angular-devkit/build-angular:ng-packagr\\" and \\"@angular-devkit/build-angular:karma\\"."`
);
});
@ -1284,8 +1284,8 @@ describe('lib migrator', () => {
'test',
'e2e',
'myCustomBuild',
'myCustomLint',
'myCustomTest',
'myCustomLint',
]);
});

View File

@ -8,8 +8,6 @@ import {
} 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 type { GeneratorOptions } from '../../schema';
import type {
Logger,
@ -19,17 +17,20 @@ import type {
ValidationResult,
} from '../../utilities';
import type { BuilderMigratorClassType } from '../builders';
import { AngularDevkitNgPackagrMigrator } from '../builders';
import {
AngularDevkitKarmaMigrator,
AngularDevkitNgPackagrMigrator,
} from '../builders';
import { ProjectMigrator } from './project.migrator';
type SupportedTargets = 'test' | 'lint';
type SupportedTargets = 'lint';
const supportedTargets: Record<SupportedTargets, Target> = {
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,
AngularDevkitKarmaMigrator,
];
export class LibMigrator extends ProjectMigrator<SupportedTargets> {
@ -69,11 +70,8 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
await builderMigrator.migrate();
}
this.updateTsConfigs();
this.updateEsLintConfig();
this.updateCacheableOperations(
[this.targetNames.lint, this.targetNames.test].filter(Boolean)
);
this.updateCacheableOperations([this.targetNames.lint].filter(Boolean));
}
override validate(): ValidationResult {
@ -103,7 +101,6 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
);
} else {
this.updateLintTargetConfiguration();
this.updateTestTargetConfiguration();
}
updateProjectConfiguration(this.tree, this.project.name, {
@ -116,16 +113,6 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
});
}
private updateTsConfigs(): void {
const rootTsConfigFile = getRootTsConfigPathInTree(this.tree);
const projectOffsetFromRoot = offsetFromRoot(this.projectConfig.root);
this.updateTsConfigFileUsedByTestTarget(
rootTsConfigFile,
projectOffsetFromRoot
);
}
private updateEsLintConfig(): void {
if (!this.targetNames.lint || !this.tree.exists(this.newEsLintConfigPath)) {
return;
@ -232,73 +219,4 @@ export class LibMigrator extends ProjectMigrator<SupportedTargets> {
lintOptions.hasTypeAwareRules = true;
}
}
private updateTestTargetConfiguration(): void {
if (!this.targetNames.test) {
return;
}
const testOptions =
this.projectConfig.targets[this.targetNames.test].options;
if (!testOptions) {
this.logger.warn(
`The target "${this.targetNames.test}" is not specifying any options. Skipping updating the target configuration.`
);
return;
}
if (!testOptions.tsConfig) {
this.logger.warn(
`The "${this.targetNames.test}" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.`
);
} else if (!this.tree.exists(testOptions.tsConfig)) {
this.logger.warn(
`The tsConfig file "${testOptions.tsConfig}" specified in the "${this.targetNames.test}" target could not be found. Skipping updating the tsConfig file.`
);
}
testOptions.main = testOptions.main && this.convertAsset(testOptions.main);
testOptions.polyfills =
testOptions.polyfills && this.convertAsset(testOptions.polyfills);
testOptions.tsConfig =
testOptions.tsConfig &&
joinPathFragments(this.project.newRoot, basename(testOptions.tsConfig));
testOptions.karmaConfig =
testOptions.karmaConfig &&
joinPathFragments(
this.project.newRoot,
basename(testOptions.karmaConfig)
);
testOptions.assets =
testOptions.assets &&
testOptions.assets.map((asset) => this.convertAsset(asset));
testOptions.styles =
testOptions.styles &&
testOptions.styles.map((style) => this.convertAsset(style));
testOptions.scripts =
testOptions.scripts &&
testOptions.scripts.map((script) => this.convertAsset(script));
}
private updateTsConfigFileUsedByTestTarget(
rootTsConfigFile: string,
projectOffsetFromRoot: string
): void {
if (!this.targetNames.test) {
return;
}
const tsConfig =
this.projectConfig.targets[this.targetNames.test].options?.tsConfig;
if (!tsConfig || !this.tree.exists(tsConfig)) {
// we already logged a warning for these cases, so just return
return;
}
this.updateTsConfigFile(
this.projectConfig.targets[this.targetNames.test].options.tsConfig,
rootTsConfigFile,
projectOffsetFromRoot
);
}
}

View File

@ -1,17 +1,18 @@
import type { TargetConfiguration, Tree } from '@nrwl/devkit';
import type { Tree } from '@nrwl/devkit';
import {
joinPathFragments,
normalizePath,
offsetFromRoot,
visitNotIgnoredFiles,
} from '@nrwl/devkit';
import { basename, dirname } from 'path';
import { dirname } from 'path';
import type { GeneratorOptions } from '../../schema';
import type {
MigrationProjectConfiguration,
Target,
ValidationError,
ValidationResult,
WorkspaceRootFileTypesInfo,
} from '../../utilities';
import { arrayToString, Logger } from '../../utilities';
import type { BuilderMigratorClassType } from '../builders';
@ -55,6 +56,20 @@ export abstract class ProjectMigrator<
this.createBuilderMigrators(supportedBuilderMigrators);
}
getWorkspaceRootFileTypesInfo(): WorkspaceRootFileTypesInfo {
const workspaceRootFileTypesInfo: WorkspaceRootFileTypesInfo = {
eslint:
Boolean(this.projectConfig.targets?.lint) ||
this.tree.exists(`${this.projectConfig.root}/.eslintrc.json`),
karma: this.builderMigrators.some(
(migrator) =>
migrator.rootFileType === 'karma' && migrator.isBuilderUsed()
),
};
return workspaceRootFileTypesInfo;
}
override validate(): ValidationResult {
const errors: ValidationError[] = [];
@ -170,14 +185,6 @@ export abstract class ProjectMigrator<
return errors.length ? errors : null;
}
protected convertAsset(asset: string | any): string | any {
if (typeof asset === 'string') {
return this.convertSourceRootPath(asset);
} else {
return { ...asset, input: this.convertSourceRootPath(asset.input) };
}
}
protected convertEsLintConfigExtendToNewPath(
eslintConfigPath: string,
extendPath: string
@ -196,15 +203,6 @@ export abstract class ProjectMigrator<
);
}
protected convertSourceRootPath(originalPath: string): string {
return originalPath?.startsWith(this.project.oldSourceRoot)
? joinPathFragments(
this.project.newSourceRoot,
originalPath.replace(this.project.oldSourceRoot, '')
)
: originalPath;
}
protected convertRootPath(originalPath: string): string {
return originalPath?.startsWith(this.project.oldRoot)
? joinPathFragments(
@ -232,80 +230,12 @@ export abstract class ProjectMigrator<
return originalPath;
}
protected getTargetValuesForOption(
target: TargetConfiguration,
optionPath: string
): any[] {
const values = new Set();
const value = this.getValueForOption(target.options, optionPath);
if (value) {
values.add(value);
}
for (const configuration of Object.values(target.configurations ?? {})) {
const value = this.getValueForOption(configuration, optionPath);
if (value) {
values.add(value);
}
}
return Array.from(values);
}
protected getValueForOption(
options: Record<string, any> | undefined,
optionPath: string
): any {
if (!options) {
return null;
}
const segments = optionPath.split('.');
let value = options;
for (const segment of segments) {
if (value && value[segment]) {
value = value[segment];
} else {
return null;
}
}
return value;
}
protected moveProjectRootFile(filePath: string, isRequired = true): void {
if (!filePath) {
return;
}
const filename = !!filePath ? basename(filePath) : '';
const from = filePath;
const to = joinPathFragments(this.project.newRoot, filename);
this.moveFile(from, to, isRequired);
}
protected moveDir(from: string, to: string): void {
visitNotIgnoredFiles(this.tree, from, (file) => {
this.moveFile(file, normalizePath(file).replace(from, to), true);
});
}
protected moveFile(from: string, to: string, required: boolean = true): void {
if (!this.tree.exists(from)) {
if (required) {
this.logger.warn(`The path "${from}" does not exist. Skipping.`);
}
} else if (this.tree.exists(to)) {
if (required) {
this.logger.warn(`The path "${to}" already exists. Skipping.`);
}
} else {
const contents = this.tree.read(from);
this.tree.write(to, contents);
this.tree.delete(from);
}
}
private collectTargetNames(): void {
const targetTypes = Object.keys(this.targets) as TargetType[];

View File

@ -18,10 +18,8 @@ export type ProjectMigrationInfo = {
newSourceRoot: string;
};
export type WorkspaceCapabilities = {
karma: boolean;
eslint: boolean;
};
export type WorkspaceRootFileType = 'karma' | 'eslint';
export type WorkspaceRootFileTypesInfo = Record<WorkspaceRootFileType, boolean>;
export type ValidationError = {
message?: string;

View File

@ -18,8 +18,9 @@ import { readFileSync } from 'fs';
import { readModulePackageJson } from 'nx/src/utils/package-json';
import { dirname, join } from 'path';
import { angularDevkitVersion, nxVersion } from '../../../utils/versions';
import type { ProjectMigrator } from '../migrators';
import type { GeneratorOptions } from '../schema';
import type { WorkspaceCapabilities, WorkspaceProjects } from './types';
import type { WorkspaceRootFileTypesInfo } from './types';
import { workspaceMigrationErrorHeading } from './validation-logging';
export function validateWorkspace(tree: Tree): void {
@ -257,45 +258,39 @@ export function createRootKarmaConfig(tree: Tree): void {
);
}
export function getWorkspaceCapabilities(
export function getWorkspaceRootFileTypesInfo(
tree: Tree,
projects: WorkspaceProjects
): WorkspaceCapabilities {
const capabilities: WorkspaceCapabilities = { eslint: false, karma: false };
migrators: ProjectMigrator[]
): WorkspaceRootFileTypesInfo {
const workspaceRootFileTypesInfo: WorkspaceRootFileTypesInfo = {
eslint: false,
karma: false,
};
if (tree.exists('.eslintrc.json')) {
capabilities.eslint = true;
workspaceRootFileTypesInfo.eslint = true;
}
if (tree.exists('karma.conf.js')) {
capabilities.karma = true;
workspaceRootFileTypesInfo.karma = true;
}
if (capabilities.eslint && capabilities.karma) {
return capabilities;
if (workspaceRootFileTypesInfo.eslint && workspaceRootFileTypesInfo.karma) {
return workspaceRootFileTypesInfo;
}
for (const project of [...projects.apps, ...projects.libs]) {
if (
!capabilities.eslint &&
(project.config.targets?.lint ||
tree.exists(`${project.config.root}/.eslintrc.json`))
) {
capabilities.eslint = true;
}
if (
!capabilities.karma &&
(project.config.targets?.test ||
tree.exists(`${project.config.root}/karma.conf.js`))
) {
capabilities.karma = true;
}
for (const migrator of migrators) {
const projectInfo = migrator.getWorkspaceRootFileTypesInfo();
workspaceRootFileTypesInfo.eslint =
workspaceRootFileTypesInfo.eslint || projectInfo.eslint;
workspaceRootFileTypesInfo.karma =
workspaceRootFileTypesInfo.karma || projectInfo.karma;
if (capabilities.eslint && capabilities.karma) {
return capabilities;
if (workspaceRootFileTypesInfo.eslint && workspaceRootFileTypesInfo.karma) {
return workspaceRootFileTypesInfo;
}
}
return capabilities;
return workspaceRootFileTypesInfo;
}
function updateVsCodeRecommendedExtensions(tree: Tree): void {