diff --git a/e2e/nx/src/import.test.ts b/e2e/nx/src/import.test.ts index 11327fccf87d0f..f843bc733c12db 100644 --- a/e2e/nx/src/import.test.ts +++ b/e2e/nx/src/import.test.ts @@ -4,6 +4,7 @@ import { getSelectedPackageManager, newProject, runCLI, + runCommand, updateJson, updateFile, e2eCwd, @@ -38,7 +39,11 @@ describe('Nx Import', () => { try { rmdirSync(join(tempImportE2ERoot)); } catch {} + + runCommand(`git add .`); + runCommand(`git commit -am "Update" --allow-empty`); }); + afterAll(() => cleanupProject()); it('should be able to import a vite app', () => { @@ -69,6 +74,9 @@ describe('Nx Import', () => { // This fails if git is already configured to have `main` branch, but that's OK } + //TODO(jack): remove this + console.log('>>>>', execSync('git status --porcelain').toString()); + const remote = tempViteProjectPath; const ref = 'main'; const source = '.'; @@ -111,7 +119,7 @@ describe('Nx Import', () => { }); mkdirSync(join(repoPath, 'packages/a'), { recursive: true }); writeFileSync(join(repoPath, 'packages/a/README.md'), `# A`); - execSync(`git add packages/a`, { + execSync(`git add *`, { cwd: repoPath, }); execSync(`git commit -m "add package a"`, { @@ -119,13 +127,16 @@ describe('Nx Import', () => { }); mkdirSync(join(repoPath, 'packages/b'), { recursive: true }); writeFileSync(join(repoPath, 'packages/b/README.md'), `# B`); - execSync(`git add packages/b`, { + execSync(`git add *`, { cwd: repoPath, }); execSync(`git commit -m "add package b"`, { cwd: repoPath, }); + //TODO(jack): remove this + console.log('>>>>', execSync('git status --porcelain').toString()); + runCLI( `import ${repoPath} packages/a --ref main --source packages/a --no-interactive`, { diff --git a/packages/angular/src/generators/init/init.ts b/packages/angular/src/generators/init/init.ts index c3351a339a57be..4cae47914c4736 100755 --- a/packages/angular/src/generators/init/init.ts +++ b/packages/angular/src/generators/init/init.ts @@ -1,13 +1,16 @@ import { addDependenciesToPackageJson, + createProjectGraphAsync, ensurePackage, formatFiles, + type GeneratorCallback, logger, readNxJson, - type GeneratorCallback, type Tree, } from '@nx/devkit'; +import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; import { getInstalledPackageVersion, versions } from '../utils/version-utils'; +import { createNodesV2 } from '../../plugins/plugin'; import { Schema } from './schema'; export async function angularInitGenerator( @@ -17,6 +20,25 @@ export async function angularInitGenerator( ignoreAngularCacheDirectory(tree); const installTask = installAngularDevkitCoreIfMissing(tree, options); + // For Angular inference plugin, we only want it during import since our + // generators do not use `angular.json`, and `nx init` should split + // `angular.json` into multiple `project.json` files -- as this is preferred + // by most folks we've talked to. + options.addPlugin ??= process.env.NX_RUNNING_NX_IMPORT === 'true'; + + if (options.addPlugin) { + await addPlugin( + tree, + await createProjectGraphAsync(), + '@nx/angular/plugin', + createNodesV2, + { + targetNamePrefix: ['', 'angular:', 'angular-'], + }, + options.updatePackageScripts + ); + } + if (!options.skipFormat) { await formatFiles(tree); } diff --git a/packages/angular/src/generators/init/schema.d.ts b/packages/angular/src/generators/init/schema.d.ts index 483cf1ed2eb48d..3db8f259f664db 100644 --- a/packages/angular/src/generators/init/schema.d.ts +++ b/packages/angular/src/generators/init/schema.d.ts @@ -3,4 +3,7 @@ export interface Schema { skipInstall?: boolean; skipPackageJson?: boolean; keepExistingVersions?: boolean; + /* internal */ + addPlugin?: boolean; + updatePackageScripts?: boolean; } diff --git a/packages/angular/src/plugins/plugin.spec.ts b/packages/angular/src/plugins/plugin.spec.ts index dbd1ff7bc3ea97..122b8bd668f3a5 100644 --- a/packages/angular/src/plugins/plugin.spec.ts +++ b/packages/angular/src/plugins/plugin.spec.ts @@ -96,7 +96,7 @@ describe('@nx/angular/plugin', () => { "targets": { "build": { "cache": true, - "command": "ng run my-app:build", + "command": "ng build", "dependsOn": [ "^build", ], @@ -131,7 +131,7 @@ describe('@nx/angular/plugin', () => { ], }, "serve": { - "command": "ng run my-app:serve", + "command": "ng serve", "configurations": { "production": { "command": "ng run my-app:serve:production", @@ -157,7 +157,7 @@ describe('@nx/angular/plugin', () => { }, "test": { "cache": true, - "command": "ng run my-app:test", + "command": "ng test", "inputs": [ "default", "^production", @@ -197,7 +197,7 @@ describe('@nx/angular/plugin', () => { "targets": { "build": { "cache": true, - "command": "ng run my-lib:build", + "command": "ng build", "dependsOn": [ "^build", ], @@ -233,7 +233,7 @@ describe('@nx/angular/plugin', () => { }, "test": { "cache": true, - "command": "ng run my-lib:test", + "command": "ng test", "inputs": [ "default", "^production", @@ -394,7 +394,7 @@ describe('@nx/angular/plugin', () => { "targets": { "build": { "cache": true, - "command": "ng run org1-app1:build", + "command": "ng build", "dependsOn": [ "^build", ], @@ -429,7 +429,7 @@ describe('@nx/angular/plugin', () => { ], }, "serve": { - "command": "ng run org1-app1:serve", + "command": "ng serve", "configurations": { "production": { "command": "ng run org1-app1:serve:production", @@ -455,7 +455,7 @@ describe('@nx/angular/plugin', () => { }, "test": { "cache": true, - "command": "ng run org1-app1:test", + "command": "ng test", "inputs": [ "default", "^production", @@ -495,7 +495,7 @@ describe('@nx/angular/plugin', () => { "targets": { "build": { "cache": true, - "command": "ng run org1-lib1:build", + "command": "ng build", "dependsOn": [ "^build", ], @@ -531,7 +531,7 @@ describe('@nx/angular/plugin', () => { }, "test": { "cache": true, - "command": "ng run org1-lib1:test", + "command": "ng test", "inputs": [ "default", "^production", @@ -578,7 +578,7 @@ describe('@nx/angular/plugin', () => { "targets": { "build": { "cache": true, - "command": "ng run org2-app1:build", + "command": "ng build", "dependsOn": [ "^build", ], @@ -613,7 +613,7 @@ describe('@nx/angular/plugin', () => { ], }, "serve": { - "command": "ng run org2-app1:serve", + "command": "ng serve", "configurations": { "production": { "command": "ng run org2-app1:serve:production", @@ -639,7 +639,7 @@ describe('@nx/angular/plugin', () => { }, "test": { "cache": true, - "command": "ng run org2-app1:test", + "command": "ng test", "inputs": [ "default", "^production", @@ -679,7 +679,7 @@ describe('@nx/angular/plugin', () => { "targets": { "build": { "cache": true, - "command": "ng run org2-lib1:build", + "command": "ng build", "dependsOn": [ "^build", ], @@ -715,7 +715,7 @@ describe('@nx/angular/plugin', () => { }, "test": { "cache": true, - "command": "ng run org2-lib1:test", + "command": "ng test", "inputs": [ "default", "^production", diff --git a/packages/angular/src/plugins/plugin.ts b/packages/angular/src/plugins/plugin.ts index 734b451329339f..662bf0e350f2e1 100644 --- a/packages/angular/src/plugins/plugin.ts +++ b/packages/angular/src/plugins/plugin.ts @@ -20,6 +20,10 @@ import * as posix from 'node:path/posix'; import { hashObject } from 'nx/src/devkit-internals'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; +export interface AngularPluginOptions { + targetNamePrefix?: string; +} + type AngularProjects = Record< string, Pick @@ -76,7 +80,7 @@ function writeProjectsToCache( writeJsonFile(cachePath, results); } -export const createNodesV2: CreateNodesV2<{}> = [ +export const createNodesV2: CreateNodesV2 = [ '**/angular.json', async (configFiles, options, context) => { const optionsHash = hashObject(options); @@ -124,6 +128,7 @@ async function createNodesInternal( projectsCache[hash] ??= await buildAngularProjects( configFilePath, + options, angularWorkspaceRoot, context ); @@ -133,6 +138,7 @@ async function createNodesInternal( async function buildAngularProjects( configFilePath: string, + options: AngularPluginOptions, angularWorkspaceRoot: string, context: CreateNodesContextV2 ): Promise { @@ -156,11 +162,25 @@ async function buildAngularProjects( const namedInputs = getNamedInputs(project.root, context); - for (const [targetName, angularTarget] of Object.entries(projectTargets)) { + for (const [_targetName, angularTarget] of Object.entries(projectTargets)) { + const targetName = options?.targetNamePrefix + ? `${options.targetNamePrefix}${_targetName}` + : _targetName; const externalDependencies = ['@angular/cli']; targets[targetName] = { - command: `ng run ${projectName}:${targetName}`, + command: + // For targets that are also Angular CLI commands, infer the simplified form. + // Otherwise, use `ng run` to support non-command targets so that they will run. + targetName === 'build' || + targetName === 'deploy' || + targetName === 'extract-i18n' || + targetName === 'e2e' || + targetName === 'lint' || + targetName === 'serve' || + targetName === 'test' + ? `ng ${targetName}` + : `ng run ${projectName}:${targetName}`, options: { cwd: angularWorkspaceRoot }, metadata: { technologies: ['angular'], diff --git a/packages/devkit/src/utils/add-plugin.ts b/packages/devkit/src/utils/add-plugin.ts index 19105b367ddf7d..f4dc4472372dd6 100644 --- a/packages/devkit/src/utils/add-plugin.ts +++ b/packages/devkit/src/utils/add-plugin.ts @@ -104,21 +104,64 @@ async function _addPluginInternal( let pluginOptions: PluginOptions; let projConfigs: ConfigurationResult; - const combinations = generateCombinations(options); - optionsLoop: for (const _pluginOptions of combinations) { - pluginOptions = _pluginOptions as PluginOptions; - nxJson.plugins ??= []; - if ( - nxJson.plugins.some((p) => - typeof p === 'string' - ? p === pluginName - : p.plugin === pluginName && !p.include - ) - ) { - // Plugin has already been added - return; + if (Object.keys(options).length > 0) { + const combinations = generateCombinations(options); + optionsLoop: for (const _pluginOptions of combinations) { + pluginOptions = _pluginOptions as PluginOptions; + + nxJson.plugins ??= []; + if ( + nxJson.plugins.some((p) => + typeof p === 'string' + ? p === pluginName + : p.plugin === pluginName && !p.include + ) + ) { + // Plugin has already been added + return; + } + global.NX_GRAPH_CREATION = true; + try { + projConfigs = await retrieveProjectConfigurations( + [pluginFactory(pluginOptions)], + tree.root, + nxJson + ); + } catch (e) { + // Errors are okay for this because we're only running 1 plugin + if (e instanceof ProjectConfigurationsError) { + projConfigs = e.partialProjectConfigurationsResult; + } else { + throw e; + } + } + global.NX_GRAPH_CREATION = false; + + for (const projConfig of Object.values(projConfigs.projects)) { + const node = graphNodes.find( + (node) => node.data.root === projConfig.root + ); + + if (!node) { + continue; + } + + for (const targetName in projConfig.targets) { + if (node.data.targets[targetName]) { + // Conflicting Target Name, check the next one + pluginOptions = null; + continue optionsLoop; + } + } + } + + break; } + } else { + // If the plugin does not take in options, we add the plugin with empty options. + nxJson.plugins ??= []; + pluginOptions = {} as unknown as PluginOptions; global.NX_GRAPH_CREATION = true; try { projConfigs = await retrieveProjectConfigurations( @@ -135,26 +178,6 @@ async function _addPluginInternal( } } global.NX_GRAPH_CREATION = false; - - for (const projConfig of Object.values(projConfigs.projects)) { - const node = graphNodes.find( - (node) => node.data.root === projConfig.root - ); - - if (!node) { - continue; - } - - for (const targetName in projConfig.targets) { - if (node.data.targets[targetName]) { - // Conflicting Target Name, check the next one - pluginOptions = null; - continue optionsLoop; - } - } - } - - break; } if (!pluginOptions) { diff --git a/packages/nx/src/command-line/import/import.ts b/packages/nx/src/command-line/import/import.ts index 03423cd728f199..a3551d7291fd48 100644 --- a/packages/nx/src/command-line/import/import.ts +++ b/packages/nx/src/command-line/import/import.ts @@ -59,7 +59,15 @@ export interface ImportOptions { } export async function importHandler(options: ImportOptions) { + process.env.NX_RUNNING_NX_IMPORT = 'true'; let { sourceRepository, ref, source, destination } = options; + const destinationGitClient = new GitRepository(process.cwd()); + + if (await destinationGitClient.hasUncommittedChanges()) { + throw new Error( + `You have uncommitted changes in the destination repository. Commit or revert the changes and try again.` + ); + } output.log({ title: @@ -186,7 +194,6 @@ export async function importHandler(options: ImportOptions) { const absDestination = join(process.cwd(), destination); - const destinationGitClient = new GitRepository(process.cwd()); await assertDestinationEmpty(destinationGitClient, absDestination); const tempImportBranch = getTempImportBranch(ref); @@ -259,7 +266,8 @@ export async function importHandler(options: ImportOptions) { const { plugins, updatePackageScripts } = await detectPlugins( nxJson, - options.interactive + options.interactive, + true ); if (packageManager !== sourcePackageManager) { diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index 457cdd99d44457..9db3dec07c7ce5 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -157,7 +157,7 @@ export async function initHandler(options: InitArgs): Promise { }); } -const npmPackageToPluginMap: Record = { +const npmPackageToPluginMap: Record = { // Generic JS tools eslint: '@nx/eslint', storybook: '@nx/storybook', @@ -181,7 +181,8 @@ const npmPackageToPluginMap: Record = { export async function detectPlugins( nxJson: NxJsonConfiguration, - interactive: boolean + interactive: boolean, + includeAngularCli?: boolean ): Promise<{ plugins: string[]; updatePackageScripts: boolean; @@ -214,7 +215,13 @@ export async function detectPlugins( ...packageJson.devDependencies, }; - for (const [dep, plugin] of Object.entries(npmPackageToPluginMap)) { + const _npmPackageToPluginMap = { + ...npmPackageToPluginMap, + }; + if (includeAngularCli) { + _npmPackageToPluginMap['@angular/cli'] = '@nx/angular'; + } + for (const [dep, plugin] of Object.entries(_npmPackageToPluginMap)) { if (deps[dep]) { detectedPlugins.add(plugin); } diff --git a/packages/nx/src/utils/git-utils.ts b/packages/nx/src/utils/git-utils.ts index ff16767809bea9..260a3e046ed6cc 100644 --- a/packages/nx/src/utils/git-utils.ts +++ b/packages/nx/src/utils/git-utils.ts @@ -45,6 +45,11 @@ export class GitRepository { .trim(); } + async hasUncommittedChanges() { + const data = await this.execAsync(`git status --porcelain`); + return data.trim() !== ''; + } + async addFetchRemote(remoteName: string, branch: string) { return await this.execAsync( `git config --add remote.${remoteName}.fetch "+refs/heads/${branch}:refs/remotes/${remoteName}/${branch}"`