diff --git a/docs/generated/packages/react/generators/application.json b/docs/generated/packages/react/generators/application.json index 5889181603..ca2d6d8128 100644 --- a/docs/generated/packages/react/generators/application.json +++ b/docs/generated/packages/react/generators/application.json @@ -95,7 +95,7 @@ "bundler": { "description": "The bundler to use.", "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?", "default": "vite", "x-priority": "important" diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index 89a5aedc41..9fa202b6f0 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -69,7 +69,7 @@ "bundler": { "description": "The bundler to use for building the application.", "type": "string", - "enum": ["webpack", "vite", "rspack", "esbuild"], + "enum": ["vite", "rspack", "rsbuild", "esbuild", "webpack"], "default": "vite" }, "docker": { diff --git a/e2e/next/src/__snapshots__/next.test.ts.snap b/e2e/next/src/__snapshots__/next.test.ts.snap index 2149098288..370c433755 100644 --- a/e2e/next/src/__snapshots__/next.test.ts.snap +++ b/e2e/next/src/__snapshots__/next.test.ts.snap @@ -5,7 +5,7 @@ exports[`Next.js Applications next-env.d.ts should remain the same after a build /// // 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 /// // 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. " `; diff --git a/e2e/vite/src/vite-legacy.test.ts b/e2e/vite/src/vite-legacy.test.ts index cc0117dd52..9eb5213a39 100644 --- a/e2e/vite/src/vite-legacy.test.ts +++ b/e2e/vite/src/vite-legacy.test.ts @@ -241,14 +241,14 @@ export default App; const results = runCLI(`build ${app} --buildLibsFromSource=true`); expect(results).toContain('Successfully ran target build for project'); // 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', () => { const results = runCLI(`build ${app} --buildLibsFromSource=false`); expect(results).toContain('Successfully ran target build for project'); // 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', () => { diff --git a/packages/next/package.json b/packages/next/package.json index cb5a5f39ce..df6b4b16e2 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -37,7 +37,7 @@ "dependencies": { "@nx/devkit": "file:../devkit", "@babel/plugin-proposal-decorators": "^7.22.7", - "@svgr/webpack": "^8.0.1", + "@svgr/webpack": "^8.1.0", "copy-webpack-plugin": "^10.2.4", "file-loader": "^6.2.0", "ignore": "^5.0.4", diff --git a/packages/next/src/generators/application/application.ts b/packages/next/src/generators/application/application.ts index 160f9eb85f..a99c7f889e 100644 --- a/packages/next/src/generators/application/application.ts +++ b/packages/next/src/generators/application/application.ts @@ -8,11 +8,8 @@ import { } from '@nx/devkit'; import { initGenerator as jsInitGenerator } from '@nx/js'; import { setupTailwindGenerator } from '@nx/react'; -import { - testingLibraryReactVersion, - typesReactDomVersion, - typesReactVersion, -} from '@nx/react/src/utils/versions'; +import { testingLibraryReactVersion } from '@nx/react/src/utils/versions'; +import { getReactDependenciesVersionsToInstall } from '@nx/react/src/utils/version-utils'; import { normalizeOptions } from './lib/normalize-options'; import { Schema } from './schema'; @@ -104,9 +101,10 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { } if (!options.skipPackageJson) { + const reactVersions = await getReactDependenciesVersionsToInstall(host); const devDependencies: Record = { - '@types/react': typesReactVersion, - '@types/react-dom': typesReactDomVersion, + '@types/react': reactVersions['@types/react'], + '@types/react-dom': reactVersions['@types/react-dom'], }; if (options.unitTestRunner && options.unitTestRunner !== 'none') { diff --git a/packages/next/src/generators/application/files/common/next-env.d.ts__tmpl__ b/packages/next/src/generators/application/files/common/next-env.d.ts__tmpl__ index d8b0ee36a6..8c2499693c 100644 --- a/packages/next/src/generators/application/files/common/next-env.d.ts__tmpl__ +++ b/packages/next/src/generators/application/files/common/next-env.d.ts__tmpl__ @@ -2,4 +2,4 @@ /// // 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. diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts index 6d30779272..97af47fe5b 100644 --- a/packages/next/src/generators/application/lib/add-e2e.ts +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -224,12 +224,12 @@ async function getNextE2EWebServerInfo( { plugin: '@nx/next/plugin', serveTargetName: 'devTargetName', - serveStaticTargetName: 'serveStaticTargetName', + serveStaticTargetName: 'startTargetName', configFilePath, }, { defaultServeTargetName: defaultServeTarget, - defaultServeStaticTargetName: 'serve-static', + defaultServeStaticTargetName: 'start', defaultE2EWebServerAddress: `http://127.0.0.1:${e2ePort}`, defaultE2ECiBaseUrl: `http://localhost:${e2ePort}`, defaultE2EPort: e2ePort, diff --git a/packages/next/src/generators/init/init.ts b/packages/next/src/generators/init/init.ts index 8401b29e4d..7ea4872677 100644 --- a/packages/next/src/generators/init/init.ts +++ b/packages/next/src/generators/init/init.ts @@ -8,23 +8,33 @@ import { createProjectGraphAsync, } from '@nx/devkit'; 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 { nextVersion, nxVersion } from '../../utils/versions'; +import { nxVersion } from '../../utils/versions'; +import { getNextDependenciesVersionsToInstall } from '../../utils/version-utils'; import type { InitSchema } from './schema'; -function updateDependencies(host: Tree, schema: InitSchema) { +async function updateDependencies(host: Tree, schema: InitSchema) { const tasks: GeneratorCallback[] = []; tasks.push(removeDependenciesFromPackageJson(host, ['@nx/next'], [])); + const versions = await getNextDependenciesVersionsToInstall( + host, + await isReact18(host) + ); + const reactVersions = await getReactDependenciesVersionsToInstall(host); + tasks.push( addDependenciesToPackageJson( host, { - next: nextVersion, - react: reactVersion, - 'react-dom': reactDomVersion, + next: versions.next, + react: reactVersions.react, + 'react-dom': reactVersions['react-dom'], }, { '@nx/next': nxVersion, @@ -86,7 +96,7 @@ export async function nextInitGeneratorInternal( let installTask: GeneratorCallback = () => {}; if (!schema.skipPackageJson) { - installTask = updateDependencies(host, schema); + installTask = await updateDependencies(host, schema); } return installTask; diff --git a/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap index 9fa42c5386..45f62c3d20 100644 --- a/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap +++ b/packages/next/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -45,12 +45,12 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = ` }, }, "my-serve-static": { - "executor": "@nx/web:file-server", + "command": "next start", + "dependsOn": [ + "my-build", + ], "options": { - "buildTarget": "my-build", - "port": 3000, - "spa": false, - "staticFilePath": "{projectRoot}/out", + "cwd": "my-app", }, }, "my-start": { @@ -121,12 +121,12 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = ` }, }, "serve-static": { - "executor": "@nx/web:file-server", + "command": "next start", + "dependsOn": [ + "build", + ], "options": { - "buildTarget": "build", - "port": 3000, - "spa": false, - "staticFilePath": "{projectRoot}/out", + "cwd": ".", }, }, "start": { diff --git a/packages/next/src/plugins/plugin.ts b/packages/next/src/plugins/plugin.ts index 1f4111909d..5c817dcbdd 100644 --- a/packages/next/src/plugins/plugin.ts +++ b/packages/next/src/plugins/plugin.ts @@ -28,6 +28,9 @@ export interface NextPluginOptions { buildTargetName?: string; devTargetName?: string; startTargetName?: string; + /** + * @deprecated Use `startTargetName` instead. + */ serveStaticTargetName?: string; buildDepsTargetName?: string; watchDepsTargetName?: string; @@ -172,9 +175,11 @@ async function buildNextTargets( 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( context.workspaceRoot, diff --git a/packages/next/src/utils/version-utils.ts b/packages/next/src/utils/version-utils.ts new file mode 100644 index 0000000000..24472a9120 --- /dev/null +++ b/packages/next/src/utils/version-utils.ts @@ -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 { + 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; +} diff --git a/packages/next/src/utils/versions.ts b/packages/next/src/utils/versions.ts index 6b05ebf344..1398b38034 100644 --- a/packages/next/src/utils/versions.ts +++ b/packages/next/src/utils/versions.ts @@ -1,6 +1,7 @@ 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 sassVersion = '1.62.1'; export const lessLoader = '11.1.0'; diff --git a/packages/react/migrations.json b/packages/react/migrations.json index 94f5a3acd2..bf08d839bf 100644 --- a/packages/react/migrations.json +++ b/packages/react/migrations.json @@ -197,6 +197,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 e5d7f0a334..0f8d11a87b 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 aaa13e44e0..33d60bc838 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, @@ -21,6 +20,17 @@ const { load } = require('@zkochan/js-yaml'); // 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', () => { @@ -41,6 +51,7 @@ describe('app', () => { beforeEach(() => { mockedInstalledCypressVersion.mockReturnValue(10); appTree = createTreeWithEmptyWorkspace(); + projectGraph = { dependencies: {}, nodes: {}, externalNodes: {} }; }); 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"` + ); + }); + }); }); diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index 176e18fd51..694aeb9b73 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -153,7 +153,7 @@ export async function applicationGeneratorInternal( // Handle tsconfig.spec.json for jest or vitest updateSpecConfig(tree, options); - const stylePreprocessorTask = installCommonDependencies(tree, options); + const stylePreprocessorTask = await installCommonDependencies(tree, options); tasks.push(stylePreprocessorTask); const styledTask = addStyledModuleDependencies(tree, 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 3f5bd9fa03..f9d12056fb 100644 --- a/packages/react/src/generators/application/lib/install-common-dependencies.ts +++ b/packages/react/src/generators/application/lib/install-common-dependencies.ts @@ -6,14 +6,16 @@ import { sassVersion, swcLoaderVersion, testingLibraryReactVersion, + testingLibraryDomVersion, tsLibVersion, typesNodeVersion, typesReactDomVersion, 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 +23,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') { @@ -58,6 +62,7 @@ export function installCommonDependencies( if (options.unitTestRunner && options.unitTestRunner !== 'none') { devDependencies['@testing-library/react'] = testingLibraryReactVersion; + devDependencies['@testing-library/dom'] = testingLibraryDomVersion; } return addDependenciesToPackageJson(host, {}, devDependencies); diff --git a/packages/react/src/generators/application/schema.json b/packages/react/src/generators/application/schema.json index 7fa75f1588..d384322b53 100644 --- a/packages/react/src/generators/application/schema.json +++ b/packages/react/src/generators/application/schema.json @@ -101,7 +101,7 @@ "bundler": { "description": "The bundler to use.", "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?", "default": "vite", "x-priority": "important" diff --git a/packages/react/src/generators/host/host.rspack.spec.ts b/packages/react/src/generators/host/host.rspack.spec.ts index 0330a9b3ed..2d3cb39bd4 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 7f4a836377..820b644e88 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 105e863d62..ac267d2fd3 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 b400220264..196f1f4287 100644 --- a/packages/react/src/generators/setup-ssr/setup-ssr.spec.ts +++ b/packages/react/src/generators/setup-ssr/setup-ssr.spec.ts @@ -5,6 +5,7 @@ import { ProjectGraph, readCachedProjectGraph, readJson, + readNxJson, Tree, } from '@nx/devkit'; import applicationGenerator from '../application/application'; @@ -12,82 +13,93 @@ import setupSsrGenerator from './setup-ssr'; import { Linter } from '@nx/eslint'; jest.mock('@nx/devkit', () => { + const myAppData = { + root: 'my-app', + sourceRoot: 'my-app/src', + targets: { + build: { + executor: '@nx/webpack:webpack', + outputs: ['{options.outputPath}'], + defaultConfiguration: 'production', + options: { + compiler: 'babel', + outputPath: 'dist/app/my-app', + index: 'my-app/src/index.html', + baseHref: '/', + main: `my-app/src/main.tsx`, + tsConfig: 'my-app/tsconfig.app.json', + assets: ['my-app/src/favicon.ico', 'src/assets'], + styles: [`my-app/src/styles.css`], + scripts: [], + webpackConfig: 'my-app/webpack.config.js', + }, + configurations: { + development: { + extractLicenses: false, + optimization: false, + sourceMap: true, + vendorChunk: true, + }, + production: { + fileReplacements: [ + { + replace: `my-app/src/environments/environment.ts`, + with: `my-app/src/environments/environment.prod.ts`, + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + namedChunks: false, + extractLicenses: true, + vendorChunk: false, + }, + }, + }, + serve: { + executor: '@nx/webpack:dev-server', + defaultConfiguration: 'development', + options: { + buildTarget: `my-app:build`, + hmr: true, + }, + configurations: { + development: { + buildTarget: `my-app:build:development`, + }, + production: { + buildTarget: `my-app:build:production`, + hmr: false, + }, + }, + }, + }, + }; + const pg: ProjectGraph = { + dependencies: {}, + nodes: { + 'my-app': { + name: 'my-app', + type: 'app', + data: { ...myAppData }, + }, + }, + }; const original = jest.requireActual('@nx/devkit'); return { ...original, - readCachedProjectGraph: jest.fn().mockImplementation( - (): ProjectGraph => ({ - dependencies: {}, - nodes: { - 'my-app': { - name: 'my-app', - type: 'app', - data: { - root: 'my-app', - sourceRoot: 'my-app/src', - targets: { - build: { - executor: '@nx/webpack:webpack', - outputs: ['{options.outputPath}'], - defaultConfiguration: 'production', - options: { - compiler: 'babel', - outputPath: 'dist/app/my-app', - index: 'my-app/src/index.html', - baseHref: '/', - main: `my-app/src/main.tsx`, - tsConfig: 'my-app/tsconfig.app.json', - assets: ['my-app/src/favicon.ico', 'src/assets'], - styles: [`my-app/src/styles.css`], - scripts: [], - webpackConfig: 'my-app/webpack.config.js', - }, - configurations: { - development: { - extractLicenses: false, - optimization: false, - sourceMap: true, - vendorChunk: true, - }, - production: { - fileReplacements: [ - { - replace: `my-app/src/environments/environment.ts`, - with: `my-app/src/environments/environment.prod.ts`, - }, - ], - optimization: true, - outputHashing: 'all', - sourceMap: false, - namedChunks: false, - extractLicenses: true, - vendorChunk: false, - }, - }, - }, - serve: { - executor: '@nx/webpack:dev-server', - defaultConfiguration: 'development', - options: { - buildTarget: `my-app:build`, - hmr: true, - }, - configurations: { - development: { - buildTarget: `my-app:build:development`, - }, - production: { - buildTarget: `my-app:build:production`, - hmr: false, - }, - }, - }, - }, - }, - }, - }, - }) - ), + 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(() => { process.env.NX_ADD_PLUGINS = originalEnv; jest.clearAllMocks(); + jest.resetModules(); }); - beforeEach(() => { + it('should add SSR files', async () => { tree = createTreeWithEmptyWorkspace(); - - applicationGenerator(tree, { + await applicationGenerator(tree, { directory: appName, style: 'css', linter: Linter.None, @@ -119,24 +131,14 @@ describe('setupSsrGenerator', () => { e2eTestRunner: 'none', 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, { project: appName, extraInclude: ['src/remote.d.ts'], }); expect(tree.exists(`${appName}/server.ts`)).toBeTruthy(); + expect(tree.exists(`${appName}/tsconfig.server.json`)).toBeTruthy(); expect(readJson(tree, `${appName}/tsconfig.server.json`)).toMatchObject({ include: ['src/remote.d.ts', 'src/main.server.tsx', 'server.ts'], }); diff --git a/packages/react/src/generators/storybook-configuration/configuration.spec.ts b/packages/react/src/generators/storybook-configuration/configuration.spec.ts index 144bd4d8af..474435061e 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 0000000000..f1ba612107 --- /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 0000000000..b45875a705 --- /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 5de406ee21..aa4eb487cd 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,8 @@ 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 testingLibraryDomVersion = '10.4.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 fae45debec..d8675268cb 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'; diff --git a/packages/workspace/src/generators/preset/schema.d.ts b/packages/workspace/src/generators/preset/schema.d.ts index b2a9c23e08..6587a027d7 100644 --- a/packages/workspace/src/generators/preset/schema.d.ts +++ b/packages/workspace/src/generators/preset/schema.d.ts @@ -11,7 +11,7 @@ export interface Schema { standaloneConfig?: boolean; framework?: string; packageManager?: PackageManager; - bundler?: 'vite' | 'webpack' | 'rspack' | 'esbuild'; + bundler?: 'vite' | 'rsbuild' | 'webpack' | 'rspack' | 'esbuild'; docker?: boolean; nextAppDir?: boolean; nextSrcDir?: boolean; diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index e3fdb0fae0..91137092a9 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -72,7 +72,7 @@ "bundler": { "description": "The bundler to use for building the application.", "type": "string", - "enum": ["webpack", "vite", "rspack", "esbuild"], + "enum": ["vite", "rspack", "rsbuild", "esbuild", "webpack"], "default": "vite" }, "docker": {