import type { NxJsonConfiguration, Tree } from '@nrwl/devkit'; import { generateFiles, getProjects, joinPathFragments, readJson, readNxJson, updateJson, updateNxJson, writeJson, } from '@nrwl/devkit'; import { Linter, lintInitGenerator } from '@nrwl/linter'; import { getRootTsConfigPathInTree, initGenerator as jsInitGenerator, } from '@nrwl/js'; import { deduceDefaultBase } from 'nx/src/utils/default-base'; import { prettierVersion } from '@nrwl/js/src/utils/versions'; import { toNewFormat } from 'nx/src/adapter/angular-json'; import { angularDevkitVersion, nxVersion } from '../../../utils/versions'; import type { ProjectMigrator } from '../migrators'; import type { GeneratorOptions } from '../schema'; import type { WorkspaceRootFileTypesInfo } from './types'; export function validateWorkspace(tree: Tree): void { const errors: string[] = []; if (!tree.exists('package.json')) { errors.push('The "package.json" file could not be found.'); } if (!tree.exists('angular.json')) { errors.push('The "angular.json" file could not be found.'); } if (!errors.length) { return; } throw new Error(`The workspace cannot be migrated because of the following issues: - ${errors.join('\n ')}`); } export function createNxJson( tree: Tree, options: GeneratorOptions, defaultProject: string | undefined ): void { const { npmScope } = options; const targets = getWorkspaceCommonTargets(tree); writeJson(tree, 'nx.json', { ...(npmScope ? { npmScope } : {}), affected: { defaultBase: options.defaultBase ?? deduceDefaultBase(), }, tasksRunnerOptions: { default: { runner: 'nx/tasks-runners/default', options: { cacheableOperations: [ 'build', targets.test ? 'test' : undefined, targets.lint ? 'lint' : undefined, targets.e2e ? 'e2e' : undefined, ].filter(Boolean), }, }, }, namedInputs: { sharedGlobals: [], default: ['{projectRoot}/**/*', 'sharedGlobals'], production: [ 'default', ...(targets.test ? [ '!{projectRoot}/tsconfig.spec.json', '!{projectRoot}/**/*.spec.[jt]s', '!{projectRoot}/karma.conf.js', ] : []), targets.lint ? '!{projectRoot}/.eslintrc.json' : undefined, ].filter(Boolean), }, targetDefaults: { build: { dependsOn: ['^build'], inputs: ['production', '^production'], }, test: targets.test ? { inputs: ['default', '^production', '{workspaceRoot}/karma.conf.js'], } : undefined, lint: targets.lint ? { inputs: ['default', '{workspaceRoot}/.eslintrc.json'], } : undefined, e2e: targets.e2e ? { inputs: ['default', '^production'], } : undefined, }, defaultProject, }); } function getWorkspaceCommonTargets(tree: Tree): { e2e: boolean; lint: boolean; test: boolean; } { const targets = { e2e: false, lint: false, test: false }; const projects = getProjects(tree); for (const [, project] of projects) { if (!targets.e2e && project.targets?.e2e) { targets.e2e = true; } if (!targets.lint && project.targets?.lint) { targets.lint = true; } if (!targets.test && project.targets?.test) { targets.test = true; } if (targets.e2e && targets.lint && targets.test) { return targets; } } return targets; } export function updateWorkspaceConfigDefaults(tree: Tree): void { const nxJson = readNxJson(tree); delete (nxJson as any).newProjectRoot; if (nxJson.cli) { delete (nxJson as any).defaultCollection; } updateNxJson(tree, nxJson); } export function updateRootTsConfig(tree: Tree): void { const tsconfig = readJson(tree, getRootTsConfigPathInTree(tree)); tsconfig.compilerOptions.paths ??= {}; tsconfig.compilerOptions.baseUrl = '.'; tsconfig.compilerOptions.rootDir = '.'; tsconfig.exclude = Array.from( new Set([...(tsconfig.exclude ?? []), 'node_modules', 'tmp']) ); writeJson(tree, 'tsconfig.base.json', tsconfig); if (tree.exists('tsconfig.json')) { tree.delete('tsconfig.json'); } } export function updatePackageJson(tree: Tree): void { updateJson(tree, 'package.json', (packageJson) => { packageJson.scripts = packageJson.scripts ?? {}; Object.keys(packageJson.scripts).forEach((script) => { packageJson.scripts[script] = packageJson.scripts[script] .replace(/^ng /, 'nx ') .replace(/ ng /, ' nx '); }); packageJson.devDependencies = packageJson.devDependencies ?? {}; packageJson.dependencies = packageJson.dependencies ?? {}; if (!packageJson.devDependencies['@angular/cli']) { packageJson.devDependencies['@angular/cli'] = angularDevkitVersion; } if (!packageJson.devDependencies['@nrwl/workspace']) { packageJson.devDependencies['@nrwl/workspace'] = nxVersion; } if (!packageJson.devDependencies['nx']) { packageJson.devDependencies['nx'] = nxVersion; } if (!packageJson.devDependencies['prettier']) { packageJson.devDependencies['prettier'] = prettierVersion; } return packageJson; }); } export function updateRootEsLintConfig( tree: Tree, existingEsLintConfig: any | undefined, unitTestRunner?: string ): void { if (tree.exists('.eslintrc.json')) { /** * If it still exists it means that there was no project at the root of the * workspace, so it was not moved. In that case, we remove the file so the * init generator do its work. We still receive the content of the file, * so we update it after the init generator has run. */ tree.delete('.eslintrc.json'); } lintInitGenerator(tree, { linter: Linter.EsLint, unitTestRunner }); if (!existingEsLintConfig) { // There was no eslint config in the root, so we keep the generated one as-is. return; } existingEsLintConfig.ignorePatterns = ['**/*']; existingEsLintConfig.plugins = Array.from( new Set([...(existingEsLintConfig.plugins ?? []), '@nrwl/nx']) ); existingEsLintConfig.overrides?.forEach((override) => { if (!override.parserOptions?.project) { return; } delete override.parserOptions.project; }); // add the @nrwl/nx/enforce-module-boundaries rule existingEsLintConfig.overrides = [ ...(existingEsLintConfig.overrides ?? []), { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], rules: { '@nrwl/nx/enforce-module-boundaries': [ 'error', { enforceBuildableLibDependency: true, allow: [], depConstraints: [ { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }, ], }, ], }, }, ]; writeJson(tree, '.eslintrc.json', existingEsLintConfig); } export function cleanupEsLintPackages(tree: Tree): void { updateJson(tree, 'package.json', (json) => { if (json.devDependencies?.['@angular-eslint/builder']) { delete json.devDependencies['@angular-eslint/builder']; } if (json.dependencies?.['@angular-eslint/builder']) { delete json.dependencies['@angular-eslint/builder']; } if (json.devDependencies?.['@angular-eslint/schematics']) { delete json.devDependencies['@angular-eslint/schematics']; } if (json.dependencies?.['@angular-eslint/schematics']) { delete json.dependencies['@angular-eslint/schematics']; } return json; }); } export async function createWorkspaceFiles(tree: Tree): Promise { updateVsCodeRecommendedExtensions(tree); await jsInitGenerator(tree, {}); generateFiles(tree, joinPathFragments(__dirname, '../files/root'), '.', { tmpl: '', dot: '.', rootTsConfigPath: getRootTsConfigPathInTree(tree), }); } export function createRootKarmaConfig(tree: Tree): void { generateFiles( tree, joinPathFragments(__dirname, '../files/root-karma'), '.', { tmpl: '', } ); } export function getWorkspaceRootFileTypesInfo( tree: Tree, migrators: ProjectMigrator[] ): WorkspaceRootFileTypesInfo { const workspaceRootFileTypesInfo: WorkspaceRootFileTypesInfo = { eslint: false, karma: false, }; if (tree.exists('.eslintrc.json')) { workspaceRootFileTypesInfo.eslint = true; } if (tree.exists('karma.conf.js')) { workspaceRootFileTypesInfo.karma = true; } if (workspaceRootFileTypesInfo.eslint && workspaceRootFileTypesInfo.karma) { return workspaceRootFileTypesInfo; } for (const migrator of migrators) { const projectInfo = migrator.getWorkspaceRootFileTypesInfo(); workspaceRootFileTypesInfo.eslint = workspaceRootFileTypesInfo.eslint || projectInfo.eslint; workspaceRootFileTypesInfo.karma = workspaceRootFileTypesInfo.karma || projectInfo.karma; if (workspaceRootFileTypesInfo.eslint && workspaceRootFileTypesInfo.karma) { return workspaceRootFileTypesInfo; } } return workspaceRootFileTypesInfo; } export function updateVsCodeRecommendedExtensions(tree: Tree): void { const recommendations = [ 'nrwl.angular-console', 'angular.ng-template', 'dbaeumer.vscode-eslint', 'esbenp.prettier-vscode', ]; if (tree.exists('.vscode/extensions.json')) { updateJson( tree, '.vscode/extensions.json', (json: { recommendations?: string[] }) => { json.recommendations = json.recommendations || []; recommendations.forEach((extension) => { if (!json.recommendations.includes(extension)) { json.recommendations.push(extension); } }); return json; } ); } else { writeJson(tree, '.vscode/extensions.json', { recommendations, }); } } export function deleteAngularJson(tree: Tree): void { const projects = toNewFormat(readJson(tree, 'angular.json')).projects; if (!Object.keys(projects).length) { tree.delete('angular.json'); } } export function deleteGitKeepFilesIfNotNeeded(tree: Tree): void { if (tree.children('apps').length > 1 && tree.exists('apps/.gitkeep')) { tree.delete('apps/.gitkeep'); } if (tree.children('libs').length > 1 && tree.exists('libs/.gitkeep')) { tree.delete('libs/.gitkeep'); } }