diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index a4358f9dd1ca4..6209a08abebb7 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -10391,6 +10391,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "infer-targets", + "path": "/nx-api/workspace/generators/infer-targets", + "name": "infer-targets", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 5d1d45836fc5c..ef366a035478a 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -3487,6 +3487,15 @@ "originalFilePath": "/packages/workspace/src/generators/ci-workflow/schema.json", "path": "/nx-api/workspace/generators/ci-workflow", "type": "generator" + }, + "/nx-api/workspace/generators/infer-targets": { + "description": "Convert Nx projects to use inferred targets.", + "file": "generated/packages/workspace/generators/infer-targets.json", + "hidden": false, + "name": "infer-targets", + "originalFilePath": "/packages/workspace/src/generators/infer-targets/schema.json", + "path": "/nx-api/workspace/generators/infer-targets", + "type": "generator" } }, "path": "/nx-api/workspace" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index ce80b4d541742..06f3052829a5d 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -3451,6 +3451,15 @@ "originalFilePath": "/packages/workspace/src/generators/ci-workflow/schema.json", "path": "workspace/generators/ci-workflow", "type": "generator" + }, + { + "description": "Convert Nx projects to use inferred targets.", + "file": "generated/packages/workspace/generators/infer-targets.json", + "hidden": false, + "name": "infer-targets", + "originalFilePath": "/packages/workspace/src/generators/infer-targets/schema.json", + "path": "workspace/generators/infer-targets", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/workspace/generators/infer-targets.json b/docs/generated/packages/workspace/generators/infer-targets.json new file mode 100644 index 0000000000000..f5b67c97b1f16 --- /dev/null +++ b/docs/generated/packages/workspace/generators/infer-targets.json @@ -0,0 +1,35 @@ +{ + "name": "infer-targets", + "factory": "./src/generators/infer-targets/infer-targets", + "schema": { + "$schema": "https://json-schema.org/schema", + "$id": "InferTargets", + "title": "", + "type": "object", + "description": "Convert Nx projects to use inferred targets.", + "properties": { + "project": { + "type": "string", + "description": "The project to convert to use inferred targets.", + "x-priority": "important" + }, + "plugins": { + "type": "array", + "description": "The plugins used to infer targets. For example @nx/eslint or @nx/jest", + "items": { "type": "string" } + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files.", + "default": false + } + }, + "presets": [] + }, + "description": "Convert Nx projects to use inferred targets.", + "implementation": "/packages/workspace/src/generators/infer-targets/infer-targets.ts", + "aliases": [], + "hidden": false, + "path": "/packages/workspace/src/generators/infer-targets/schema.json", + "type": "generator" +} diff --git a/docs/shared/recipes/running-tasks/convert-to-inferred.md b/docs/shared/recipes/running-tasks/convert-to-inferred.md index 28e92882ff787..1acc78d6e1365 100644 --- a/docs/shared/recipes/running-tasks/convert-to-inferred.md +++ b/docs/shared/recipes/running-tasks/convert-to-inferred.md @@ -19,7 +19,17 @@ For the best experience, we recommend that you [migrate](/features/automate-upda npx nx migrate latest ``` -## Migrate a Plugin +## Migrate All Plugins + +You can use the `infer-targets` generator to quickly migrate all available plugins to use inferred tasks. See the sections below for more details on the individual plugins' migration processes. + +```shell +npx nx g infer-targets +``` + +The generator will automatically detect all available `convert-to-inferred` generators and run the ones you choose. If you only want to try it on a single project, pass the `--project` option. + +## Migrate a Single Plugin Most of the official plugins come with a `convert-to-inferred` generator. This generator will @@ -42,7 +52,7 @@ None of the above For third-party plugins that provide `convert-to-inferred` generators, you should pick the `None of the above` option and type in the name of the package manually. Alternatively, you can also provide the package explicitly with `nx g :convert-to-inferred`. {% /callout %} -We recommend that you migrate the plugins one at a time, and check that the configurations are correct before continuing to the next plugin. If you only want to try it on a single project, pass the `--project` option. +We recommend that you check that the configurations are correct before continuing to the next plugin. If you only want to try it on a single project, pass the `--project` option. ## Understand the Migration Process @@ -96,7 +106,7 @@ For example, if we migrated the `@nx/vite` plugin for a single app (i.e. `nx g @ You'll notice that the `serve` and `build` tasks are running the [Vite CLI](https://vitejs.dev/guide/cli.html) and there are no references to Nx executors. Since the targets directly invoke the Vite CLI, any options that may be passed to it can be passed via Nx commands. e.g. `nx serve demo --cors --port 8888` enables CORs and uses port `8888` using [Vite CLI options](https://vitejs.dev/guide/cli.html#options) The same CLI setup applies to other plugins as well. -- `@nx/cypess` calls the [Cypress CLI](https://docs.cypress.io/guides/guides/command-line) +- `@nx/cypress` calls the [Cypress CLI](https://docs.cypress.io/guides/guides/command-line) - `@nx/playwright` calls the [Playwright CLI](https://playwright.dev/docs/test-cli) - `@nx/webpack` calls the [Webpack CLI](https://webpack.js.org/api/cli/) - etc. diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 746a4a1779b4b..2059beb0ec394 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -755,3 +755,4 @@ - [fix-configuration](/nx-api/workspace/generators/fix-configuration) - [npm-package](/nx-api/workspace/generators/npm-package) - [ci-workflow](/nx-api/workspace/generators/ci-workflow) + - [infer-targets](/nx-api/workspace/generators/infer-targets) diff --git a/e2e/nx/src/workspace.test.ts b/e2e/nx/src/workspace.test.ts index 8290c2fb784d8..9b66cefe14442 100644 --- a/e2e/nx/src/workspace.test.ts +++ b/e2e/nx/src/workspace.test.ts @@ -13,11 +13,150 @@ import { tmpProjPath, uniq, updateFile, + updateJson, } from '@nx/e2e/utils'; import { join } from 'path'; let proj: string; +describe('@nx/workspace:infer-targets', () => { + beforeEach(() => { + proj = newProject({ + packages: ['@nx/playwright', '@nx/remix', '@nx/eslint', '@nx/jest'], + }); + }); + + afterEach(() => cleanupProject()); + + it('should run or skip conversions depending on whether executors are present', async () => { + // default case, everything is generated with crystal, everything should be skipped + const remixApp = uniq('remix'); + runCLI( + `generate @nx/remix:app ${remixApp} --dir apps --unitTestRunner jest --e2eTestRunner=playwright --projectNameAndDirectoryFormat=as-provided --no-interactive` + ); + + const output = runCLI(`generate infer-targets --no-interactive`); + + expect(output).toContain('@nx/remix:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/playwright:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/eslint:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/jest:convert-to-inferred - Skipped'); + + // if we make sure there are executors to convert, conversions will run + updateJson('nx.json', (json) => { + json.plugins = []; + return json; + }); + + updateJson(join('apps', remixApp, 'project.json'), (json) => { + json.targets = { + build: { + executor: '@nx/remix:build', + }, + lint: { + executor: '@nx/eslint:lint', + }, + }; + return json; + }); + + const output2 = runCLI(`generate infer-targets --no-interactive`); + + expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); + expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); + }); + + it('should run or skip only specific conversions if --plugins is passed', async () => { + // default case, everything is generated with crystal, relevant plugins should be skipped + const remixApp = uniq('remix'); + runCLI( + `generate @nx/remix:app ${remixApp} --dir apps --unitTestRunner jest --e2eTestRunner=playwright --projectNameAndDirectoryFormat=as-provided --no-interactive` + ); + + const output = runCLI( + `generate infer-targets --plugins=@nx/eslint,@nx/jest --no-interactive` + ); + + expect(output).toContain('@nx/eslint:convert-to-inferred - Skipped'); + expect(output).toContain('@nx/jest:convert-to-inferred - Skipped'); + + expect(output).not.toContain('@nx/remix'); + expect(output).not.toContain('@nx/playwright'); + + // if we make sure there are executors to convert, relevant conversions will run + updateJson('nx.json', (json) => { + json.plugins = []; + return json; + }); + + updateJson(join('apps', remixApp, 'project.json'), (json) => { + json.targets = { + build: { + executor: '@nx/remix:build', + }, + lint: { + executor: '@nx/eslint:lint', + }, + }; + return json; + }); + + const output2 = runCLI( + `generate infer-targets --plugins=@nx/remix,@nx/eslint --no-interactive` + ); + + expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); + expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); + + expect(output2).not.toContain('@nx/jest'); + expect(output2).not.toContain('@nx/playwright'); + }); + + it('should run only specific conversions for a specific project if --project is passed', async () => { + // even if we make sure there are executors for remix & remix-e2e, only remix conversions will run with --project option + const remixApp = uniq('remix'); + runCLI( + `generate @nx/remix:app ${remixApp} --dir apps --unitTestRunner jest --e2eTestRunner=playwright --projectNameAndDirectoryFormat=as-provided --no-interactive` + ); + + updateJson('nx.json', (json) => { + json.plugins = []; + return json; + }); + + updateJson(join('apps', remixApp, 'project.json'), (json) => { + json.targets = { + build: { + executor: '@nx/remix:build', + }, + lint: { + executor: '@nx/eslint:lint', + }, + }; + return json; + }); + + updateJson(join('apps', `${remixApp}-e2e`, 'project.json'), (json) => { + json.targets = { + e2e: { + executor: '@nx/playwright:playwright', + }, + }; + return json; + }); + + const output2 = runCLI( + `generate infer-targets --project ${remixApp} --no-interactive` + ); + + expect(output2).toContain('@nx/remix:convert-to-inferred - Success'); + expect(output2).toContain('@nx/eslint:convert-to-inferred - Success'); + + expect(output2).toContain('@nx/jest:convert-to-inferred - Skipped'); + expect(output2).toContain('@nx/playwright:convert-to-inferred - Skipped'); + }); +}); + describe('@nx/workspace:convert-to-monorepo', () => { beforeEach(() => { proj = newProject({ packages: ['@nx/react', '@nx/js'] }); diff --git a/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts index 280e4bfd18d79..a24a7f29d5176 100644 --- a/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/cypress/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -4,7 +4,10 @@ import { type TargetConfiguration, type Tree, } from '@nx/devkit'; -import { migrateProjectExecutorsToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPlugin, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { processTargetOutputs, toProjectRelativePath, @@ -46,7 +49,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts index 57bd700154bed..991093e4eabd8 100644 --- a/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/detox/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -7,7 +7,10 @@ import { updateNxJson, } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; -import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPluginV1, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { createNodes } from '../../plugins/plugin'; import { processBuildOptions } from './lib/process-build-options'; import { postTargetTransformer } from './lib/post-target-transformer'; @@ -97,7 +100,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { updateNxJson(tree, nxJson); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts b/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts index e314b1e7ca015..31b49b4ae3377 100644 --- a/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts +++ b/packages/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator.ts @@ -28,6 +28,7 @@ import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-comma import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils'; import { forEachExecutorOptions } from '../executor-options-utils'; import { deleteMatchingProperties } from './plugin-migration-utils'; +import { logger as devkitLogger } from 'nx/src/devkit-exports'; export type InferredTargetConfiguration = TargetConfiguration & { name: string; @@ -57,6 +58,7 @@ class ExecutorToPluginMigrator { readonly #skipTargetFilter: SkipTargetFilter; readonly #skipProjectFilter: SkipProjectFilter; readonly #specificProjectToMigrate: string; + readonly #logger: typeof devkitLogger; #nxJson: NxJsonConfiguration; #targetDefaultsForExecutor: Partial; #targetAndProjectsToMigrate: Map>; @@ -78,7 +80,8 @@ class ExecutorToPluginMigrator { filters?: { skipProjectFilter?: SkipProjectFilter; skipTargetFilter?: SkipTargetFilter; - } + }, + logger?: typeof devkitLogger ) { this.tree = tree; this.#projectGraph = projectGraph; @@ -92,6 +95,7 @@ class ExecutorToPluginMigrator { this.#skipProjectFilter = filters?.skipProjectFilter ?? ((...args) => false); this.#skipTargetFilter = filters?.skipTargetFilter ?? ((...args) => false); + this.#logger = logger ?? devkitLogger; } async run(): Promise>> { @@ -255,7 +259,7 @@ class ExecutorToPluginMigrator { throw new Error(errorMsg); } - console.warn(errorMsg); + this.#logger.warn(errorMsg); return; } @@ -268,7 +272,7 @@ class ExecutorToPluginMigrator { if (this.#specificProjectToMigrate) { throw new Error(errorMsg); } else { - console.warn(errorMsg); + this.#logger.warn(errorMsg); } return; } @@ -336,6 +340,12 @@ class ExecutorToPluginMigrator { } } +export class NoTargetsToMigrateError extends Error { + constructor() { + super('Could not find any targets to migrate.'); + } +} + export async function migrateProjectExecutorsToPlugin( tree: Tree, projectGraph: ProjectGraph, @@ -349,7 +359,8 @@ export async function migrateProjectExecutorsToPlugin( skipProjectFilter?: SkipProjectFilter; skipTargetFilter?: SkipTargetFilter; }>, - specificProjectToMigrate?: string + specificProjectToMigrate?: string, + logger?: typeof devkitLogger ): Promise>> { const projects = await migrateProjects( tree, @@ -359,7 +370,8 @@ export async function migrateProjectExecutorsToPlugin( createNodesV2, defaultPluginOptions, migrations, - specificProjectToMigrate + specificProjectToMigrate, + logger ); return projects; @@ -408,7 +420,8 @@ async function migrateProjects( skipProjectFilter?: SkipProjectFilter; skipTargetFilter?: SkipTargetFilter; }>, - specificProjectToMigrate?: string + specificProjectToMigrate?: string, + logger?: typeof devkitLogger ): Promise>> { const projects = new Map>(); @@ -427,7 +440,8 @@ async function migrateProjects( { skipProjectFilter: migration.skipProjectFilter, skipTargetFilter: migration.skipTargetFilter, - } + }, + logger ); const result = await migrator.run(); diff --git a/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts index 60da20c7c43d6..a3237fbb8f2b4 100644 --- a/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -5,7 +5,10 @@ import { type TargetConfiguration, type Tree, } from '@nx/devkit'; -import { migrateProjectExecutorsToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPlugin, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { processTargetOutputs } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils'; import { basename, dirname, relative } from 'node:path/posix'; import { interpolate } from 'nx/src/tasks-runner/utils'; @@ -40,7 +43,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/expo/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/expo/src/generators/convert-to-inferred/convert-to-inferred.ts index e0c4780ba7f42..b1c6025b5fd63 100644 --- a/packages/expo/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/expo/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -5,7 +5,10 @@ import { type Tree, } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; -import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPluginV1, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { createNodes } from '../../../plugins/plugin'; import { processBuildOptions } from './lib/process-build-options'; import { postTargetTransformer } from './lib/post-target-transformer'; @@ -140,7 +143,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/jest/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/jest/src/generators/convert-to-inferred/convert-to-inferred.ts index 2e433e96b8222..05448c0decef1 100644 --- a/packages/jest/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/jest/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -5,7 +5,10 @@ import { type TargetConfiguration, type Tree, } from '@nx/devkit'; -import { migrateProjectExecutorsToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPlugin, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { processTargetOutputs, toProjectRelativePath, @@ -40,7 +43,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts index dfe5208ff0ddd..a01338f37898a 100644 --- a/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -1,6 +1,9 @@ import { createProjectGraphAsync, formatFiles, Tree } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; -import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPluginV1, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { createNodes } from '../../plugins/plugin'; import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; import { servePosTargetTransformer } from './lib/serve-post-target-tranformer'; @@ -45,7 +48,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/nx/src/command-line/generate/generator-utils.ts b/packages/nx/src/command-line/generate/generator-utils.ts index 20d57f4eff259..b5de6cef8411c 100644 --- a/packages/nx/src/command-line/generate/generator-utils.ts +++ b/packages/nx/src/command-line/generate/generator-utils.ts @@ -12,12 +12,7 @@ import { import { readJsonFile } from '../../utils/fileutils'; import { readPluginPackageJson } from '../../project-graph/plugins'; -export function getGeneratorInformation( - collectionName: string, - generatorName: string, - root: string | null, - projects: Record -): { +export type GeneratorInformation = { resolvedCollectionName: string; normalizedGeneratorName: string; schema: any; @@ -25,7 +20,14 @@ export function getGeneratorInformation( isNgCompat: boolean; isNxGenerator: boolean; generatorConfiguration: GeneratorsJsonEntry; -} { +}; + +export function getGeneratorInformation( + collectionName: string, + generatorName: string, + root: string | null, + projects: Record +): GeneratorInformation { try { const { generatorsFilePath, diff --git a/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts index 849012751daab..3d446a4e8f07c 100644 --- a/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/playwright/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -6,7 +6,10 @@ import { type Tree, } from '@nx/devkit'; import { createNodesV2, PlaywrightPluginOptions } from '../../plugins/plugin'; -import { migrateProjectExecutorsToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPlugin, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; interface Schema { project?: string; @@ -34,7 +37,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/react-native/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/react-native/src/generators/convert-to-inferred/convert-to-inferred.ts index d80b809a23ff3..3e924041004c5 100644 --- a/packages/react-native/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/react-native/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -1,6 +1,9 @@ import { createProjectGraphAsync, formatFiles, type Tree } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; -import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPluginV1, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { createNodes } from '../../../plugins/plugin'; import { postTargetTransformer } from './lib/post-target-transformer'; import { processStartOptions } from './lib/process-start-options'; @@ -123,7 +126,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts index 6dfb1edf87b80..0506b309b8784 100644 --- a/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -1,6 +1,9 @@ import { createProjectGraphAsync, formatFiles, type Tree } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; -import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPluginV1, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { createNodes } from '../../plugins/plugin'; import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; import { servePostTargetTransformer } from './lib/serve-post-target-transformer'; @@ -45,7 +48,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/rollup/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/rollup/src/generators/convert-to-inferred/convert-to-inferred.ts index ead904a4ca138..18487dc7c9778 100644 --- a/packages/rollup/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/rollup/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -10,6 +10,7 @@ import type { RollupExecutorOptions } from '../../executors/rollup/schema'; import type { RollupPluginOptions } from '../../plugins/plugin'; import { extractRollupConfigFromExecutorOptions } from './lib/extract-rollup-config-from-executor-options'; import { addPluginRegistrations } from './lib/add-plugin-registrations'; +import { NoTargetsToMigrateError } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; interface Schema { project?: string; @@ -100,7 +101,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } projects = getProjects(tree); diff --git a/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts index a3f8808086cd9..11cbc2db215b6 100644 --- a/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -6,7 +6,10 @@ import { type Tree, } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; -import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPluginV1, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; import { servePostTargetTransformer } from './lib/serve-post-target-transformer'; import { createNodes } from '../../plugins/plugin'; @@ -51,7 +54,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts index 7ee000b715628..f99d299abc6e2 100644 --- a/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -1,5 +1,8 @@ import { createProjectGraphAsync, formatFiles, type Tree } from '@nx/devkit'; -import { migrateProjectExecutorsToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPlugin, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { createNodesV2, VitePluginOptions } from '../../plugins/plugin'; import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; import { servePostTargetTransformer } from './lib/serve-post-target-transformer'; @@ -55,7 +58,7 @@ export async function convertToInferred(tree: Tree, options: Schema) { ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + throw new NoTargetsToMigrateError(); } if (!options.skipFormat) { diff --git a/packages/webpack/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/webpack/src/generators/convert-to-inferred/convert-to-inferred.ts index 11b1a1d766777..dca66bfbaa12c 100644 --- a/packages/webpack/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/webpack/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -7,7 +7,10 @@ import { type Tree, } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; -import { migrateProjectExecutorsToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { + migrateProjectExecutorsToPlugin, + NoTargetsToMigrateError, +} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { tsquery } from '@phenomnomnominal/tsquery'; import * as ts from 'typescript'; import { createNodesV2, type WebpackPluginOptions } from '../../plugins/plugin'; @@ -17,6 +20,7 @@ import { servePostTargetTransformerFactory, type MigrationContext, } from './utils'; +import { logger as devkitLogger } from 'nx/src/devkit-exports'; interface Schema { project?: string; @@ -31,6 +35,8 @@ export async function convertToInferred(tree: Tree, options: Schema) { workspaceRoot: tree.root, }; + const logger = createCollectingLogger(); + const migratedProjects = await migrateProjectExecutorsToPlugin( tree, @@ -59,11 +65,24 @@ export async function convertToInferred(tree: Tree, options: Schema) { skipProjectFilter: skipProjectFilterFactory(tree), }, ], - options.project + options.project, + logger ); if (migratedProjects.size === 0) { - throw new Error('Could not find any targets to migrate.'); + const convertMessage = [...logger.loggedMessages.values()] + .flat() + .find((v) => v.includes('@nx/webpack:convert-config-to-webpack-plugin')); + + if (convertMessage.length > 0) { + logger.flushLogs((message) => !convertMessage.includes(message)); + throw new Error(convertMessage); + } else { + logger.flushLogs(); + throw new NoTargetsToMigrateError(); + } + } else { + logger.flushLogs(); } const installCallback = addDependenciesToPackageJson( @@ -125,3 +144,42 @@ function skipProjectFilterFactory(tree: Tree) { return false; }; } + +export function createCollectingLogger(): typeof devkitLogger & { + loggedMessages: Map; + flushLogs: (filter?: (message: string) => boolean) => void; +} { + const loggedMessages = new Map(); + + const flushLogs = (filter?: (message: string) => boolean) => { + loggedMessages.forEach((messages, method) => { + messages.forEach((message) => { + if (!filter || filter(message)) { + devkitLogger[method](message); + } + }); + }); + }; + + return new Proxy( + { ...devkitLogger, loggedMessages, flushLogs }, + { + get(target, property) { + const originalMethod = target[property]; + + if (typeof originalMethod === 'function') { + return (...args) => { + const message = args.join(' '); + const propertyString = String(property); + if (!loggedMessages.has(message)) { + loggedMessages.set(propertyString, []); + } + loggedMessages.get(propertyString).push(message); + }; + } + + return originalMethod; + }, + } + ); +} diff --git a/packages/workspace/generators.json b/packages/workspace/generators.json index aadabfccde872..8ef6a398dbe3e 100644 --- a/packages/workspace/generators.json +++ b/packages/workspace/generators.json @@ -54,6 +54,11 @@ "factory": "./src/generators/ci-workflow/ci-workflow#ciWorkflowGenerator", "schema": "./src/generators/ci-workflow/schema.json", "description": "Generate a CI workflow." + }, + "infer-targets": { + "factory": "./src/generators/infer-targets/infer-targets", + "schema": "./src/generators/infer-targets/schema.json", + "description": "Convert Nx projects to use inferred targets." } } } diff --git a/packages/workspace/src/generators/infer-targets/infer-targets.ts b/packages/workspace/src/generators/infer-targets/infer-targets.ts new file mode 100644 index 0000000000000..d0300c02fd2c3 --- /dev/null +++ b/packages/workspace/src/generators/infer-targets/infer-targets.ts @@ -0,0 +1,146 @@ +import { + createProjectGraphAsync, + formatFiles, + GeneratorCallback, + output, + readProjectsConfigurationFromProjectGraph, + runTasksInSerial, + Tree, + workspaceRoot, +} from '@nx/devkit'; +import { NoTargetsToMigrateError } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { prompt } from 'enquirer'; +import { + GeneratorInformation, + getGeneratorInformation, +} from 'nx/src/command-line/generate/generator-utils'; +import { findInstalledPlugins } from 'nx/src/utils/plugins/installed-plugins'; + +interface Schema { + project?: string; + plugins?: string[]; + skipFormat?: boolean; +} + +export async function convertToInferredGenerator(tree: Tree, options: Schema) { + const generatorCollectionChoices = + await getPossibleConvertToInferredGenerators(); + + if (generatorCollectionChoices.size === 0) { + output.error({ + title: + 'No inference plugin found. For information on this migration, see https://nx.dev/recipes/running-tasks/convert-to-inferred', + }); + return; + } + let generatorsToRun: string[]; + if (options.plugins && options.plugins.filter((p) => !!p).length > 0) { + generatorsToRun = Array.from(generatorCollectionChoices.values()) + .filter((generator) => + options.plugins.includes(generator.resolvedCollectionName) + ) + .map((generator) => generator.resolvedCollectionName); + } else if (process.argv.includes('--no-interactive')) { + generatorsToRun = Array.from(generatorCollectionChoices.keys()); + } else { + const allChoices = Array.from(generatorCollectionChoices.keys()); + + generatorsToRun = ( + await prompt<{ generatorsToRun: string[] }>({ + type: 'multiselect', + name: 'generatorsToRun', + message: 'Which inference plugin do you want to use?', + choices: allChoices, + initial: allChoices, + validate: (result: string[]) => { + if (result.length === 0) { + return 'Please select at least one plugin.'; + } + return true; + }, + } as any) + ).generatorsToRun; + } + + if (generatorsToRun.length === 0) { + output.error({ + title: 'Please select at least one plugin.', + }); + return; + } + + const tasks: GeneratorCallback[] = []; + for (const generatorCollection of generatorsToRun) { + try { + const generator = generatorCollectionChoices.get(generatorCollection); + if (generator) { + const generatorFactory = generator.implementationFactory(); + const callback = await generatorFactory(tree, { + project: options.project, + skipFormat: options.skipFormat, + }); + if (callback) { + const task = await callback(); + if (typeof task === 'function') tasks.push(task); + } + output.success({ + title: `${generatorCollection}:convert-to-inferred - Success`, + }); + } + } catch (e) { + if (e instanceof NoTargetsToMigrateError) { + output.note({ + title: `${generatorCollection}:convert-to-inferred - Skipped (No targets to migrate)`, + }); + } else { + output.error({ + title: `${generatorCollection}:convert-to-inferred - Failed`, + }); + throw e; + } + } + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return runTasksInSerial(...tasks); +} + +async function getPossibleConvertToInferredGenerators() { + const installedCollections = Array.from( + new Set(findInstalledPlugins().map((x) => x.name)) + ); + + const projectGraph = await createProjectGraphAsync(); + const projectsConfigurations = + readProjectsConfigurationFromProjectGraph(projectGraph); + + const choices = new Map(); + + for (const collectionName of installedCollections) { + try { + const generator = getGeneratorInformation( + collectionName, + 'convert-to-inferred', + workspaceRoot, + projectsConfigurations.projects + ); + if ( + generator.generatorConfiguration.hidden || + generator.generatorConfiguration['x-deprecated'] + ) { + continue; + } + + choices.set(generator.resolvedCollectionName, generator); + } catch { + // this just means that no convert-to-inferred generator exists for a given collection, ignore + } + } + + return choices; +} + +export default convertToInferredGenerator; diff --git a/packages/workspace/src/generators/infer-targets/schema.json b/packages/workspace/src/generators/infer-targets/schema.json new file mode 100644 index 0000000000000..51f8923c40d2a --- /dev/null +++ b/packages/workspace/src/generators/infer-targets/schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "InferTargets", + "title": "", + "type": "object", + "description": "Convert Nx projects to use inferred targets.", + "properties": { + "project": { + "type": "string", + "description": "The project to convert to use inferred targets.", + "x-priority": "important" + }, + "plugins": { + "type": "array", + "description": "The plugins used to infer targets. For example @nx/eslint or @nx/jest", + "items": { + "type": "string" + } + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files.", + "default": false + } + } +}