From e1a2c9855624348661794a0eb6686cd2c21ff0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Tue, 7 Mar 2023 23:08:10 +0000 Subject: [PATCH] cleanup(core): inline angular cli nested project migration logic into nx init (#15396) --- CODEOWNERS | 1 + e2e/nx-init/src/nx-init-angular.test.ts | 65 +++ e2e/utils/create-project-utils.ts | 5 +- .../src/add-nx-to-monorepo.ts | 5 +- packages/nx/src/command-line/init.ts | 12 +- packages/nx/src/nx-init/add-nx-to-nest.ts | 5 +- packages/nx/src/nx-init/add-nx-to-npm-repo.ts | 4 +- packages/nx/src/nx-init/angular/index.ts | 388 ++++++++++++++++++ .../angular/legacy-angular-versions.ts | 133 ++++++ packages/nx/src/nx-init/utils.ts | 53 ++- 10 files changed, 644 insertions(+), 27 deletions(-) create mode 100644 e2e/nx-init/src/nx-init-angular.test.ts create mode 100644 packages/nx/src/nx-init/angular/index.ts create mode 100644 packages/nx/src/nx-init/angular/legacy-angular-versions.ts diff --git a/CODEOWNERS b/CODEOWNERS index f810be9e13ddf..ae04a8949d425 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -121,6 +121,7 @@ /packages/nx/src/adapter @AgentEnder @leosvelperez /packages/nx/src/native @vsavkin @FrozenPandaz @Cammisuli /packages/nx/src/lock-file @meeroslav @FrozenPandaz +/packages/nx/src/nx-init/angular/** @Coly010 @leosvelperez @FrozenPandaz /e2e/nx*/** @FrozenPandaz @AgentEnder @vsavkin /packages/workspace/** @FrozenPandaz @AgentEnder @vsavkin /e2e/workspace-create/** @FrozenPandaz @AgentEnder @vsavkin diff --git a/e2e/nx-init/src/nx-init-angular.test.ts b/e2e/nx-init/src/nx-init-angular.test.ts new file mode 100644 index 0000000000000..e7257a74ba68f --- /dev/null +++ b/e2e/nx-init/src/nx-init-angular.test.ts @@ -0,0 +1,65 @@ +import { PackageManager } from 'nx/src/utils/package-manager'; +import { + checkFilesDoNotExist, + checkFilesExist, + cleanupProject, + getPackageManagerCommand, + getPublishedVersion, + getSelectedPackageManager, + runCLI, + runCommand, + runNgNew, + uniq, +} from '../../utils'; + +describe('nx init (Angular CLI)', () => { + let project: string; + let packageManager: PackageManager; + let pmc: ReturnType; + + beforeEach(() => { + project = uniq('proj'); + packageManager = getSelectedPackageManager(); + // TODO: solve issues with pnpm and remove this fallback + packageManager = packageManager === 'pnpm' ? 'yarn' : packageManager; + pmc = getPackageManagerCommand({ packageManager }); + }); + + afterEach(() => { + cleanupProject(); + }); + + it('should successfully convert an Angular CLI workspace to an Nx workspace', () => { + runNgNew(project, packageManager); + + const output = runCommand( + `${pmc.runUninstalledPackage} nx@${getPublishedVersion()} init -y` + ); + + expect(output).toContain('Nx is now enabled in your workspace!'); + // angular.json should have been deleted + checkFilesDoNotExist('angular.json'); + // check nx config files exist + checkFilesExist('nx.json', 'project.json'); + + // check build + const coldBuildOutput = runCLI(`build ${project} --outputHashing none`); + expect(coldBuildOutput).toContain( + `> nx run ${project}:build:production --outputHashing none` + ); + expect(coldBuildOutput).toContain( + `Successfully ran target build for project ${project}` + ); + checkFilesExist(`dist/${project}/main.js`); + + // run build again to check is coming from cache + const cachedBuildOutput = runCLI(`build ${project} --outputHashing none`); + expect(cachedBuildOutput).toContain( + `> nx run ${project}:build:production --outputHashing none [local cache]` + ); + expect(cachedBuildOutput).toContain('Nx read the output from the cache'); + expect(cachedBuildOutput).toContain( + `Successfully ran target build for project ${project}` + ); + }); +}); diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index efdeacca63970..50e5e942f0c71 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -15,7 +15,7 @@ import { } from './get-env-info'; import * as isCI from 'is-ci'; -import { angularCliVersion } from '@nrwl/workspace/src/utils/versions'; +import { angularCliVersion as defaultAngularCliVersion } from '@nrwl/workspace/src/utils/versions'; import { dump } from '@zkochan/js-yaml'; import { execSync, ExecSyncOptions } from 'child_process'; @@ -247,7 +247,8 @@ export function packageInstall( export function runNgNew( projectName: string, - packageManager = getSelectedPackageManager() + packageManager = getSelectedPackageManager(), + angularCliVersion = defaultAngularCliVersion ): string { projName = projectName; diff --git a/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts b/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts index a2c450edf0078..f61ee9c682bf2 100644 --- a/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts +++ b/packages/add-nx-to-monorepo/src/add-nx-to-monorepo.ts @@ -101,8 +101,7 @@ async function addNxToMonorepo() { repoRoot, targetDefaults, cacheableOperations, - scriptOutputs, - undefined + scriptOutputs ); addDepsToPackageJson(repoRoot, useCloud); @@ -111,7 +110,7 @@ async function addNxToMonorepo() { runInstall(repoRoot); if (useCloud) { - initCloud(repoRoot); + initCloud(repoRoot, 'nx-init-monorepo'); } printFinalMessage(); diff --git a/packages/nx/src/command-line/init.ts b/packages/nx/src/command-line/init.ts index b36a09a36d677..dc802e1d28a26 100644 --- a/packages/nx/src/command-line/init.ts +++ b/packages/nx/src/command-line/init.ts @@ -1,12 +1,13 @@ import { execSync } from 'child_process'; import { existsSync } from 'fs'; +import { prerelease } from 'semver'; +import * as parser from 'yargs-parser'; import { addNxToNest } from '../nx-init/add-nx-to-nest'; import { addNxToNpmRepo } from '../nx-init/add-nx-to-npm-repo'; +import { addNxToAngularCliRepo } from '../nx-init/angular'; +import { generateEncapsulatedNxSetup } from '../nx-init/encapsulated/add-nx-scripts'; import { directoryExists, readJsonFile } from '../utils/fileutils'; import { PackageJson } from '../utils/package-json'; -import * as parser from 'yargs-parser'; -import { generateEncapsulatedNxSetup } from '../nx-init/encapsulated/add-nx-scripts'; -import { prerelease } from 'semver'; export async function initHandler() { const args = process.argv.slice(2).join(' '); @@ -43,10 +44,7 @@ export async function initHandler() { } else if (existsSync('package.json')) { const packageJson: PackageJson = readJsonFile('package.json'); if (existsSync('angular.json')) { - // TODO(leo): remove make-angular-cli-faster - execSync(`npx --yes make-angular-cli-faster@${version} ${args}`, { - stdio: [0, 1, 2], - }); + await addNxToAngularCliRepo(); } else if (isCRA(packageJson)) { // TODO(jack): remove cra-to-nx execSync(`npx --yes cra-to-nx@${version} ${args}`, { diff --git a/packages/nx/src/nx-init/add-nx-to-nest.ts b/packages/nx/src/nx-init/add-nx-to-nest.ts index 13a6f494834a5..7bab293e63eed 100644 --- a/packages/nx/src/nx-init/add-nx-to-nest.ts +++ b/packages/nx/src/nx-init/add-nx-to-nest.ts @@ -108,8 +108,7 @@ export async function addNxToNest(packageJson: PackageJson) { repoRoot, [], [...cacheableOperations, ...nestCacheableScripts], - {}, - packageJson.name + {} ); const pmc = getPackageManagerCommand(); @@ -136,7 +135,7 @@ export async function addNxToNest(packageJson: PackageJson) { runInstall(repoRoot); if (useCloud) { - initCloud(repoRoot); + initCloud(repoRoot, 'nx-init-nest'); } printFinalMessage(); diff --git a/packages/nx/src/nx-init/add-nx-to-npm-repo.ts b/packages/nx/src/nx-init/add-nx-to-npm-repo.ts index 3b251ce6e99b8..52f6e73fbb0b4 100644 --- a/packages/nx/src/nx-init/add-nx-to-npm-repo.ts +++ b/packages/nx/src/nx-init/add-nx-to-npm-repo.ts @@ -76,7 +76,7 @@ export async function addNxToNpmRepo() { useCloud = false; } - createNxJsonFile(repoRoot, [], cacheableOperations, {}, packageJson.name); + createNxJsonFile(repoRoot, [], cacheableOperations, {}); const pmc = getPackageManagerCommand(); @@ -93,7 +93,7 @@ export async function addNxToNpmRepo() { runInstall(repoRoot, pmc); if (useCloud) { - initCloud(repoRoot); + initCloud(repoRoot, 'nx-init-npm-repo'); } printFinalMessage(); diff --git a/packages/nx/src/nx-init/angular/index.ts b/packages/nx/src/nx-init/angular/index.ts new file mode 100644 index 0000000000000..4d2ed9b1b2958 --- /dev/null +++ b/packages/nx/src/nx-init/angular/index.ts @@ -0,0 +1,388 @@ +import { prompt } from 'enquirer'; +import { unlinkSync } from 'fs'; +import { dirname, join, relative, resolve } from 'path'; +import { toNewFormat } from '../../adapter/angular-json'; +import { NxJsonConfiguration } from '../../config/nx-json'; +import { + ProjectConfiguration, + TargetConfiguration, +} from '../../config/workspace-json-project-json'; +import { fileExists, readJsonFile, writeJsonFile } from '../../utils/fileutils'; +import { sortObjectByKeys } from '../../utils/object-sort'; +import { output } from '../../utils/output'; +import { PackageJson } from '../../utils/package-json'; +import { normalizePath } from '../../utils/path'; +import { + addDepsToPackageJson, + addVsCodeRecommendedExtensions, + askAboutNxCloud, + createNxJsonFile, + initCloud, + runInstall, +} from '../utils'; +import { getLegacyMigrationFunctionIfApplicable } from './legacy-angular-versions'; +import yargsParser = require('yargs-parser'); + +const defaultCacheableOperations: string[] = [ + 'build', + 'server', + 'test', + 'lint', +]; +const parsedArgs = yargsParser(process.argv, { + boolean: ['yes'], + string: ['cacheable'], // only used for testing + alias: { + yes: ['y'], + }, +}); + +let repoRoot: string; +let workspaceTargets: string[]; + +export async function addNxToAngularCliRepo() { + repoRoot = process.cwd(); + + output.log({ title: '🧐 Checking versions compatibility' }); + const legacyMigrationFn = await getLegacyMigrationFunctionIfApplicable( + repoRoot, + parsedArgs.yes !== true + ); + if (legacyMigrationFn) { + output.log({ title: '💽 Running migration for a legacy Angular version' }); + await legacyMigrationFn(); + process.exit(0); + } + + output.success({ + title: + '✅ The Angular version is compatible with the latest version of Nx!', + }); + + output.log({ title: '🐳 Nx initialization' }); + const cacheableOperations = await collectCacheableOperations(); + const useNxCloud = parsedArgs.yes !== true ? await askAboutNxCloud() : false; + + output.log({ title: '📦 Installing dependencies' }); + installDependencies(useNxCloud); + + output.log({ title: '📝 Setting up workspace' }); + await setupWorkspace(cacheableOperations); + + if (useNxCloud) { + output.log({ title: '🛠️ Setting up Nx Cloud' }); + initCloud(repoRoot, 'nx-init-angular'); + } + + output.success({ + title: '🎉 Nx is now enabled in your workspace!', + bodyLines: [ + `Execute 'npx nx build' twice to see the computation caching in action.`, + 'Learn more about the changes done to your workspace at https://nx.dev/recipes/adopting-nx/migration-angular.', + ], + }); +} + +async function collectCacheableOperations(): Promise { + let cacheableOperations: string[]; + + workspaceTargets = getWorkspaceTargets(); + const defaultCacheableTargetsInWorkspace = defaultCacheableOperations.filter( + (t) => workspaceTargets.includes(t) + ); + + if (parsedArgs.yes !== true) { + output.log({ + title: `🧑‍🔧 Please answer the following questions about the targets found in your angular.json in order to generate task runner configuration`, + }); + + cacheableOperations = ( + (await prompt([ + { + type: 'multiselect', + name: 'cacheableOperations', + initial: defaultCacheableTargetsInWorkspace as any, + message: + 'Which of the following targets are cacheable? (Produce the same output given the same input, e.g. build, test and lint usually are, serve and start are not)', + // enquirer mutates the array below, create a new one to avoid it + choices: [...workspaceTargets], + }, + ])) as any + ).cacheableOperations; + } else { + cacheableOperations = parsedArgs.cacheable + ? parsedArgs.cacheable.split(',') + : defaultCacheableTargetsInWorkspace; + } + + return cacheableOperations; +} + +function installDependencies(useNxCloud: boolean): void { + addDepsToPackageJson(repoRoot, useNxCloud); + addPluginDependencies(); + runInstall(repoRoot); +} + +function addPluginDependencies(): void { + const packageJsonPath = join(repoRoot, 'package.json'); + const packageJson = readJsonFile(packageJsonPath); + const nxVersion = require('../../../package.json').version; + + packageJson.devDependencies ??= {}; + packageJson.devDependencies['@nrwl/angular'] = nxVersion; + packageJson.devDependencies['@nrwl/workspace'] = nxVersion; + + const peerDepsToInstall = [ + '@angular-devkit/core', + '@angular-devkit/schematics', + '@schematics/angular', + ]; + const angularCliVersion = + packageJson.devDependencies['@angular/cli'] ?? + packageJson.dependencies?.['@angular/cli'] ?? + packageJson.devDependencies['@angular-devkit/build-angular'] ?? + packageJson.dependencies?.['@angular-devkit/build-angular']; + + for (const dep of peerDepsToInstall) { + if (!packageJson.devDependencies[dep] && !packageJson.dependencies?.[dep]) { + packageJson.devDependencies[dep] = angularCliVersion; + } + } + + packageJson.devDependencies = sortObjectByKeys(packageJson.devDependencies); + + writeJsonFile(packageJsonPath, packageJson); +} + +type AngularJsonConfigTargetConfiguration = Exclude< + TargetConfiguration, + 'command' | 'executor' | 'outputs' | 'dependsOn' | 'inputs' +> & { + builder: string; +}; +type AngularJsonProjectConfiguration = { + root: string; + sourceRoot: string; + architect?: Record; +}; +interface AngularJsonConfig { + projects: Record; + defaultProject?: string; +} + +async function setupWorkspace(cacheableOperations: string[]): Promise { + const angularJsonPath = join(repoRoot, 'angular.json'); + const angularJson = readJsonFile(angularJsonPath); + const workspaceCapabilities = getWorkspaceCapabilities(angularJson.projects); + createNxJson(angularJson, cacheableOperations, workspaceCapabilities); + addVsCodeRecommendedExtensions( + repoRoot, + [ + 'nrwl.angular-console', + 'angular.ng-template', + workspaceCapabilities.eslintProjectConfigFile + ? 'dbaeumer.vscode-eslint' + : undefined, + ].filter(Boolean) + ); + replaceNgWithNxInPackageJsonScripts(); + + // convert workspace config format to standalone project configs + // update its targets outputs and delete angular.json + const projects = toNewFormat(angularJson).projects; + for (const [projectName, project] of Object.entries(projects)) { + updateProjectOutputs(project); + writeJsonFile(join(project.root, 'project.json'), { + $schema: normalizePath( + relative( + join(repoRoot, project.root), + join(repoRoot, 'node_modules/nx/schemas/project-schema.json') + ) + ), + name: projectName, + ...project, + root: undefined, + }); + } + unlinkSync(angularJsonPath); +} + +type WorkspaceCapabilities = { + eslintProjectConfigFile: boolean; + test: boolean; + karmaProjectConfigFile: boolean; +}; + +function createNxJson( + angularJson: AngularJsonConfig, + cacheableOperations: string[], + { + eslintProjectConfigFile, + test, + karmaProjectConfigFile, + }: WorkspaceCapabilities +): void { + createNxJsonFile(repoRoot, [], cacheableOperations, {}); + + const nxJson = readJsonFile(join(repoRoot, 'nx.json')); + nxJson.namedInputs = { + sharedGlobals: [], + default: ['{projectRoot}/**/*', 'sharedGlobals'], + production: [ + 'default', + ...(test + ? [ + '!{projectRoot}/tsconfig.spec.json', + '!{projectRoot}/**/*.spec.[jt]s', + karmaProjectConfigFile ? '!{projectRoot}/karma.conf.js' : undefined, + ].filter(Boolean) + : []), + eslintProjectConfigFile ? '!{projectRoot}/.eslintrc.json' : undefined, + ].filter(Boolean), + }; + nxJson.targetDefaults = {}; + if (workspaceTargets.includes('build')) { + nxJson.targetDefaults.build = { + dependsOn: ['^build'], + inputs: ['production', '^production'], + }; + } + if (workspaceTargets.includes('server')) { + nxJson.targetDefaults.server = { inputs: ['production', '^production'] }; + } + if (workspaceTargets.includes('test')) { + const inputs = ['default', '^production']; + if (fileExists(join(repoRoot, 'karma.conf.js'))) { + inputs.push('{workspaceRoot}/karma.conf.js'); + } + nxJson.targetDefaults.test = { inputs }; + } + if (workspaceTargets.includes('lint')) { + const inputs = ['default']; + if (fileExists(join(repoRoot, '.eslintrc.json'))) { + inputs.push('{workspaceRoot}/.eslintrc.json'); + } + nxJson.targetDefaults.lint = { inputs }; + } + if (workspaceTargets.includes('e2e')) { + nxJson.targetDefaults.e2e = { inputs: ['default', '^production'] }; + } + // Angular 14 workspaces support defaultProject, keep it until we drop support + nxJson.defaultProject = angularJson.defaultProject; + writeJsonFile(join(repoRoot, 'nx.json'), nxJson); +} + +function updateProjectOutputs(project: ProjectConfiguration): void { + Object.values(project.targets ?? {}).forEach((target) => { + if ( + [ + '@angular-devkit/build-angular:browser', + '@angular-builders/custom-webpack:browser', + 'ngx-build-plus:browser', + '@angular-devkit/build-angular:server', + '@angular-builders/custom-webpack:server', + 'ngx-build-plus:server', + ].includes(target.executor) + ) { + target.outputs = ['{options.outputPath}']; + } else if (target.executor === '@angular-eslint/builder:lint') { + target.outputs = ['{options.outputFile}']; + } else if (target.executor === '@angular-devkit/build-angular:ng-packagr') { + try { + const ngPackageJsonPath = join(repoRoot, target.options.project); + const ngPackageJson = readJsonFile(ngPackageJsonPath); + const outputPath = relative( + repoRoot, + resolve(dirname(ngPackageJsonPath), ngPackageJson.dest) + ); + target.outputs = [`{workspaceRoot}/${normalizePath(outputPath)}`]; + } catch {} + } + }); +} + +function getWorkspaceTargets(): string[] { + const { projects } = readJsonFile( + join(repoRoot, 'angular.json') + ); + const targets = new Set(); + for (const project of Object.values(projects ?? {})) { + for (const target of Object.keys(project.architect ?? {})) { + targets.add(target); + } + } + + return Array.from(targets); +} + +function getWorkspaceCapabilities( + projects: Record +): WorkspaceCapabilities { + const capabilities = { + eslintProjectConfigFile: false, + test: false, + karmaProjectConfigFile: false, + }; + + for (const project of Object.values(projects)) { + if ( + !capabilities.eslintProjectConfigFile && + projectHasEslintConfig(project) + ) { + capabilities.eslintProjectConfigFile = true; + } + if (!capabilities.test && projectUsesKarmaBuilder(project)) { + capabilities.test = true; + } + if ( + !capabilities.karmaProjectConfigFile && + projectHasKarmaConfig(project) + ) { + capabilities.karmaProjectConfigFile = true; + } + + if ( + capabilities.eslintProjectConfigFile && + capabilities.test && + capabilities.karmaProjectConfigFile + ) { + return capabilities; + } + } + + return capabilities; +} + +function projectUsesKarmaBuilder( + project: AngularJsonProjectConfiguration +): boolean { + return Object.values(project.architect ?? {}).some( + (target) => target.builder === '@angular-devkit/build-angular:karma' + ); +} + +function projectHasKarmaConfig( + project: AngularJsonProjectConfiguration +): boolean { + return fileExists(join(project.root, 'karma.conf.js')); +} + +function projectHasEslintConfig( + project: AngularJsonProjectConfiguration +): boolean { + return fileExists(join(project.root, '.eslintrc.json')); +} + +function replaceNgWithNxInPackageJsonScripts(): void { + const packageJsonPath = join(repoRoot, 'package.json'); + const packageJson = readJsonFile(packageJsonPath); + packageJson.scripts ??= {}; + Object.keys(packageJson.scripts).forEach((script) => { + packageJson.scripts[script] = packageJson.scripts[script] + .replace(/^nx$/, 'nx') + .replace(/^ng /, 'nx ') + .replace(/ ng /g, ' nx '); + }); + writeJsonFile(packageJsonPath, packageJson); +} diff --git a/packages/nx/src/nx-init/angular/legacy-angular-versions.ts b/packages/nx/src/nx-init/angular/legacy-angular-versions.ts new file mode 100644 index 0000000000000..ea982a2a5e908 --- /dev/null +++ b/packages/nx/src/nx-init/angular/legacy-angular-versions.ts @@ -0,0 +1,133 @@ +import { execSync } from 'child_process'; +import { join } from 'path'; +import { gte, major } from 'semver'; +import { readJsonFile, writeJsonFile } from '../../utils/fileutils'; +import { sortObjectByKeys } from '../../utils/object-sort'; +import { output } from '../../utils/output'; +import { readModulePackageJson } from '../../utils/package-json'; +import { + PackageManagerCommands, + getPackageManagerCommand, + resolvePackageVersionUsingInstallation, + resolvePackageVersionUsingRegistry, +} from '../../utils/package-manager'; +import { askAboutNxCloud, initCloud } from '../utils'; + +// map of Angular major versions to Nx versions to use for legacy `nx init` migrations, +// key is major Angular version and value is Nx version to use +const nxAngularLegacyVersionMap: Record = {}; +// min major angular version supported in latest Nx +const minMajorAngularVersionSupported = 14; +// version when the Nx CLI changed from @nrwl/tao & @nrwl/cli to nx +const versionWithConsolidatedPackages = '13.9.0'; + +export async function getLegacyMigrationFunctionIfApplicable( + repoRoot: string, + interactive: boolean +): Promise<() => Promise | null> { + const angularVersion = + readModulePackageJson('@angular/core').packageJson.version; + const majorAngularVersion = major(angularVersion); + if (majorAngularVersion > minMajorAngularVersionSupported) { + // non-legacy + return null; + } + + let legacyMigrationCommand: string; + let pkgName: string; + let pkgVersion: string; + if (majorAngularVersion < 13) { + // for versions lower than 13, the migration was in @nrwl/workspace:ng-add + pkgName = '@nrwl/workspace'; + pkgVersion = await resolvePackageVersion( + pkgName, + `^${majorAngularVersion}.0.0` + ); + legacyMigrationCommand = `ng g ${pkgName}:ng-add --preserveAngularCLILayout`; + } else if (majorAngularVersion < 14) { + // for v13, the migration was in @nrwl/angular:ng-add + pkgName = '@nrwl/angular'; + pkgVersion = await resolvePackageVersion(pkgName, '~14.1.0'); + legacyMigrationCommand = `ng g ${pkgName}:ng-add --preserve-angular-cli-layout`; + } else { + // use the latest Nx version that supported the Angular version + legacyMigrationCommand = `nx@${ + nxAngularLegacyVersionMap[majorAngularVersion] + } init ${process.argv.slice(2).join(' ')}`; + } + + return async () => { + output.log({ title: '🐳 Nx initialization' }); + const useNxCloud = interactive ? await askAboutNxCloud() : false; + + output.log({ title: '📦 Installing dependencies' }); + const pmc = getPackageManagerCommand(); + await installDependencies(repoRoot, pkgName, pkgVersion, useNxCloud, pmc); + + output.log({ title: '📝 Setting up workspace' }); + execSync(`${pmc.exec} ${legacyMigrationCommand}`, { stdio: [0, 1, 2] }); + + if (useNxCloud) { + output.log({ title: '🛠️ Setting up Nx Cloud' }); + initCloud(repoRoot, 'nx-init-angular'); + } + + output.success({ + title: '🎉 Nx is now enabled in your workspace!', + bodyLines: [ + `Execute 'npx nx build' twice to see the computation caching in action.`, + 'Learn more about the changes done to your workspace at https://nx.dev/recipes/adopting-nx/migration-angular.', + ], + }); + }; +} + +async function installDependencies( + repoRoot: string, + pkgName: string, + pkgVersion: string, + useNxCloud: boolean, + pmc: PackageManagerCommands +): Promise { + const json = readJsonFile(join(repoRoot, 'package.json')); + + json.devDependencies ??= {}; + json.devDependencies['@nrwl/workspace'] = pkgVersion; + + if (gte(pkgVersion, versionWithConsolidatedPackages)) { + json.devDependencies['nx'] = pkgVersion; + } else { + json.devDependencies['@nrwl/cli'] = pkgVersion; + json.devDependencies['@nrwl/tao'] = pkgVersion; + } + + if (useNxCloud) { + // get the latest @nrwl/nx-cloud version compatible with the Nx major + // version being installed + json.devDependencies['@nrwl/nx-cloud'] = await resolvePackageVersion( + '@nrwl/nx-cloud', + `^${major(pkgVersion)}.0.0` + ); + } + json.devDependencies = sortObjectByKeys(json.devDependencies); + + if (pkgName === '@nrwl/angular') { + json.dependencies ??= {}; + json.dependencies['@nrwl/angular'] = pkgVersion; + json.dependencies = sortObjectByKeys(json.dependencies); + } + writeJsonFile(`package.json`, json); + + execSync(pmc.install, { stdio: [0, 1, 2] }); +} + +async function resolvePackageVersion( + packageName: string, + version: string +): Promise { + try { + return await resolvePackageVersionUsingRegistry(packageName, version); + } catch { + return await resolvePackageVersionUsingInstallation(packageName, version); + } +} diff --git a/packages/nx/src/nx-init/utils.ts b/packages/nx/src/nx-init/utils.ts index 47e99168dc44a..95cd24f322509 100644 --- a/packages/nx/src/nx-init/utils.ts +++ b/packages/nx/src/nx-init/utils.ts @@ -1,12 +1,13 @@ -import { joinPathFragments } from '../utils/path'; -import { readJsonFile, writeJsonFile } from '../utils/fileutils'; -import * as enquirer from 'enquirer'; import { execSync } from 'child_process'; +import * as enquirer from 'enquirer'; +import { join } from 'path'; +import { fileExists, readJsonFile, writeJsonFile } from '../utils/fileutils'; import { getPackageManagerCommand, PackageManagerCommands, } from '../utils/package-manager'; import { runNxSync } from '../utils/child-process'; +import { joinPathFragments } from '../utils/path'; export function askAboutNxCloud() { return enquirer @@ -35,8 +36,7 @@ export function createNxJsonFile( repoRoot: string, targetDefaults: string[], cacheableOperations: string[], - scriptOutputs: { [name: string]: string }, - defaultProject: string | undefined + scriptOutputs: { [name: string]: string } ) { const nxJsonPath = joinPathFragments(repoRoot, 'nx.json'); let nxJson = {} as any; @@ -121,9 +121,42 @@ export function runInstall( execSync(pmc.install, { stdio: [0, 1, 2], cwd: repoRoot }); } -export function initCloud(repoRoot: string) { - runNxSync(`g @nrwl/nx-cloud:init --installationSource=add-nx-to-monorepo`, { - stdio: [0, 1, 2], - cwd: repoRoot, - }); +export function initCloud( + repoRoot: string, + installationSource: + | 'nx-init-angular' + | 'nx-init-cra' + | 'nx-init-monorepo' + | 'nx-init-nest' + | 'nx-init-npm-repo' +) { + runNxSync( + `g @nrwl/nx-cloud:init --installationSource=${installationSource}`, + { + stdio: [0, 1, 2], + cwd: repoRoot, + } + ); +} + +export function addVsCodeRecommendedExtensions( + repoRoot: string, + extensions: string[] +): void { + const vsCodeExtensionsPath = join(repoRoot, '.vscode/extensions.json'); + + if (fileExists(vsCodeExtensionsPath)) { + const vsCodeExtensionsJson = readJsonFile(vsCodeExtensionsPath); + + vsCodeExtensionsJson.recommendations ??= []; + extensions.forEach((extension) => { + if (!vsCodeExtensionsJson.recommendations.includes(extension)) { + vsCodeExtensionsJson.recommendations.push(extension); + } + }); + + writeJsonFile(vsCodeExtensionsPath, vsCodeExtensionsJson); + } else { + writeJsonFile(vsCodeExtensionsPath, { recommendations: extensions }); + } }