feat(react): add support for React 19 for new Workspaces (#29286)

## Current Behavior
We currently have no support for React 19, generating only React 18
applications.

## Expected Behavior
Add utils to determine what version of React is installed in the
workspace.
If React 18 is the main version of react installed, continue to generate
React 18 projects.
If React 19 is the main version of react installed, generate React 19
projects.
If no React version is installed or can be determined, generate React 19
projects.
This commit is contained in:
Colum Ferry 2025-01-17 15:14:48 +00:00 committed by GitHub
parent bc0566f4c3
commit a468d72c7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 512 additions and 155 deletions

View File

@ -95,7 +95,7 @@
"bundler": { "bundler": {
"description": "The bundler to use.", "description": "The bundler to use.",
"type": "string", "type": "string",
"enum": ["vite", "webpack", "rspack", "rsbuild"], "enum": ["vite", "rsbuild", "rspack", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?", "x-prompt": "Which bundler do you want to use to build the application?",
"default": "vite", "default": "vite",
"x-priority": "important" "x-priority": "important"

View File

@ -69,7 +69,7 @@
"bundler": { "bundler": {
"description": "The bundler to use for building the application.", "description": "The bundler to use for building the application.",
"type": "string", "type": "string",
"enum": ["webpack", "vite", "rspack", "esbuild"], "enum": ["vite", "rspack", "rsbuild", "esbuild", "webpack"],
"default": "vite" "default": "vite"
}, },
"docker": { "docker": {

View File

@ -5,7 +5,7 @@ exports[`Next.js Applications next-env.d.ts should remain the same after a build
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
" "
`; `;
@ -14,6 +14,6 @@ exports[`Next.js Applications next-env.d.ts should remain the same after a build
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
" "
`; `;

View File

@ -241,14 +241,14 @@ export default App;
const results = runCLI(`build ${app} --buildLibsFromSource=true`); const results = runCLI(`build ${app} --buildLibsFromSource=true`);
expect(results).toContain('Successfully ran target build for project'); expect(results).toContain('Successfully ran target build for project');
// this should be more modules than build from dist // this should be more modules than build from dist
expect(results).toContain('40 modules transformed'); expect(results).toContain('43 modules transformed');
}); });
it('should build app from libs dist', () => { it('should build app from libs dist', () => {
const results = runCLI(`build ${app} --buildLibsFromSource=false`); const results = runCLI(`build ${app} --buildLibsFromSource=false`);
expect(results).toContain('Successfully ran target build for project'); expect(results).toContain('Successfully ran target build for project');
// this should be less modules than building from source // this should be less modules than building from source
expect(results).toContain('38 modules transformed'); expect(results).toContain('41 modules transformed');
}); });
it('should build app from libs without package.json in lib', () => { it('should build app from libs without package.json in lib', () => {

View File

@ -37,7 +37,7 @@
"dependencies": { "dependencies": {
"@nx/devkit": "file:../devkit", "@nx/devkit": "file:../devkit",
"@babel/plugin-proposal-decorators": "^7.22.7", "@babel/plugin-proposal-decorators": "^7.22.7",
"@svgr/webpack": "^8.0.1", "@svgr/webpack": "^8.1.0",
"copy-webpack-plugin": "^10.2.4", "copy-webpack-plugin": "^10.2.4",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"ignore": "^5.0.4", "ignore": "^5.0.4",

View File

@ -8,11 +8,8 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js'; import { initGenerator as jsInitGenerator } from '@nx/js';
import { setupTailwindGenerator } from '@nx/react'; import { setupTailwindGenerator } from '@nx/react';
import { import { testingLibraryReactVersion } from '@nx/react/src/utils/versions';
testingLibraryReactVersion, import { getReactDependenciesVersionsToInstall } from '@nx/react/src/utils/version-utils';
typesReactDomVersion,
typesReactVersion,
} from '@nx/react/src/utils/versions';
import { normalizeOptions } from './lib/normalize-options'; import { normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema'; import { Schema } from './schema';
@ -104,9 +101,10 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
} }
if (!options.skipPackageJson) { if (!options.skipPackageJson) {
const reactVersions = await getReactDependenciesVersionsToInstall(host);
const devDependencies: Record<string, string> = { const devDependencies: Record<string, string> = {
'@types/react': typesReactVersion, '@types/react': reactVersions['@types/react'],
'@types/react-dom': typesReactDomVersion, '@types/react-dom': reactVersions['@types/react-dom'],
}; };
if (options.unitTestRunner && options.unitTestRunner !== 'none') { if (options.unitTestRunner && options.unitTestRunner !== 'none') {

View File

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/<%- appDirType %>/building-your-application/configuring/typescript for more information. // see https://nextjs.org/docs/<%- appDirType %>/api-reference/config/typescript for more information.

View File

@ -224,12 +224,12 @@ async function getNextE2EWebServerInfo(
{ {
plugin: '@nx/next/plugin', plugin: '@nx/next/plugin',
serveTargetName: 'devTargetName', serveTargetName: 'devTargetName',
serveStaticTargetName: 'serveStaticTargetName', serveStaticTargetName: 'startTargetName',
configFilePath, configFilePath,
}, },
{ {
defaultServeTargetName: defaultServeTarget, defaultServeTargetName: defaultServeTarget,
defaultServeStaticTargetName: 'serve-static', defaultServeStaticTargetName: 'start',
defaultE2EWebServerAddress: `http://127.0.0.1:${e2ePort}`, defaultE2EWebServerAddress: `http://127.0.0.1:${e2ePort}`,
defaultE2ECiBaseUrl: `http://localhost:${e2ePort}`, defaultE2ECiBaseUrl: `http://localhost:${e2ePort}`,
defaultE2EPort: e2ePort, defaultE2EPort: e2ePort,

View File

@ -8,23 +8,33 @@ import {
createProjectGraphAsync, createProjectGraphAsync,
} from '@nx/devkit'; } from '@nx/devkit';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { reactDomVersion, reactVersion } from '@nx/react/src/utils/versions'; import {
getReactDependenciesVersionsToInstall,
isReact18,
} from '@nx/react/src/utils/version-utils';
import { addGitIgnoreEntry } from '../../utils/add-gitignore-entry'; import { addGitIgnoreEntry } from '../../utils/add-gitignore-entry';
import { nextVersion, nxVersion } from '../../utils/versions'; import { nxVersion } from '../../utils/versions';
import { getNextDependenciesVersionsToInstall } from '../../utils/version-utils';
import type { InitSchema } from './schema'; import type { InitSchema } from './schema';
function updateDependencies(host: Tree, schema: InitSchema) { async function updateDependencies(host: Tree, schema: InitSchema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
tasks.push(removeDependenciesFromPackageJson(host, ['@nx/next'], [])); tasks.push(removeDependenciesFromPackageJson(host, ['@nx/next'], []));
const versions = await getNextDependenciesVersionsToInstall(
host,
await isReact18(host)
);
const reactVersions = await getReactDependenciesVersionsToInstall(host);
tasks.push( tasks.push(
addDependenciesToPackageJson( addDependenciesToPackageJson(
host, host,
{ {
next: nextVersion, next: versions.next,
react: reactVersion, react: reactVersions.react,
'react-dom': reactDomVersion, 'react-dom': reactVersions['react-dom'],
}, },
{ {
'@nx/next': nxVersion, '@nx/next': nxVersion,
@ -86,7 +96,7 @@ export async function nextInitGeneratorInternal(
let installTask: GeneratorCallback = () => {}; let installTask: GeneratorCallback = () => {};
if (!schema.skipPackageJson) { if (!schema.skipPackageJson) {
installTask = updateDependencies(host, schema); installTask = await updateDependencies(host, schema);
} }
return installTask; return installTask;

View File

@ -45,12 +45,12 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = `
}, },
}, },
"my-serve-static": { "my-serve-static": {
"executor": "@nx/web:file-server", "command": "next start",
"dependsOn": [
"my-build",
],
"options": { "options": {
"buildTarget": "my-build", "cwd": "my-app",
"port": 3000,
"spa": false,
"staticFilePath": "{projectRoot}/out",
}, },
}, },
"my-start": { "my-start": {
@ -121,12 +121,12 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = `
}, },
}, },
"serve-static": { "serve-static": {
"executor": "@nx/web:file-server", "command": "next start",
"dependsOn": [
"build",
],
"options": { "options": {
"buildTarget": "build", "cwd": ".",
"port": 3000,
"spa": false,
"staticFilePath": "{projectRoot}/out",
}, },
}, },
"start": { "start": {

View File

@ -28,6 +28,9 @@ export interface NextPluginOptions {
buildTargetName?: string; buildTargetName?: string;
devTargetName?: string; devTargetName?: string;
startTargetName?: string; startTargetName?: string;
/**
* @deprecated Use `startTargetName` instead.
*/
serveStaticTargetName?: string; serveStaticTargetName?: string;
buildDepsTargetName?: string; buildDepsTargetName?: string;
watchDepsTargetName?: string; watchDepsTargetName?: string;
@ -172,9 +175,11 @@ async function buildNextTargets(
targets[options.devTargetName] = getDevTargetConfig(projectRoot); targets[options.devTargetName] = getDevTargetConfig(projectRoot);
targets[options.startTargetName] = getStartTargetConfig(options, projectRoot); const startTarget = getStartTargetConfig(options, projectRoot);
targets[options.serveStaticTargetName] = getStaticServeTargetConfig(options); targets[options.startTargetName] = startTarget;
targets[options.serveStaticTargetName] = startTarget;
addBuildAndWatchDepsTargets( addBuildAndWatchDepsTargets(
context.workspaceRoot, context.workspaceRoot,

View File

@ -0,0 +1,55 @@
import { type Tree, readJson, createProjectGraphAsync } from '@nx/devkit';
import { clean, coerce, major } from 'semver';
import { nextVersion, next14Version } from './versions';
type NextDependenciesVersions = {
next: string;
};
export async function getNextDependenciesVersionsToInstall(
tree: Tree,
isReact18 = false
): Promise<NextDependenciesVersions> {
if (await isNext14(tree)) {
return {
next: next14Version,
};
} else {
return {
next: nextVersion,
};
}
}
export async function isNext14(tree: Tree) {
let installedNextVersion = await getInstalledNextVersionFromGraph();
if (!installedNextVersion) {
installedNextVersion = getInstalledNextVersion(tree);
}
return major(installedNextVersion) === 14;
}
export function getInstalledNextVersion(tree: Tree): string {
const pkgJson = readJson(tree, 'package.json');
const installedNextVersion =
pkgJson.dependencies && pkgJson.dependencies['next'];
if (
!installedNextVersion ||
installedNextVersion === 'latest' ||
installedNextVersion === 'next'
) {
return clean(nextVersion) ?? coerce(nextVersion).version;
}
return clean(installedNextVersion) ?? coerce(installedNextVersion).version;
}
export async function getInstalledNextVersionFromGraph() {
const graph = await createProjectGraphAsync();
const nextDep = graph.externalNodes?.['npm:next'];
if (!nextDep) {
return undefined;
}
return clean(nextDep.data.version) ?? coerce(nextDep.data.version).version;
}

View File

@ -1,6 +1,7 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const nextVersion = '14.2.16'; export const nextVersion = '~15.1.4';
export const next14Version = '~14.2.16';
export const eslintConfigNextVersion = '14.2.16'; export const eslintConfigNextVersion = '14.2.16';
export const sassVersion = '1.62.1'; export const sassVersion = '1.62.1';
export const lessLoader = '11.1.0'; export const lessLoader = '11.1.0';

View File

@ -197,6 +197,15 @@
"alwaysAddToPackageJson": false "alwaysAddToPackageJson": false
} }
} }
},
"20.3.0": {
"version": "20.3.0-beta.0",
"packages": {
"@testing-library/react": {
"version": "16.1.0",
"alwaysAddToPackageJson": false
}
}
} }
} }
} }

View File

@ -44,7 +44,8 @@
"@nx/web": "file:../web", "@nx/web": "file:../web",
"@nx/module-federation": "file:../module-federation", "@nx/module-federation": "file:../module-federation",
"express": "^4.19.2", "express": "^4.19.2",
"http-proxy-middleware": "^3.0.3" "http-proxy-middleware": "^3.0.3",
"semver": "^7.6.3"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -1,9 +1,8 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import { import {
getPackageManagerCommand, getPackageManagerCommand,
getProjects, getProjects,
ProjectGraph,
readJson, readJson,
readNxJson, readNxJson,
Tree, Tree,
@ -21,6 +20,17 @@ const { load } = require('@zkochan/js-yaml');
// which is v9 while we are testing for the new v10 version // which is v9 while we are testing for the new v10 version
jest.mock('@nx/cypress/src/utils/cypress-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; const packageCmd = getPackageManagerCommand().exec;
describe('app', () => { describe('app', () => {
@ -41,6 +51,7 @@ describe('app', () => {
beforeEach(() => { beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10); mockedInstalledCypressVersion.mockReturnValue(10);
appTree = createTreeWithEmptyWorkspace(); appTree = createTreeWithEmptyWorkspace();
projectGraph = { dependencies: {}, nodes: {}, externalNodes: {} };
}); });
describe('not nested', () => { describe('not nested', () => {
@ -1569,4 +1580,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"`
);
});
});
}); });

View File

@ -153,7 +153,7 @@ export async function applicationGeneratorInternal(
// Handle tsconfig.spec.json for jest or vitest // Handle tsconfig.spec.json for jest or vitest
updateSpecConfig(tree, options); updateSpecConfig(tree, options);
const stylePreprocessorTask = installCommonDependencies(tree, options); const stylePreprocessorTask = await installCommonDependencies(tree, options);
tasks.push(stylePreprocessorTask); tasks.push(stylePreprocessorTask);
const styledTask = addStyledModuleDependencies(tree, options); const styledTask = addStyledModuleDependencies(tree, options);
tasks.push(styledTask); tasks.push(styledTask);

View File

@ -6,14 +6,16 @@ import {
sassVersion, sassVersion,
swcLoaderVersion, swcLoaderVersion,
testingLibraryReactVersion, testingLibraryReactVersion,
testingLibraryDomVersion,
tsLibVersion, tsLibVersion,
typesNodeVersion, typesNodeVersion,
typesReactDomVersion, typesReactDomVersion,
typesReactVersion, typesReactVersion,
} from '../../../utils/versions'; } from '../../../utils/versions';
import { NormalizedSchema } from '../schema'; import { NormalizedSchema } from '../schema';
import { getReactDependenciesVersionsToInstall } from '../../../utils/version-utils';
export function installCommonDependencies( export async function installCommonDependencies(
host: Tree, host: Tree,
options: NormalizedSchema options: NormalizedSchema
) { ) {
@ -21,11 +23,13 @@ export function installCommonDependencies(
return () => {}; return () => {};
} }
const reactDeps = await getReactDependenciesVersionsToInstall(host);
const dependencies: Record<string, string> = {}; const dependencies: Record<string, string> = {};
const devDependencies: Record<string, string> = { const devDependencies: Record<string, string> = {
'@types/node': typesNodeVersion, '@types/node': typesNodeVersion,
'@types/react': typesReactVersion, '@types/react': reactDeps['@types/react'],
'@types/react-dom': typesReactDomVersion, '@types/react-dom': reactDeps['@types/react-dom'],
}; };
if (options.bundler !== 'vite') { if (options.bundler !== 'vite') {
@ -58,6 +62,7 @@ export function installCommonDependencies(
if (options.unitTestRunner && options.unitTestRunner !== 'none') { if (options.unitTestRunner && options.unitTestRunner !== 'none') {
devDependencies['@testing-library/react'] = testingLibraryReactVersion; devDependencies['@testing-library/react'] = testingLibraryReactVersion;
devDependencies['@testing-library/dom'] = testingLibraryDomVersion;
} }
return addDependenciesToPackageJson(host, {}, devDependencies); return addDependenciesToPackageJson(host, {}, devDependencies);

View File

@ -101,7 +101,7 @@
"bundler": { "bundler": {
"description": "The bundler to use.", "description": "The bundler to use.",
"type": "string", "type": "string",
"enum": ["vite", "webpack", "rspack", "rsbuild"], "enum": ["vite", "rsbuild", "rspack", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?", "x-prompt": "Which bundler do you want to use to build the application?",
"default": "vite", "default": "vite",
"x-priority": "important" "x-priority": "important"

View File

@ -9,6 +9,10 @@ jest.mock('@nx/devkit', () => {
const original = jest.requireActual('@nx/devkit'); const original = jest.requireActual('@nx/devkit');
return { return {
...original, ...original,
createProjectGraphAsync: jest.fn().mockResolvedValue({
dependencies: {},
nodes: {},
}),
readCachedProjectGraph: jest.fn().mockImplementation( readCachedProjectGraph: jest.fn().mockImplementation(
(): ProjectGraph => ({ (): ProjectGraph => ({
dependencies: {}, dependencies: {},

View File

@ -9,6 +9,10 @@ jest.mock('@nx/devkit', () => {
const original = jest.requireActual('@nx/devkit'); const original = jest.requireActual('@nx/devkit');
return { return {
...original, ...original,
createProjectGraphAsync: jest.fn().mockResolvedValue({
dependencies: {},
nodes: {},
}),
readCachedProjectGraph: jest.fn().mockImplementation( readCachedProjectGraph: jest.fn().mockImplementation(
(): ProjectGraph => ({ (): ProjectGraph => ({
dependencies: {}, dependencies: {},

View File

@ -6,21 +6,22 @@ import {
type GeneratorCallback, type GeneratorCallback,
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { nxVersion, reactDomVersion, reactVersion } from '../../utils/versions'; import { nxVersion } from '../../utils/versions';
import { InitSchema } from './schema'; 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[] = []; const tasks: GeneratorCallback[] = [];
if (!schema.skipPackageJson) { if (!schema.skipPackageJson) {
tasks.push(removeDependenciesFromPackageJson(host, ['@nx/react'], [])); tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/react'], []));
const reactDeps = await getReactDependenciesVersionsToInstall(tree);
tasks.push( tasks.push(
addDependenciesToPackageJson( addDependenciesToPackageJson(
host, tree,
{ {
react: reactVersion, react: reactDeps.react,
'react-dom': reactDomVersion, 'react-dom': reactDeps['react-dom'],
}, },
{ {
'@nx/react': nxVersion, '@nx/react': nxVersion,
@ -32,7 +33,7 @@ export async function reactInitGenerator(host: Tree, schema: InitSchema) {
} }
if (!schema.skipFormat) { if (!schema.skipFormat) {
await formatFiles(host); await formatFiles(tree);
} }
return runTasksInSerial(...tasks); return runTasksInSerial(...tasks);

View File

@ -5,6 +5,7 @@ import {
ProjectGraph, ProjectGraph,
readCachedProjectGraph, readCachedProjectGraph,
readJson, readJson,
readNxJson,
Tree, Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import applicationGenerator from '../application/application'; import applicationGenerator from '../application/application';
@ -12,17 +13,7 @@ import setupSsrGenerator from './setup-ssr';
import { Linter } from '@nx/eslint'; import { Linter } from '@nx/eslint';
jest.mock('@nx/devkit', () => { jest.mock('@nx/devkit', () => {
const original = jest.requireActual('@nx/devkit'); const myAppData = {
return {
...original,
readCachedProjectGraph: jest.fn().mockImplementation(
(): ProjectGraph => ({
dependencies: {},
nodes: {
'my-app': {
name: 'my-app',
type: 'app',
data: {
root: 'my-app', root: 'my-app',
sourceRoot: 'my-app/src', sourceRoot: 'my-app/src',
targets: { targets: {
@ -83,11 +74,32 @@ jest.mock('@nx/devkit', () => {
}, },
}, },
}, },
};
const pg: ProjectGraph = {
dependencies: {},
nodes: {
'my-app': {
name: 'my-app',
type: 'app',
data: { ...myAppData },
}, },
}, },
}, };
}) const original = jest.requireActual('@nx/devkit');
), return {
...original,
createProjectGraphAsync: jest.fn().mockResolvedValue(pg),
readCachedProjectGraph: jest
.fn()
.mockImplementation((): ProjectGraph => pg),
readProjectConfiguration: jest
.fn()
.mockImplementation((tree, projectName) => {
if (projectName === 'my-app') {
return { ...myAppData };
}
return original.readProjectConfiguration(tree, projectName);
}),
}; };
}); });
@ -106,12 +118,12 @@ describe('setupSsrGenerator', () => {
afterEach(() => { afterEach(() => {
process.env.NX_ADD_PLUGINS = originalEnv; process.env.NX_ADD_PLUGINS = originalEnv;
jest.clearAllMocks(); jest.clearAllMocks();
jest.resetModules();
}); });
beforeEach(() => { it('should add SSR files', async () => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
await applicationGenerator(tree, {
applicationGenerator(tree, {
directory: appName, directory: appName,
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,
@ -119,24 +131,14 @@ describe('setupSsrGenerator', () => {
e2eTestRunner: 'none', e2eTestRunner: 'none',
skipFormat: true, skipFormat: true,
}); });
});
it('should add SSR files', async () => {
await setupSsrGenerator(tree, {
project: appName,
});
expect(tree.exists(`${appName}/server.ts`)).toBeTruthy();
expect(tree.exists(`${appName}/tsconfig.server.json`)).toBeTruthy();
});
it('should support adding additional include files', async () => {
await setupSsrGenerator(tree, { await setupSsrGenerator(tree, {
project: appName, project: appName,
extraInclude: ['src/remote.d.ts'], extraInclude: ['src/remote.d.ts'],
}); });
expect(tree.exists(`${appName}/server.ts`)).toBeTruthy(); expect(tree.exists(`${appName}/server.ts`)).toBeTruthy();
expect(tree.exists(`${appName}/tsconfig.server.json`)).toBeTruthy();
expect(readJson(tree, `${appName}/tsconfig.server.json`)).toMatchObject({ expect(readJson(tree, `${appName}/tsconfig.server.json`)).toMatchObject({
include: ['src/remote.d.ts', 'src/main.server.tsx', 'server.ts'], include: ['src/remote.d.ts', 'src/main.server.tsx', 'server.ts'],
}); });

View File

@ -7,8 +7,8 @@ import libraryGenerator from '../library/library';
import storybookConfigurationGenerator from './configuration'; import storybookConfigurationGenerator from './configuration';
// nested code imports graph from the repo, which might have innacurate graph version // nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({ jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'), ...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest createProjectGraphAsync: jest
.fn() .fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })), .mockImplementation(async () => ({ nodes: {}, dependencies: {} })),

View File

@ -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,
});
});
});

View File

@ -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<ReactDependenciesVersions> {
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;
}

View File

@ -1,13 +1,19 @@
export const nxVersion = require('../../package.json').version; export const nxVersion = require('../../package.json').version;
export const reactVersion = '18.3.1'; export const reactVersion = '19.0.0';
export const reactDomVersion = '18.3.1'; export const reactV18Version = '18.3.1';
export const reactIsVersion = '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 swcLoaderVersion = '0.1.15';
export const babelLoaderVersion = '^9.1.2'; export const babelLoaderVersion = '^9.1.2';
export const typesReactVersion = '18.3.1'; export const typesReactV18Version = '18.3.1';
export const typesReactDomVersion = '18.3.0'; export const typesReactVersion = '19.0.0';
export const typesReactIsVersion = '18.3.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 reactViteVersion = '^4.2.0';
export const typesNodeVersion = '18.16.9'; export const typesNodeVersion = '18.16.9';
@ -27,7 +33,8 @@ export const styledJsxVersion = '5.1.2';
export const reactRouterDomVersion = '6.11.2'; export const reactRouterDomVersion = '6.11.2';
export const testingLibraryReactVersion = '15.0.6'; export const testingLibraryReactVersion = '16.1.0';
export const testingLibraryDomVersion = '10.4.0';
export const reduxjsToolkitVersion = '1.9.3'; export const reduxjsToolkitVersion = '1.9.3';
export const reactReduxVersion = '8.0.5'; export const reactReduxVersion = '8.0.5';

View File

@ -6,11 +6,7 @@ export const rspackPluginMinifyVersion = '^0.7.5';
export const rspackPluginReactRefreshVersion = '^1.0.0'; export const rspackPluginReactRefreshVersion = '^1.0.0';
export const lessLoaderVersion = '~11.1.3'; export const lessLoaderVersion = '~11.1.3';
export const reactVersion = '~18.2.0';
export const reactRefreshVersion = '~0.14.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 nestjsCommonVersion = '~9.0.0';
export const nestjsCoreVersion = '~9.0.0'; export const nestjsCoreVersion = '~9.0.0';

View File

@ -11,7 +11,7 @@ export interface Schema {
standaloneConfig?: boolean; standaloneConfig?: boolean;
framework?: string; framework?: string;
packageManager?: PackageManager; packageManager?: PackageManager;
bundler?: 'vite' | 'webpack' | 'rspack' | 'esbuild'; bundler?: 'vite' | 'rsbuild' | 'webpack' | 'rspack' | 'esbuild';
docker?: boolean; docker?: boolean;
nextAppDir?: boolean; nextAppDir?: boolean;
nextSrcDir?: boolean; nextSrcDir?: boolean;

View File

@ -72,7 +72,7 @@
"bundler": { "bundler": {
"description": "The bundler to use for building the application.", "description": "The bundler to use for building the application.",
"type": "string", "type": "string",
"enum": ["webpack", "vite", "rspack", "esbuild"], "enum": ["vite", "rspack", "rsbuild", "esbuild", "webpack"],
"default": "vite" "default": "vite"
}, },
"docker": { "docker": {