Skip to content

Commit

Permalink
feat(nextjs): Add convert-to-inferred generator (#26706)
Browse files Browse the repository at this point in the history
This PR enables the ability to migrate project(s) from using nextjs
executors to inferred targets.

---------

Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
  • Loading branch information
ndcunningham and jaysoo authored Jun 27, 2024
1 parent 7f8bb4b commit 4197a91
Show file tree
Hide file tree
Showing 18 changed files with 1,165 additions and 22 deletions.
8 changes: 8 additions & 0 deletions docs/generated/manifests/menus.json
Original file line number Diff line number Diff line change
Expand Up @@ -8216,6 +8216,14 @@
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "convert-to-inferred",
"path": "/nx-api/next/generators/convert-to-inferred",
"name": "convert-to-inferred",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
Expand Down
9 changes: 9 additions & 0 deletions docs/generated/manifests/nx-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,15 @@
"originalFilePath": "/packages/next/src/generators/cypress-component-configuration/schema.json",
"path": "/nx-api/next/generators/cypress-component-configuration",
"type": "generator"
},
"/nx-api/next/generators/convert-to-inferred": {
"description": "Convert an existing Next.js project(s) using `@nx/next:build` to use `@nx/next/plugin`. Defaults to migrating all projects. Pass '--project' to migrate a single project.",
"file": "generated/packages/next/generators/convert-to-inferred.json",
"hidden": false,
"name": "convert-to-inferred",
"originalFilePath": "/packages/next/src/generators/convert-to-inferred/schema.json",
"path": "/nx-api/next/generators/convert-to-inferred",
"type": "generator"
}
},
"path": "/nx-api/next"
Expand Down
9 changes: 9 additions & 0 deletions docs/generated/packages-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,15 @@
"originalFilePath": "/packages/next/src/generators/cypress-component-configuration/schema.json",
"path": "next/generators/cypress-component-configuration",
"type": "generator"
},
{
"description": "Convert an existing Next.js project(s) using `@nx/next:build` to use `@nx/next/plugin`. Defaults to migrating all projects. Pass '--project' to migrate a single project.",
"file": "generated/packages/next/generators/convert-to-inferred.json",
"hidden": false,
"name": "convert-to-inferred",
"originalFilePath": "/packages/next/src/generators/convert-to-inferred/schema.json",
"path": "next/generators/convert-to-inferred",
"type": "generator"
}
],
"githubRoot": "https://github.com/nrwl/nx/blob/master",
Expand Down
30 changes: 30 additions & 0 deletions docs/generated/packages/next/generators/convert-to-inferred.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "convert-to-inferred",
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
"schema": {
"$schema": "https://json-schema.org/schema",
"$id": "NxNextjsConvertToInferred",
"description": "Convert existing Next.js project(s) using `@nx/next:build` executor to use `@nx/next/plugin`.",
"title": "Convert a Nextjs project from executor to plugin",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The project to convert from using the `@nx/next:build` executor to use `@nx/next/plugin`. If not provided, all projects using the `@nx/next:build` executor will be converted.",
"x-priority": "important"
},
"skipFormat": {
"type": "boolean",
"description": "Whether to format files.",
"default": false
}
},
"presets": []
},
"description": "Convert an existing Next.js project(s) using `@nx/next:build` to use `@nx/next/plugin`. Defaults to migrating all projects. Pass '--project' to migrate a single project.",
"implementation": "/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts",
"aliases": [],
"hidden": false,
"path": "/packages/next/src/generators/convert-to-inferred/schema.json",
"type": "generator"
}
100 changes: 85 additions & 15 deletions docs/shared/guides/troubleshoot-convert-to-inferred.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Troubleshoot Convert to Inferred Migration

Nx comes with plugins that automatically [infer tasks](/concepts/inferred-tasks) (i.e. Project Crystal) for your projects based on the configuration of different tools. Inference plugins come with many benefits, such as reduced boilerplate and access to features such as [task splitting](/ci/features/split-e2e-tasks). To make the transition easier for existing projects that are not yet using inference plugins, many plugins provide the `convert-to-inferred` generator that will switch from executor-based tasks to inferred tasks.
Nx comes with plugins that automatically [infer tasks](/concepts/inferred-tasks) (i.e. Project Crystal) for your
projects based on the configuration of different tools. Inference plugins come with many benefits, such as reduced
boilerplate and access to features such as [task splitting](/ci/features/split-e2e-tasks). To make the transition easier
for existing projects that are not yet using inference plugins, many plugins provide the `convert-to-inferred` generator
that will switch from executor-based tasks to inferred tasks.

