From a5317608dcb57a4d5e2326e4f7eec05fa704629f Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 10 Dec 2024 09:31:32 +0000 Subject: [PATCH] feat(react): add support for React 19 for new Workspaces --- packages/react/migrations.json | 9 ++ packages/react/package.json | 3 +- .../application/application.spec.ts | 66 +++++++++- .../src/generators/application/application.ts | 2 +- .../lib/install-common-dependencies.ts | 9 +- .../src/generators/host/host.rspack.spec.ts | 4 + .../src/generators/host/host.webpack.spec.ts | 4 + packages/react/src/generators/init/init.ts | 17 +-- .../generators/setup-ssr/setup-ssr.spec.ts | 4 + .../configuration.spec.ts | 4 +- .../react/src/utils/version-utils.spec.ts | 115 ++++++++++++++++++ packages/react/src/utils/version-utils.ts | 82 +++++++++++++ packages/react/src/utils/versions.ts | 20 +-- packages/rspack/src/utils/versions.ts | 4 - 14 files changed, 315 insertions(+), 28 deletions(-) create mode 100644 packages/react/src/utils/version-utils.spec.ts create mode 100644 packages/react/src/utils/version-utils.ts diff --git a/packages/react/migrations.json b/packages/react/migrations.json index 83c5716ced6e8..e4c92a3290300 100644 --- a/packages/react/migrations.json +++ b/packages/react/migrations.json @@ -185,6 +185,15 @@ "alwaysAddToPackageJson": false } } + }, + "20.3.0": { + "version": "20.3.0-beta.0", + "packages": { + "@testing-library/react": { + "version": "16.1.0", + "alwaysAddToPackageJson": false + } + } } } } diff --git a/packages/react/package.json b/packages/react/package.json index e5d7f0a3348d8..0f8d11a87b47a 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -44,7 +44,8 @@ "@nx/web": "file:../web", "@nx/module-federation": "file:../module-federation", "express": "^4.19.2", - "http-proxy-middleware": "^3.0.3" + "http-proxy-middleware": "^3.0.3", + "semver": "^7.6.3" }, "publishConfig": { "access": "public" diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index 874abcde48376..47b23f489c52d 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -1,9 +1,8 @@ -import 'nx/src/internal-testing-utils/mock-project-graph'; - import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { getPackageManagerCommand, getProjects, + ProjectGraph, readJson, readNxJson, Tree, @@ -19,6 +18,17 @@ import { Schema } from './schema'; // which is v9 while we are testing for the new v10 version jest.mock('@nx/cypress/src/utils/cypress-version'); +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => { + const original = jest.requireActual('@nx/devkit'); + return { + ...original, + createProjectGraphAsync: jest + .fn() + .mockImplementation(() => Promise.resolve(projectGraph)), + }; +}); + const packageCmd = getPackageManagerCommand().exec; describe('app', () => { @@ -39,6 +49,7 @@ describe('app', () => { beforeEach(() => { mockedInstalledCypressVersion.mockReturnValue(10); appTree = createTreeWithEmptyWorkspace(); + projectGraph = { dependencies: {}, nodes: {}, externalNodes: {} }; }); describe('not nested', () => { @@ -1419,4 +1430,55 @@ describe('app', () => { `); }); }); + + describe('react 19 support', () => { + beforeEach(() => { + projectGraph = { dependencies: {}, nodes: {}, externalNodes: {} }; + }); + + it('should add react 19 dependencies when react version is not found', async () => { + projectGraph.externalNodes['npm:react'] = undefined; + const tree = createTreeWithEmptyWorkspace(); + + await applicationGenerator(tree, { + ...schema, + directory: 'my-dir/my-app', + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.dependencies['react']).toMatchInlineSnapshot( + `"19.0.0"` + ); + expect(packageJson.dependencies['react-dom']).toMatchInlineSnapshot( + `"19.0.0"` + ); + }); + + it('should add react 18 dependencies when react version is already 18', async () => { + const tree = createTreeWithEmptyWorkspace(); + + projectGraph.externalNodes['npm:react'] = { + type: 'npm', + name: 'npm:react', + data: { + version: '18.3.1', + packageName: 'react', + hash: 'sha512-4+0/v9+l9/3+3/2+2/1+1/0', + }, + }; + + await applicationGenerator(tree, { + ...schema, + directory: 'my-dir/my-app', + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.dependencies['react']).toMatchInlineSnapshot( + `"18.3.1"` + ); + expect(packageJson.dependencies['react-dom']).toMatchInlineSnapshot( + `"18.3.1"` + ); + }); + }); }); diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index c9a421896e46a..e2c155657fd24 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -313,7 +313,7 @@ export async function applicationGeneratorInternal( // Handle tsconfig.spec.json for jest or vitest updateSpecConfig(host, options); - const stylePreprocessorTask = installCommonDependencies(host, options); + const stylePreprocessorTask = await installCommonDependencies(host, options); tasks.push(stylePreprocessorTask); const styledTask = addStyledModuleDependencies(host, options); tasks.push(styledTask); diff --git a/packages/react/src/generators/application/lib/install-common-dependencies.ts b/packages/react/src/generators/application/lib/install-common-dependencies.ts index 3f5bd9fa03030..c0c4eca96bf12 100644 --- a/packages/react/src/generators/application/lib/install-common-dependencies.ts +++ b/packages/react/src/generators/application/lib/install-common-dependencies.ts @@ -12,8 +12,9 @@ import { typesReactVersion, } from '../../../utils/versions'; import { NormalizedSchema } from '../schema'; +import { getReactDependenciesVersionsToInstall } from '../../../utils/version-utils'; -export function installCommonDependencies( +export async function installCommonDependencies( host: Tree, options: NormalizedSchema ) { @@ -21,11 +22,13 @@ export function installCommonDependencies( return () => {}; } + const reactDeps = await getReactDependenciesVersionsToInstall(host); + const dependencies: Record = {}; const devDependencies: Record = { '@types/node': typesNodeVersion, - '@types/react': typesReactVersion, - '@types/react-dom': typesReactDomVersion, + '@types/react': reactDeps['@types/react'], + '@types/react-dom': reactDeps['@types/react-dom'], }; if (options.bundler !== 'vite') { diff --git a/packages/react/src/generators/host/host.rspack.spec.ts b/packages/react/src/generators/host/host.rspack.spec.ts index 032ab68ce10d6..41dc2c1c7d495 100644 --- a/packages/react/src/generators/host/host.rspack.spec.ts +++ b/packages/react/src/generators/host/host.rspack.spec.ts @@ -9,6 +9,10 @@ jest.mock('@nx/devkit', () => { const original = jest.requireActual('@nx/devkit'); return { ...original, + createProjectGraphAsync: jest.fn().mockResolvedValue({ + dependencies: {}, + nodes: {}, + }), readCachedProjectGraph: jest.fn().mockImplementation( (): ProjectGraph => ({ dependencies: {}, diff --git a/packages/react/src/generators/host/host.webpack.spec.ts b/packages/react/src/generators/host/host.webpack.spec.ts index 7f4a836377e4a..820b644e884eb 100644 --- a/packages/react/src/generators/host/host.webpack.spec.ts +++ b/packages/react/src/generators/host/host.webpack.spec.ts @@ -9,6 +9,10 @@ jest.mock('@nx/devkit', () => { const original = jest.requireActual('@nx/devkit'); return { ...original, + createProjectGraphAsync: jest.fn().mockResolvedValue({ + dependencies: {}, + nodes: {}, + }), readCachedProjectGraph: jest.fn().mockImplementation( (): ProjectGraph => ({ dependencies: {}, diff --git a/packages/react/src/generators/init/init.ts b/packages/react/src/generators/init/init.ts index 105e863d62793..ac267d2fd3ab3 100755 --- a/packages/react/src/generators/init/init.ts +++ b/packages/react/src/generators/init/init.ts @@ -6,21 +6,22 @@ import { type GeneratorCallback, type Tree, } from '@nx/devkit'; -import { nxVersion, reactDomVersion, reactVersion } from '../../utils/versions'; +import { nxVersion } from '../../utils/versions'; import { InitSchema } from './schema'; +import { getReactDependenciesVersionsToInstall } from '../../utils/version-utils'; -export async function reactInitGenerator(host: Tree, schema: InitSchema) { +export async function reactInitGenerator(tree: Tree, schema: InitSchema) { const tasks: GeneratorCallback[] = []; if (!schema.skipPackageJson) { - tasks.push(removeDependenciesFromPackageJson(host, ['@nx/react'], [])); - + tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/react'], [])); + const reactDeps = await getReactDependenciesVersionsToInstall(tree); tasks.push( addDependenciesToPackageJson( - host, + tree, { - react: reactVersion, - 'react-dom': reactDomVersion, + react: reactDeps.react, + 'react-dom': reactDeps['react-dom'], }, { '@nx/react': nxVersion, @@ -32,7 +33,7 @@ export async function reactInitGenerator(host: Tree, schema: InitSchema) { } if (!schema.skipFormat) { - await formatFiles(host); + await formatFiles(tree); } return runTasksInSerial(...tasks); diff --git a/packages/react/src/generators/setup-ssr/setup-ssr.spec.ts b/packages/react/src/generators/setup-ssr/setup-ssr.spec.ts index b400220264ef9..23885b94ff68b 100644 --- a/packages/react/src/generators/setup-ssr/setup-ssr.spec.ts +++ b/packages/react/src/generators/setup-ssr/setup-ssr.spec.ts @@ -15,6 +15,10 @@ jest.mock('@nx/devkit', () => { const original = jest.requireActual('@nx/devkit'); return { ...original, + createProjectGraphAsync: jest.fn().mockResolvedValue({ + dependencies: {}, + nodes: {}, + }), readCachedProjectGraph: jest.fn().mockImplementation( (): ProjectGraph => ({ dependencies: {}, diff --git a/packages/react/src/generators/storybook-configuration/configuration.spec.ts b/packages/react/src/generators/storybook-configuration/configuration.spec.ts index 144bd4d8afa41..474435061e6ca 100644 --- a/packages/react/src/generators/storybook-configuration/configuration.spec.ts +++ b/packages/react/src/generators/storybook-configuration/configuration.spec.ts @@ -7,8 +7,8 @@ import libraryGenerator from '../library/library'; import storybookConfigurationGenerator from './configuration'; // nested code imports graph from the repo, which might have innacurate graph version -jest.mock('nx/src/project-graph/project-graph', () => ({ - ...jest.requireActual('nx/src/project-graph/project-graph'), +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), createProjectGraphAsync: jest .fn() .mockImplementation(async () => ({ nodes: {}, dependencies: {} })), diff --git a/packages/react/src/utils/version-utils.spec.ts b/packages/react/src/utils/version-utils.spec.ts new file mode 100644 index 0000000000000..f1ba61210708b --- /dev/null +++ b/packages/react/src/utils/version-utils.spec.ts @@ -0,0 +1,115 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { getReactDependenciesVersionsToInstall } from './version-utils'; +import { type ProjectGraph } from '@nx/devkit'; +import { + reactDomV18Version, + reactDomVersion, + reactIsV18Version, + reactIsVersion, + reactV18Version, + reactVersion, + typesReactDomV18Version, + typesReactDomVersion, + typesReactIsV18Version, + typesReactIsVersion, + typesReactV18Version, + typesReactVersion, +} from './versions'; + +let projectGraph: ProjectGraph; + +jest.mock('@nx/devkit', () => { + const original = jest.requireActual('@nx/devkit'); + return { + ...original, + createProjectGraphAsync: jest + .fn() + .mockImplementation(() => Promise.resolve(projectGraph)), + }; +}); + +describe('getReactDependenciesVersionsToInstall', () => { + beforeEach(() => { + projectGraph = { + dependencies: {}, + nodes: {}, + externalNodes: {}, + }; + }); + + it('should return the correct versions of react and react-dom when react 18 is installed', async () => { + // ARRANGE + projectGraph.externalNodes['npm:react'] = { + type: 'npm', + name: 'npm:react', + data: { + version: '18.3.1', + packageName: 'react', + hash: 'sha512-4+0/v9+l9/3+3/2+2/1+1/0', + }, + }; + + // ACT + const reactDependencies = await getReactDependenciesVersionsToInstall( + createTreeWithEmptyWorkspace() + ); + + // ASSERT + expect(reactDependencies).toEqual({ + react: reactV18Version, + 'react-dom': reactDomV18Version, + 'react-is': reactIsV18Version, + '@types/react': typesReactV18Version, + '@types/react-dom': typesReactDomV18Version, + '@types/react-is': typesReactIsV18Version, + }); + }); + + it('should return the correct versions of react and react-dom when react 19 is installed', async () => { + // ARRANGE + projectGraph.externalNodes['npm:react'] = { + type: 'npm', + name: 'npm:react', + data: { + version: '19.0.0', + packageName: 'react', + hash: 'sha512-4+0/v9+l9/3+3/2+2/1+1/0', + }, + }; + + // ACT + const reactDependencies = await getReactDependenciesVersionsToInstall( + createTreeWithEmptyWorkspace() + ); + + // ASSERT + expect(reactDependencies).toEqual({ + react: reactVersion, + 'react-dom': reactDomVersion, + 'react-is': reactIsVersion, + '@types/react': typesReactVersion, + '@types/react-dom': typesReactDomVersion, + '@types/react-is': typesReactIsVersion, + }); + }); + + it('should return the correct versions of react and react-dom when react is not installed', async () => { + // ARRANGE + projectGraph.externalNodes['npm:react'] = undefined; + + // ACT + const reactDependencies = await getReactDependenciesVersionsToInstall( + createTreeWithEmptyWorkspace() + ); + + // ASSERT + expect(reactDependencies).toEqual({ + react: reactVersion, + 'react-dom': reactDomVersion, + 'react-is': reactIsVersion, + '@types/react': typesReactVersion, + '@types/react-dom': typesReactDomVersion, + '@types/react-is': typesReactIsVersion, + }); + }); +}); diff --git a/packages/react/src/utils/version-utils.ts b/packages/react/src/utils/version-utils.ts new file mode 100644 index 0000000000000..b45875a705a7e --- /dev/null +++ b/packages/react/src/utils/version-utils.ts @@ -0,0 +1,82 @@ +import { type Tree, readJson, createProjectGraphAsync } from '@nx/devkit'; +import { clean, coerce, major } from 'semver'; +import { + reactDomV18Version, + reactIsV18Version, + reactV18Version, + reactVersion, + typesReactDomV18Version, + typesReactIsV18Version, + typesReactV18Version, + reactDomVersion, + reactIsVersion, + typesReactVersion, + typesReactDomVersion, + typesReactIsVersion, +} from './versions'; + +type ReactDependenciesVersions = { + react: string; + 'react-dom': string; + 'react-is': string; + '@types/react': string; + '@types/react-dom': string; + '@types/react-is': string; +}; + +export async function getReactDependenciesVersionsToInstall( + tree: Tree +): Promise { + if (await isReact18(tree)) { + return { + react: reactV18Version, + 'react-dom': reactDomV18Version, + 'react-is': reactIsV18Version, + '@types/react': typesReactV18Version, + '@types/react-dom': typesReactDomV18Version, + '@types/react-is': typesReactIsV18Version, + }; + } else { + return { + react: reactVersion, + 'react-dom': reactDomVersion, + 'react-is': reactIsVersion, + '@types/react': typesReactVersion, + '@types/react-dom': typesReactDomVersion, + '@types/react-is': typesReactIsVersion, + }; + } +} + +export async function isReact18(tree: Tree) { + let installedReactVersion = await getInstalledReactVersionFromGraph(); + if (!installedReactVersion) { + installedReactVersion = getInstalledReactVersion(tree); + } + return major(installedReactVersion) === 18; +} + +export function getInstalledReactVersion(tree: Tree): string { + const pkgJson = readJson(tree, 'package.json'); + const installedReactVersion = + pkgJson.dependencies && pkgJson.dependencies['react']; + + if ( + !installedReactVersion || + installedReactVersion === 'latest' || + installedReactVersion === 'next' + ) { + return clean(reactVersion) ?? coerce(reactVersion).version; + } + + return clean(installedReactVersion) ?? coerce(installedReactVersion).version; +} + +export async function getInstalledReactVersionFromGraph() { + const graph = await createProjectGraphAsync(); + const reactDep = graph.externalNodes?.['npm:react']; + if (!reactDep) { + return undefined; + } + return clean(reactDep.data.version) ?? coerce(reactDep.data.version).version; +} diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts index 782a8b62d69d4..5b16e484d539e 100755 --- a/packages/react/src/utils/versions.ts +++ b/packages/react/src/utils/versions.ts @@ -1,13 +1,19 @@ export const nxVersion = require('../../package.json').version; -export const reactVersion = '18.3.1'; -export const reactDomVersion = '18.3.1'; -export const reactIsVersion = '18.3.1'; +export const reactVersion = '19.0.0'; +export const reactV18Version = '18.3.1'; +export const reactDomVersion = '19.0.0'; +export const reactDomV18Version = '18.3.1'; +export const reactIsVersion = '19.0.0'; +export const reactIsV18Version = '18.3.1'; export const swcLoaderVersion = '0.1.15'; export const babelLoaderVersion = '^9.1.2'; -export const typesReactVersion = '18.3.1'; -export const typesReactDomVersion = '18.3.0'; -export const typesReactIsVersion = '18.3.0'; +export const typesReactV18Version = '18.3.1'; +export const typesReactVersion = '19.0.0'; +export const typesReactDomV18Version = '18.3.0'; +export const typesReactDomVersion = '19.0.0'; +export const typesReactIsV18Version = '18.3.0'; +export const typesReactIsVersion = '19.0.0'; export const reactViteVersion = '^4.2.0'; export const typesNodeVersion = '18.16.9'; @@ -27,7 +33,7 @@ export const styledJsxVersion = '5.1.2'; export const reactRouterDomVersion = '6.11.2'; -export const testingLibraryReactVersion = '15.0.6'; +export const testingLibraryReactVersion = '16.1.0'; export const reduxjsToolkitVersion = '1.9.3'; export const reactReduxVersion = '8.0.5'; diff --git a/packages/rspack/src/utils/versions.ts b/packages/rspack/src/utils/versions.ts index fae45debec1a8..d8675268cb41d 100644 --- a/packages/rspack/src/utils/versions.ts +++ b/packages/rspack/src/utils/versions.ts @@ -6,11 +6,7 @@ export const rspackPluginMinifyVersion = '^0.7.5'; export const rspackPluginReactRefreshVersion = '^1.0.0'; export const lessLoaderVersion = '~11.1.3'; -export const reactVersion = '~18.2.0'; export const reactRefreshVersion = '~0.14.0'; -export const reactDomVersion = '~18.2.0'; -export const typesReactVersion = '~18.0.28'; -export const typesReactDomVersion = '~18.0.10'; export const nestjsCommonVersion = '~9.0.0'; export const nestjsCoreVersion = '~9.0.0';