To see a list of the available migration generators, run:

Expand All @@ -10,19 +14,69 @@ nx g convert-to-inferred

This will prompt you to choose a plugin to run the migration for.

Although the `convert-to-inferred` generator should work for most projects, there are situations that require additional changes to be done by hand. If you run into issues that are not covered on this page, please open an issue on [GitHub](https://github.com/nrwl/nx/issues).
Although the `convert-to-inferred` generator should work for most projects, there are situations that require additional
changes to be done by hand. If you run into issues that are not covered on this page, please open an issue
on [GitHub](https://github.com/nrwl/nx/issues).

## Error: The nx plugin did not find a project inside...

This error occurs when a configuration file matching the tooling cannot be found. For example, Vite works with `vite.config.ts` (or `.js`, `.cts`, `.mts`, etc.). If you've named your configuration file to something unconventional, you must rename it back to the standard naming convention before running the migration generator again.
This error occurs when a configuration file matching the tooling cannot be found. For example, Vite works
with `vite.config.ts` (or `.js`, `.cts`, `.mts`, etc.). If you've named your configuration file to something
unconventional, you must rename it back to the standard naming convention before running the migration generator again.

For example, if you have a `apps/demo/vite.custom.ts` file and are running `nx g @nx/vite:convert-to-inferred`, you must first rename the file to `apps/demo/vite.config.ts` before running the generator.
For example, if you have a `apps/demo/vite.custom.ts` file and are running `nx g @nx/vite:convert-to-inferred`, you must
first rename the file to `apps/demo/vite.config.ts` before running the generator.

## Remix: Unsupported `outputPath` Option
## Next.js: Unable to Migrate `outputPath`, `generateLockfile` and `includeDevDependenciesInPackageJson` Options

The [`outputPath`](/nx-api/remix/executors/build#outputpath) option from `@nx/remix:build` is ignored because it often leads to ESM errors when the output path is outside the project root. The ESM error occurs because the root `package.json` may not have `"type": "module"` set, which means that the compiled ESM code will fail to run. To guarantee that `serve` works, we migrate the outputs to the Remix defaults (`build` and `public/build` inside the project root). If you have custom directories already defined in your Remix config, it will continue to be used.
The [`outputPath`](/nx-api/remix/executors/build#outputpath) option from `@nx/next:build` is ignored because it
conflicts with Next.js' requirement that [`distDir`](https://nextjs.org/docs/app/api-reference/next-config-js/distDir)
remain inside the project directory. Previously, the `@nx/next:build` executor performed workarounds to bring it outside
the project root, but those workarounds lead to other issues, such as Turbopack not working.

To change the outputs after the migration, edit the remix config file, and look for `serverBuildPath` and `assetsBuildDirectory` and set it to the locations you want.
To customize the output directory, set `distDir` in your Next.js config file.

```js
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default';
// ...
const nextConfig = {
nx: {
...options,
},
// Differentiate production and development builds. You can also use the `configuration` variable that will match the `--configuration` option passed to Nx.
distDir: process.env.NODE_ENV === 'production' ? 'dist' : 'dist-dev',
};
const plugins = [withNx];
module.exports = composePlugins(...plugins)(nextConfig);
```

Since the output directory is now inside the project, we do not generate `package.json` since it is already present. The
lockfile generation support also no longer exists, which does not affect deployments to Vercel, Netlify, or similar
environments. However, it could affect deployments via Docker images where you do not copy the whole monorepo, but
rather just the build artifacts.

These removals are necessary to align with Next.js recommendations.

## Next.js: Nx `serve` Only Starts Dev Server

To better align with Next.js CLI, projects after the migration have two targets to start the server:

1. `serve` - Starts the dev server (same as `next dev`)
2. `start` - Starts the prod server (same as `next start`)

Note that `serve` could be different depending on what you used for `@nx/next:server` previously. After the
migration, `nx run <proj>:serve --prod` not longer starts the prod server. Use `nx run <proj>:start` instead.

## Remix: Unable to Migrate `outputPath` Option

The [`outputPath`](/nx-api/remix/executors/build#outputpath) option from `@nx/remix:build` is ignored because it often
leads to ESM errors when the output path is outside the project root. The ESM error occurs because the
root `package.json` may not have `"type": "module"` set, which means that the compiled ESM code will fail to run. To
guarantee that `serve` works, we migrate the outputs to the Remix defaults (`build` and `public/build` inside the
project root). If you have custom directories already defined in your Remix config, it will continue to be used.

To change the outputs after the migration, edit the remix config file, and look for `serverBuildPath`
and `assetsBuildDirectory` and set it to the locations you want.

```ts
// ...
Expand All @@ -33,17 +87,24 @@ export default {
};
```

Note that you will need to address potential ESM issues that may arise. For example, change the root `package.json` to `"type": "module"`.
Note that you will need to address potential ESM issues that may arise. For example, change the root `package.json`
to `"type": "module"`.

## Remix: Unsupported `generatePackageJson` and `generateLockFile` Options

The `generatePackageJson` and `generateLockFile` options in [`@nx/remix:build`](/nx-api/remix/executors/build) cannot currently be migrated. There is support for this feature in the [Nx Vite plugin](/recipes/vite/configure-vite#typescript-paths), so in the future we may be able to support it if using Remix+Vite.
The `generatePackageJson` and `generateLockFile` options in [`@nx/remix:build`](/nx-api/remix/executors/build) cannot
currently be migrated. There is support for this feature in
the [Nx Vite plugin](/recipes/vite/configure-vite#typescript-paths), so in the future we may be able to support it if
using Remix+Vite.

## Storybook: Conflicting `staticDir` Options

Using `staticDir` for both `@nx/storybook:build-storybook` and `@nx/storybook:storybook` executor options will result in the one from `build-storybook` being used in the resulting `.storybook/main.ts` file. It is not possible for us to support both automatically.
Using `staticDir` for both `@nx/storybook:build-storybook` and `@nx/storybook:storybook` executor options will result in
the one from `build-storybook` being used in the resulting `.storybook/main.ts` file. It is not possible for us to
support both automatically.

If you need to differentiate `staticDir` between build and serve, then consider putting logic into your `main.ts` file directly.
If you need to differentiate `staticDir` between build and serve, then consider putting logic into your `main.ts` file
directly.

```ts
// ...
Expand All @@ -60,7 +121,8 @@ export default config;

## Vite: Unsupported `proxyConfig` Option

Projects that used the [`proxyConfig`](/nx-api/vite/executors/dev-server#proxyconfig) option of `@nx/vite:dev-server` will need to inline the proxy configuration from the original file into `vite.config.ts`.
Projects that used the [`proxyConfig`](/nx-api/vite/executors/dev-server#proxyconfig) option of `@nx/vite:dev-server`
will need to inline the proxy configuration from the original file into `vite.config.ts`.

For example, if you previously used this in `proxy.config.json`:

Expand Down Expand Up @@ -90,13 +152,20 @@ export default defineConfig({

## Webpack: Project Cannot Be Migrated

Projects that use [Nx-enhanced Webpack configuration](/recipes/webpack/webpack-config-setup#nxenhanced-configuration-with-composable-plugins) files cannot be migrated to use Webpack CLI. Nx-enhanced configuration files that contain `composePlugins` and `withNx` require the `@nx/webpack:webpack` executor to work.
Projects that
use [Nx-enhanced Webpack configuration](/recipes/webpack/webpack-config-setup#nxenhanced-configuration-with-composable-plugins)
files cannot be migrated to use Webpack CLI. Nx-enhanced configuration files that contain `composePlugins` and `withNx`
require the `@nx/webpack:webpack` executor to work.

To solve this issue, run `nx g @nx/webpack:convert-config-to-webpack-plugin` first, and then try again.

## Webpack: Usage of `useLegacyNxPlugin`

When converting from Nx-enhanced to basic Webpack configuration, we add the `useLegacyNxPlugin` utility to ensure that the functionality of your existing configuration continues to function normally. We recommend that you refactor the configuration such that `useLegacyNxPlugin` is not needed.
When converting
from [Nx-enhanced](/recipes/webpack/webpack-config-setup#nxenhanced-configuration-with-composable-plugins) to basic
Webpack configuration, we add the `useLegacyNxPlugin` utility function to
ensure that your build tasks behave the same after the migration. We recommend that you refactor the configuration such
that `useLegacyNxPlugin` is not needed.

For example, if you previously added plugins using the configuration function.

Expand All @@ -117,7 +186,8 @@ module.exports = async () => ({
});
```

If you need to apply configuration changes after `NxAppWebpackPlugin` is applied, then you can create a plugin object as follows.
If you need to apply configuration changes after `NxAppWebpackPlugin` is applied, then you can create a plugin object as
follows.

```js
module.exports = async () => ({
Expand Down
1 change: 1 addition & 0 deletions docs/shared/reference/sitemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@
- [library](/nx-api/next/generators/library)
- [custom-server](/nx-api/next/generators/custom-server)
- [cypress-component-configuration](/nx-api/next/generators/cypress-component-configuration)
- [convert-to-inferred](/nx-api/next/generators/convert-to-inferred)
- [node](/nx-api/node)
- [documents](/nx-api/node/documents)
- [Overview](/nx-api/node/documents/overview)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-con
import { forEachExecutorOptions } from '../executor-options-utils';
import { deleteMatchingProperties } from './plugin-migration-utils';

export type InferredTargetConfiguration = TargetConfiguration & {
name: string;
};
type PluginOptionsBuilder<T> = (targetName: string) => T;
type PostTargetTransformer = (
targetConfiguration: TargetConfiguration,
tree: Tree,
projectDetails: { projectName: string; root: string },
inferredTargetConfiguration: TargetConfiguration
inferredTargetConfiguration: InferredTargetConfiguration
) => TargetConfiguration | Promise<TargetConfiguration>;
type SkipTargetFilter = (
targetConfiguration: TargetConfiguration
Expand Down Expand Up @@ -154,7 +157,7 @@ class ExecutorToPluginMigrator<T> {
projectTarget,
this.tree,
{ projectName, root: projectFromGraph.data.root },
createdTarget
{ ...createdTarget, name: targetName }
);

if (
Expand Down
5 changes: 5 additions & 0 deletions packages/next/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
"factory": "./src/generators/cypress-component-configuration/cypress-component-configuration#cypressComponentConfigurationInternal",
"schema": "./src/generators/cypress-component-configuration/schema.json",
"description": "cypress-component-configuration generator"
},
"convert-to-inferred": {
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
"schema": "./src/generators/convert-to-inferred/schema.json",
"description": "Convert an existing Next.js project(s) using `@nx/next:build` to use `@nx/next/plugin`. Defaults to migrating all projects. Pass '--project' to migrate a single project."
}
}
}
3 changes: 2 additions & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"@nx/react": "file:../react",
"@nx/web": "file:../web",
"@nx/webpack": "file:../webpack",
"@nx/workspace": "file:../workspace"
"@nx/workspace": "file:../workspace",
"@phenomnomnominal/tsquery": "~5.0.1"
},
"publishConfig": {
"access": "public"
Expand Down
14 changes: 10 additions & 4 deletions packages/next/plugins/with-nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ProjectGraphProjectNode,
type Target,
} from '@nx/devkit';
import type { AssetGlobPattern } from '@nx/webpack';

export interface SvgrOptions {
svgo?: boolean;
Expand All @@ -22,6 +23,8 @@ export interface WithNxOptions extends NextConfig {
nx?: {
svgr?: boolean | SvgrOptions;
babelUpwardRootMode?: boolean;
fileReplacements?: { replace: string; with: string }[];
assets?: AssetGlobPattern[];
};
}

Expand Down Expand Up @@ -210,12 +213,15 @@ function withNx(
const userWebpackConfig = nextConfig.webpack;

const { createWebpackConfig } = require('@nx/next/src/utils/config');
// If we have file replacements or assets, inside of the next config we pass the workspaceRoot as a join of the workspaceRoot and the projectDirectory
// Because the file replacements and assets are relative to the projectRoot, not the workspaceRoot
nextConfig.webpack = (a, b) =>
createWebpackConfig(
workspaceRoot,
projectDirectory,
options.fileReplacements,
options.assets
_nextConfig.nx?.fileReplacements
? joinPathFragments(workspaceRoot, projectDirectory)
: workspaceRoot,
_nextConfig.nx?.assets || options.assets,
_nextConfig.nx?.fileReplacements || options.fileReplacements
)(userWebpackConfig ? userWebpackConfig(a, b) : a, b);

return nextConfig;
Expand Down
Loading

0 comments on commit 4197a91

Please sign in to comment.