diff --git a/docs/generated/cli/init.md b/docs/generated/cli/init.md index 8c5cf318a0..b0897add55 100644 --- a/docs/generated/cli/init.md +++ b/docs/generated/cli/init.md @@ -17,10 +17,11 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx` ## Options -| Option | Type | Description | -| ------------------------ | ------- | --------------------------------------------------------------------------------------------------- | -| `--help` | boolean | Show help. | -| `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) | -| `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. | -| `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) | -| `--version` | boolean | Show version number. | +| Option | Type | Description | +| ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `--force` | boolean | Force the migration to continue and ignore custom webpack setup or uncommitted changes. Only for CRA projects. (Default: `false`) | +| `--help` | boolean | Show help. | +| `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) | +| `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. | +| `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) | +| `--version` | boolean | Show version number. | diff --git a/docs/generated/packages/nx/documents/init.md b/docs/generated/packages/nx/documents/init.md index 8c5cf318a0..b0897add55 100644 --- a/docs/generated/packages/nx/documents/init.md +++ b/docs/generated/packages/nx/documents/init.md @@ -17,10 +17,11 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx` ## Options -| Option | Type | Description | -| ------------------------ | ------- | --------------------------------------------------------------------------------------------------- | -| `--help` | boolean | Show help. | -| `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) | -| `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. | -| `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) | -| `--version` | boolean | Show version number. | +| Option | Type | Description | +| ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `--force` | boolean | Force the migration to continue and ignore custom webpack setup or uncommitted changes. Only for CRA projects. (Default: `false`) | +| `--help` | boolean | Show help. | +| `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) | +| `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. | +| `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) | +| `--version` | boolean | Show version number. | diff --git a/e2e/nx-init/src/nx-init-react.test.ts b/e2e/nx-init/src/nx-init-react.test.ts index 017a74094b..ffc428b171 100644 --- a/e2e/nx-init/src/nx-init-react.test.ts +++ b/e2e/nx-init/src/nx-init-react.test.ts @@ -1,6 +1,7 @@ import { checkFilesDoNotExist, checkFilesExist, + createFile, getPackageManagerCommand, getPublishedVersion, getSelectedPackageManager, @@ -19,118 +20,26 @@ import { updateJson, } from '../../utils'; -describe('nx init (for React - legacy)', () => { +describe('nx init (React)', () => { let pmc: ReturnType; beforeAll(() => { pmc = getPackageManagerCommand({ packageManager: getSelectedPackageManager(), }); - - process.env.NX_ADD_PLUGINS = 'false'; }); - afterAll(() => { - delete process.env.NX_ADD_PLUGINS; - }); - - // TODO(@jaysoo): Please investigate why this test is failing - xit('should convert to an integrated workspace with craco (webpack)', () => { + it('should convert a CRA project to Vite', () => { const appName = 'my-app'; createReactApp(appName); const craToNxOutput = runCommand( `${ pmc.runUninstalledPackage - } nx@${getPublishedVersion()} init --no-interactive --integrated --vite=false` + } nx@${getPublishedVersion()} init --no-interactive` ); - expect(craToNxOutput).toContain('๐ŸŽ‰ Done!'); - - const packageJson = readJson('package.json'); - expect(packageJson.devDependencies['@nx/jest']).toBeDefined(); - expect(packageJson.devDependencies['@nx/vite']).toBeUndefined(); - expect(packageJson.devDependencies['@nx/webpack']).toBeDefined(); - expect(packageJson.dependencies['redux']).toBeDefined(); - expect(packageJson.name).toEqual(appName); - - runCLI(`build ${appName}`, { - env: { - // since craco 7.1.0 the NODE_ENV is used, since the tests set it - // to "test" is causes an issue with React Refresh Babel - NODE_ENV: undefined, - }, - }); - checkFilesExist(`dist/apps/${appName}/index.html`); - }); - - // TODO(crystal, @jaysoo): Investigate why this is failing - xit('should convert to an integrated workspace with Vite', () => { - // TODO investigate why this is broken - const originalPM = process.env.SELECTED_PM; - process.env.SELECTED_PM = originalPM === 'pnpm' ? 'yarn' : originalPM; - - const appName = 'my-app'; - createReactApp(appName); - - const craToNxOutput = runCommand( - `${ - pmc.runUninstalledPackage - } nx@${getPublishedVersion()} init --no-interactive --integrated` - ); - - expect(craToNxOutput).toContain('๐ŸŽ‰ Done!'); - - const packageJson = readJson('package.json'); - expect(packageJson.devDependencies['@nx/jest']).toBeUndefined(); - expect(packageJson.devDependencies['@nx/vite']).toBeDefined(); - expect(packageJson.devDependencies['@nx/webpack']).toBeUndefined(); - - const viteConfig = readFile(`apps/${appName}/vite.config.js`); - expect(viteConfig).toContain('port: 4200'); // default port - - runCLI(`build ${appName}`); - checkFilesExist(`dist/apps/${appName}/index.html`); - - const unitTestsOutput = runCLI(`test ${appName}`); - expect(unitTestsOutput).toContain('Successfully ran target test'); - process.env.SELECTED_PM = originalPM; - }); - - // TODO(crystal, @jaysoo): Investigate why this is failing - xit('should convert to an integrated workspace with Vite with custom port', () => { - // TODO investigate why this is broken - const originalPM = process.env.SELECTED_PM; - process.env.SELECTED_PM = originalPM === 'pnpm' ? 'yarn' : originalPM; - const appName = 'my-app'; - createReactApp(appName); - updateFile(`.env`, `NOT_THE_PORT=8000\nPORT=3000\nSOMETHING_ELSE=whatever`); - - runCommand( - `${ - pmc.runUninstalledPackage - } nx@${getPublishedVersion()} init --no-interactive --force --integrated` - ); - - const viteConfig = readFile(`apps/${appName}/vite.config.js`); - expect(viteConfig).toContain('port: 3000'); - - const unitTestsOutput = runCLI(`test ${appName}`); - expect(unitTestsOutput).toContain('Successfully ran target test'); - process.env.SELECTED_PM = originalPM; - }); - - it('should convert to an standalone workspace with Vite', () => { - const appName = 'my-app'; - createReactApp(appName); - - const craToNxOutput = runCommand( - `${ - pmc.runUninstalledPackage - } nx@${getPublishedVersion()} init --no-interactive --vite` - ); - - expect(craToNxOutput).toContain('๐ŸŽ‰ Done!'); + expect(craToNxOutput).toContain('Done!'); checkFilesDoNotExist( 'libs/.gitkeep', @@ -144,8 +53,8 @@ describe('nx init (for React - legacy)', () => { expect(packageJson.dependencies['redux']).toBeDefined(); expect(packageJson.name).toEqual(appName); - const viteConfig = readFile(`vite.config.js`); - expect(viteConfig).toContain('port: 4200'); // default port + const viteConfig = readFile(`vite.config.mjs`); + expect(viteConfig).toContain('port: 3000'); // default port runCLI(`build ${appName}`); checkFilesExist(`dist/${appName}/index.html`); @@ -154,6 +63,41 @@ describe('nx init (for React - legacy)', () => { expect(unitTestsOutput).toContain('Successfully ran target test'); }); + it('should support path aliases', () => { + const appName = 'my-app'; + createReactApp(appName); + createFile( + 'jsconfig.json', + JSON.stringify({ + compilerOptions: { + baseUrl: '.', + paths: { + 'foo/*': ['src/foo/*'], + }, + }, + }) + ); + createFile('src/foo/Foo.js', `export const Foo = () =>

Foo

;`); + updateFile( + 'src/App.js', + ` + import { Foo } from 'foo/Foo'; + function App() { + return ; + } + export default App; + ` + ); + + runCommand( + `${ + pmc.runUninstalledPackage + } nx@${getPublishedVersion()} init --no-interactive` + ); + + expect(() => runCLI(`build ${appName}`)).not.toThrow(); + }); + function createReactApp(appName: string) { createNonNxProjectDirectory(); const projPath = tmpProjPath(); diff --git a/packages/nx/src/command-line/init/command-object.ts b/packages/nx/src/command-line/init/command-object.ts index 726718f895..fea648a0cf 100644 --- a/packages/nx/src/command-line/init/command-object.ts +++ b/packages/nx/src/command-line/init/command-object.ts @@ -49,6 +49,12 @@ async function withInitOptions(yargs: Argv) { description: 'Initialize an Nx workspace setup in the .nx directory of the current repository.', default: false, + }) + .option('force', { + describe: + 'Force the migration to continue and ignore custom webpack setup or uncommitted changes. Only for CRA projects.', + type: 'boolean', + default: false, }); } else { return yargs diff --git a/packages/nx/src/command-line/init/implementation/react/add-vite-commands-to-package-scripts.ts b/packages/nx/src/command-line/init/implementation/react/add-vite-commands-to-package-scripts.ts index d374b292ad..cbde1c5862 100644 --- a/packages/nx/src/command-line/init/implementation/react/add-vite-commands-to-package-scripts.ts +++ b/packages/nx/src/command-line/init/implementation/react/add-vite-commands-to-package-scripts.ts @@ -10,10 +10,12 @@ export function addViteCommandsToPackageScripts( const packageJson = readJsonFile(packageJsonPath); packageJson.scripts = { ...packageJson.scripts, - start: 'nx exec -- vite', - serve: 'nx exec -- vite', - build: `nx exec -- vite build`, - test: 'nx exec -- vitest', + // These should be replaced by the vite init generator later. + start: 'vite', + test: 'vitest', + dev: 'vite', + build: 'vite build', + eject: undefined, }; writeJsonFile(packageJsonPath, packageJson, { spaces: 2 }); } diff --git a/packages/nx/src/command-line/init/implementation/react/index.ts b/packages/nx/src/command-line/init/implementation/react/index.ts index 4b106b7e25..6aaa85caed 100644 --- a/packages/nx/src/command-line/init/implementation/react/index.ts +++ b/packages/nx/src/command-line/init/implementation/react/index.ts @@ -1,29 +1,20 @@ import { execSync } from 'child_process'; -import { cpSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs'; -import { dirname, join } from 'path'; +import { join } from 'path'; +import { appendFileSync } from 'fs'; + import { InitArgs } from '../../init-v1'; -import { - fileExists, - readJsonFile, - writeJsonFile, -} from '../../../../utils/fileutils'; +import { fileExists } from '../../../../utils/fileutils'; import { output } from '../../../../utils/output'; import { detectPackageManager, getPackageManagerCommand, PackageManagerCommands, } from '../../../../utils/package-manager'; -import { PackageJson } from '../../../../utils/package-json'; import { checkForCustomWebpackSetup } from './check-for-custom-webpack-setup'; -import { checkForUncommittedChanges } from './check-for-uncommitted-changes'; -import { cleanUpFiles } from './clean-up-files'; import { readNameFromPackageJson } from './read-name-from-package-json'; import { renameJsToJsx } from './rename-js-to-jsx'; -import { setupTsConfig } from './tsconfig-setup'; -import { writeCracoConfig } from './write-craco-config'; import { writeViteConfig } from './write-vite-config'; import { writeViteIndexHtml } from './write-vite-index-html'; -import { connectExistingRepoToNxCloudPrompt } from '../../../connect/connect-to-nx-cloud'; type Options = InitArgs; @@ -32,41 +23,52 @@ type NormalizedOptions = Options & { pmc: PackageManagerCommands; appIsJs: boolean; reactAppName: string; - isCRA5: boolean; - npxYesFlagNeeded: boolean; - isVite: boolean; isStandalone: boolean; }; -export async function addNxToCraRepo(options: Options) { - if (!options.force) { - checkForUncommittedChanges(); +export async function addNxToCraRepo(_options: Options) { + if (!_options.force) { checkForCustomWebpackSetup(); } - output.log({ title: '๐Ÿณ Nx initialization' }); + const options = await normalizeOptions(_options); - const normalizedOptions = await normalizeOptions(options); - await reorgnizeWorkspaceStructure(normalizedOptions); + await addBundler(options); + + appendFileSync(`.gitignore`, '\nnode_modules'); + appendFileSync(`.gitignore`, '\ndist'); + + installDependencies(options); + + // Vite expects index.html to be in the root as the main entry point. + const indexPath = options.isStandalone + ? 'index.html' + : join('apps', options.reactAppName, 'index.html'); + const oldIndexPath = options.isStandalone + ? join('public', 'index.html') + : join('apps', options.reactAppName, 'public', 'index.html'); + output.note({ + title: `A new ${indexPath} has been created. Compare it to the previous ${oldIndexPath} file and make any changes needed, then delete the previous file.`, + }); + + if (_options.force) { + output.note({ + title: `Using --force converts projects with custom Webpack setup. You will need to manually update your vite.config.js file to match the plugins used in your old Webpack configuration.`, + }); + } } function installDependencies(options: NormalizedOptions) { const dependencies = [ + '@rollup/plugin-replace', '@testing-library/jest-dom', + '@vitejs/plugin-react', 'eslint-config-react-app', 'web-vitals', 'jest-watch-typeahead', + 'vite', + 'vitest', ]; - if (options.isVite) { - dependencies.push('vite', 'vitest', '@vitejs/plugin-react'); - } else { - dependencies.push( - '@craco/craco', - 'cross-env', - 'react-scripts', - 'tsconfig-paths-webpack-plugin' - ); - } execSync(`${options.pmc.addDev} ${dependencies.join(' ')}`, { stdio: [0, 1, 2], @@ -77,282 +79,29 @@ function installDependencies(options: NormalizedOptions) { async function normalizeOptions(options: Options): Promise { const packageManager = detectPackageManager(); const pmc = getPackageManagerCommand(packageManager); - const appIsJs = !fileExists(`tsconfig.json`); - const reactAppName = readNameFromPackageJson(); - const packageJson = readJsonFile(join(process.cwd(), 'package.json')); - const deps = { - ...packageJson.dependencies, - ...packageJson.devDependencies, - }; - const isCRA5 = /^[^~]?5/.test(deps['react-scripts']); - const npmVersion = execSync('npm -v', { - windowsHide: false, - }).toString(); - // Should remove this check 04/2023 once Node 14 & npm 6 reach EOL - const npxYesFlagNeeded = !npmVersion.startsWith('6'); // npm 7 added -y flag to npx - const isVite = options.vite; const isStandalone = !options.integrated; - - const nxCloud = - options.nxCloud ?? - (options.interactive ? await connectExistingRepoToNxCloudPrompt() : false); - return { ...options, - nxCloud, packageManager, pmc, appIsJs, reactAppName, - isCRA5, - npxYesFlagNeeded, - isVite, isStandalone, }; } -/** - * - Create a temp workspace - * - Move all files to temp workspace - * - Add bundler to temp workspace - * - Move files back to root - * - Clean up unused files - */ -async function reorgnizeWorkspaceStructure(options: NormalizedOptions) { - createTempWorkspace(options); - - moveFilesToTempWorkspace(options); - - await addBundler(options); - - output.log({ title: '๐Ÿงถ Updating .gitignore file' }); - - execSync(`echo "node_modules" >> .gitignore`, { - stdio: [0, 1, 2], - windowsHide: false, - }); - execSync(`echo "dist" >> .gitignore`, { - stdio: [0, 1, 2], - windowsHide: false, - }); - - process.chdir('..'); - - copyFromTempWorkspaceToRoot(); - - cleanUpUnusedFilesAndAddConfigFiles(options); - - output.log({ title: '๐Ÿ™‚ Please be patient, one final step remaining!' }); - - output.log({ title: '๐Ÿ“ฆ Installing dependencies' }); - installDependencies(options); - - if (options.isVite) { - const indexPath = options.isStandalone - ? 'index.html' - : join('apps', options.reactAppName, 'index.html'); - const oldIndexPath = options.isStandalone - ? join('public', 'index.html') - : join('apps', options.reactAppName, 'public', 'index.html'); - output.note({ - title: `A new ${indexPath} has been created. Compare it to the previous ${oldIndexPath} file and make any changes needed, then delete the previous file.`, - }); - } -} - -function createTempWorkspace(options: NormalizedOptions) { - rmSync('temp-workspace', { recursive: true, force: true }); - - execSync( - `npx ${ - options.npxYesFlagNeeded ? '-y' : '' - } create-nx-workspace@latest temp-workspace --appName=${ - options.reactAppName - } --preset=react-monorepo --style=css --bundler=${ - options.isVite ? 'vite' : 'webpack' - } --packageManager=${options.packageManager} ${ - options.nxCloud ? '--nxCloud=yes' : '--nxCloud=skip' - } ${ - options.addE2e ? '--e2eTestRunner=playwright' : '--e2eTestRunner=none' - }`, - { stdio: [0, 1, 2], windowsHide: false } - ); - - output.log({ title: '๐Ÿ‘‹ Welcome to Nx!' }); - - output.log({ title: '๐Ÿงน Clearing unused files' }); - - cpSync( - join('temp-workspace', 'apps', options.reactAppName, 'project.json'), - 'project.json', - { recursive: true } - ); - rmSync(join('temp-workspace', 'apps', options.reactAppName), { - recursive: true, - force: true, - }); - rmSync('node_modules', { recursive: true, force: true }); -} - -function copyPackageJsonDepsFromTempWorkspace() { - const repoRoot = process.cwd(); - let rootPackageJson = readJsonFile(join(repoRoot, 'package.json')); - const tempWorkspacePackageJson = readJsonFile( - join(repoRoot, 'temp-workspace', 'package.json') - ); - - rootPackageJson = overridePackageDeps( - 'dependencies', - rootPackageJson, - tempWorkspacePackageJson - ); - rootPackageJson = overridePackageDeps( - 'devDependencies', - rootPackageJson, - tempWorkspacePackageJson - ); - rootPackageJson.scripts = {}; // remove existing scripts - writeJsonFile(join(repoRoot, 'package.json'), rootPackageJson); - writeJsonFile( - join(repoRoot, 'temp-workspace', 'package.json'), - rootPackageJson - ); -} - -function overridePackageDeps( - depConfigName: 'dependencies' | 'devDependencies', - base: PackageJson, - override: PackageJson -): PackageJson { - if (!base[depConfigName]) { - base[depConfigName] = override[depConfigName]; - return base; - } - const deps = override[depConfigName]; - Object.keys(deps).forEach((dep) => { - if (base.dependencies?.[dep]) { - delete base.dependencies[dep]; - } - if (base.devDependencies?.[dep]) { - delete base.devDependencies[dep]; - } - base[depConfigName][dep] = deps[dep]; - }); - return base; -} - -function moveSync(src: string, dest: string) { - const destParentDir = dirname(dest); - mkdirSync(destParentDir, { recursive: true }); - rmSync(dest, { recursive: true, force: true }); - return renameSync(src, dest); -} - -function moveFilesToTempWorkspace(options: NormalizedOptions) { - output.log({ title: '๐Ÿšš Moving your React app in your new Nx workspace' }); - - copyPackageJsonDepsFromTempWorkspace(); - const requiredCraFiles = [ - 'project.json', - 'package.json', - 'src', - 'public', - options.appIsJs ? null : 'tsconfig.json', - options.packageManager === 'yarn' ? 'yarn.lock' : null, - options.packageManager === 'pnpm' ? 'pnpm-lock.yaml' : null, - options.packageManager === 'npm' ? 'package-lock.json' : null, - options.packageManager === 'bun' ? 'bun.lockb' : null, - ]; - - const optionalCraFiles = ['README.md']; - - const filesToMove = [...requiredCraFiles, ...optionalCraFiles].filter( - Boolean - ); - - filesToMove.forEach((f) => { - try { - moveSync( - f, - options.isStandalone - ? join('temp-workspace', f) - : join('temp-workspace', 'apps', options.reactAppName, f) - ); - } catch (error) { - if (requiredCraFiles.includes(f)) { - throw error; - } - } - }); - - process.chdir('temp-workspace'); -} - async function addBundler(options: NormalizedOptions) { - if (options.isVite) { - output.log({ title: '๐Ÿง‘โ€๐Ÿ”ง Setting up Vite' }); - const { addViteCommandsToPackageScripts } = await import( - './add-vite-commands-to-package-scripts' - ); - addViteCommandsToPackageScripts(options.reactAppName, options.isStandalone); - writeViteConfig( - options.reactAppName, - options.isStandalone, - options.appIsJs - ); - writeViteIndexHtml( - options.reactAppName, - options.isStandalone, - options.appIsJs - ); - await renameJsToJsx(options.reactAppName, options.isStandalone); - } else { - output.log({ title: '๐Ÿง‘โ€๐Ÿ”ง Setting up craco + Webpack' }); - const { addCracoCommandsToPackageScripts } = await import( - './add-craco-commands-to-package-scripts' - ); - addCracoCommandsToPackageScripts( - options.reactAppName, - options.isStandalone - ); - - writeCracoConfig( - options.reactAppName, - options.isCRA5, - options.isStandalone - ); - - output.log({ - title: '๐Ÿ›ฌ Skip CRA preflight check since Nx manages the monorepo', - }); - - execSync(`echo "SKIP_PREFLIGHT_CHECK=true" > .env`, { - stdio: [0, 1, 2], - windowsHide: false, - }); - } -} - -function copyFromTempWorkspaceToRoot() { - output.log({ title: '๐Ÿšš Folder restructuring.' }); - - readdirSync('temp-workspace').forEach((f) => { - moveSync(join('temp-workspace', f), f); - }); -} - -function cleanUpUnusedFilesAndAddConfigFiles(options: NormalizedOptions) { - output.log({ title: '๐Ÿงน Cleaning up.' }); - - cleanUpFiles(options.reactAppName, options.isStandalone); - - output.log({ title: "๐Ÿ“ƒ Extend the app's tsconfig.json from the base" }); - - setupTsConfig(options.reactAppName, options.isStandalone); - - if (options.isStandalone) { - rmSync('apps', { recursive: true, force: true }); - } + const { addViteCommandsToPackageScripts } = await import( + './add-vite-commands-to-package-scripts' + ); + addViteCommandsToPackageScripts(options.reactAppName, options.isStandalone); + writeViteConfig(options.reactAppName, options.isStandalone, options.appIsJs); + writeViteIndexHtml( + options.reactAppName, + options.isStandalone, + options.appIsJs + ); + await renameJsToJsx(options.reactAppName, options.isStandalone); } diff --git a/packages/nx/src/command-line/init/implementation/react/write-craco-config.ts b/packages/nx/src/command-line/init/implementation/react/write-craco-config.ts deleted file mode 100644 index cf6194ad36..0000000000 --- a/packages/nx/src/command-line/init/implementation/react/write-craco-config.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { writeFileSync } from 'fs'; - -export function writeCracoConfig( - appName: string, - isCRA5: boolean, - isStandalone: boolean -) { - const configOverride = ` - const path = require('path'); - const TsConfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); - const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); - module.exports = { - webpack: { - configure: (config) => { - // Remove guard against importing modules outside of \`src\`. - // Needed for workspace projects. - config.resolve.plugins = config.resolve.plugins.filter( - (plugin) => !(plugin instanceof ModuleScopePlugin) - ); - // Add support for importing workspace projects. - config.resolve.plugins.push( - new TsConfigPathsPlugin({ - configFile: path.resolve(__dirname, 'tsconfig.json'), - extensions: ['.ts', '.tsx', '.js', '.jsx'], - mainFields: ['browser', 'module', 'main'], - }) - ); - ${ - isCRA5 - ? ` - // Replace include option for babel loader with exclude - // so babel will handle workspace projects as well. - config.module.rules[1].oneOf.forEach((r) => { - if (r.loader && r.loader.indexOf('babel') !== -1) { - r.exclude = /node_modules/; - delete r.include; - } - });` - : ` - // Replace include option for babel loader with exclude - // so babel will handle workspace projects as well. - config.module.rules.forEach((r) => { - if (r.oneOf) { - const babelLoader = r.oneOf.find( - (rr) => rr.loader.indexOf('babel-loader') !== -1 - ); - babelLoader.exclude = /node_modules/; - delete babelLoader.include; - } - }); - ` - } - return config; - }, - }, - jest: { - configure: (config) => { - config.resolver = '@nx/jest/plugins/resolver'; - return config; - }, - }, - }; - `; - writeFileSync( - isStandalone ? 'craco.config.js' : `apps/${appName}/craco.config.js`, - configOverride - ); -} diff --git a/packages/nx/src/command-line/init/implementation/react/write-vite-config.ts b/packages/nx/src/command-line/init/implementation/react/write-vite-config.ts index 476a83a4f0..b41e1934c2 100644 --- a/packages/nx/src/command-line/init/implementation/react/write-vite-config.ts +++ b/packages/nx/src/command-line/init/implementation/react/write-vite-config.ts @@ -5,7 +5,7 @@ export function writeViteConfig( isStandalone: boolean, isJs: boolean ) { - let port = 4200; + let port = 3000; // Use PORT from .env file if it exists in project. if (existsSync(`../.env`)) { @@ -18,9 +18,21 @@ export function writeViteConfig( } writeFileSync( - isStandalone ? 'vite.config.js' : `apps/${appName}/vite.config.js`, + isStandalone ? 'vite.config.mjs' : `apps/${appName}/vite.config.mjs`, `import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import replace from '@rollup/plugin-replace'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +// Match CRA's environment variables. +// TODO: Replace these with VITE_ prefixed environment variables, and using import.meta.env.VITE_* instead of process.env.REACT_APP_*. +const craEnvVarRegex = /^REACT_APP/i; +const craEnvVars = Object.keys(process.env) + .filter((key) => craEnvVarRegex.test(key)) + .reduce((env, key) => { + env[\`process.env.\${key}\`] = JSON.stringify(process.env[key]); + return env; + }, {}); // https://vitejs.dev/config/ export default defineConfig({ @@ -39,7 +51,11 @@ export default defineConfig({ setupFiles: 'src/setupTests.${isJs ? 'js' : 'ts'}', css: true, }, - plugins: [react()], + plugins: [ + react(), + replace({ values: craEnvVars, preventAssignment: true }), + nxViteTsPaths(), + ], }); ` ); diff --git a/packages/nx/src/command-line/init/implementation/utils.ts b/packages/nx/src/command-line/init/implementation/utils.ts index 025c124e95..0851984431 100644 --- a/packages/nx/src/command-line/init/implementation/utils.ts +++ b/packages/nx/src/command-line/init/implementation/utils.ts @@ -3,6 +3,7 @@ import { join } from 'path'; import { NxJsonConfiguration } from '../../../config/nx-json'; import { + directoryExists, fileExists, readJsonFile, writeJsonFile, @@ -336,3 +337,18 @@ export function isMonorepo(packageJson: PackageJson) { return false; } + +export function isCRA(packageJson: PackageJson) { + const combinedDependencies = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + }; + return ( + // Required dependencies for CRA projects + combinedDependencies['react'] && + combinedDependencies['react-dom'] && + combinedDependencies['react-scripts'] && + directoryExists('src') && + directoryExists('public') + ); +} diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index 77fab508d4..7bd438527c 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -8,7 +8,10 @@ import { readJsonFile } from '../../utils/fileutils'; import { getPackageNameFromImportPath } from '../../utils/get-package-name-from-import-path'; import { output } from '../../utils/output'; import { PackageJson } from '../../utils/package-json'; -import { getPackageManagerCommand } from '../../utils/package-manager'; +import { + detectPackageManager, + getPackageManagerCommand, +} from '../../utils/package-manager'; import { nxVersion } from '../../utils/versions'; import { globWithWorkspaceContextSync } from '../../utils/workspace-context'; import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud'; @@ -24,10 +27,12 @@ import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts'; import { createNxJsonFile, initCloud, + isCRA, isMonorepo, printFinalMessage, updateGitIgnore, } from './implementation/utils'; +import { addNxToCraRepo } from './implementation/react'; export interface InitArgs { interactive: boolean; @@ -35,6 +40,7 @@ export interface InitArgs { useDotNxInstallation?: boolean; integrated?: boolean; // For Angular projects only verbose?: boolean; + force?: boolean; } export async function initHandler(options: InitArgs): Promise { @@ -85,6 +91,7 @@ export async function initHandler(options: InitArgs): Promise { const packageJson: PackageJson = readJsonFile('package.json'); const _isTurborepo = existsSync('turbo.json'); const _isMonorepo = isMonorepo(packageJson); + const _isCRA = isCRA(packageJson); const learnMoreLink = _isTurborepo ? 'https://nx.dev/recipes/adopting-nx/from-turborepo' @@ -108,7 +115,18 @@ export async function initHandler(options: InitArgs): Promise { return; } - if (_isMonorepo) { + const pmc = getPackageManagerCommand(); + + if (_isCRA) { + await addNxToCraRepo({ + addE2e: false, + force: options.force, + vite: true, + integrated: false, + interactive: options.interactive, + nxCloud: false, + }); + } else if (_isMonorepo) { await addNxToMonorepo({ interactive: options.interactive, nxCloud: false, @@ -125,7 +143,6 @@ export async function initHandler(options: InitArgs): Promise { (options.interactive ? await connectExistingRepoToNxCloudPrompt() : false); const repoRoot = process.cwd(); - const pmc = getPackageManagerCommand(); createNxJsonFile(repoRoot, [], [], {}); updateGitIgnore(repoRoot); @@ -134,10 +151,18 @@ export async function initHandler(options: InitArgs): Promise { output.log({ title: '๐Ÿง Checking dependencies' }); - const { plugins, updatePackageScripts } = await detectPlugins( - nxJson, - options.interactive - ); + let plugins: string[]; + let updatePackageScripts: boolean; + + if (_isCRA) { + plugins = ['@nx/vite']; + updatePackageScripts = true; + } else { + const { plugins: _plugins, updatePackageScripts: _updatePackageScripts } = + await detectPlugins(nxJson, options.interactive); + plugins = _plugins; + updatePackageScripts = _updatePackageScripts; + } output.log({ title: '๐Ÿ“ฆ Installing Nx' }); diff --git a/packages/vite/plugins/nx-tsconfig-paths.plugin.ts b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts index eb26044db5..3da9c89591 100644 --- a/packages/vite/plugins/nx-tsconfig-paths.plugin.ts +++ b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts @@ -48,6 +48,7 @@ export interface nxViteTsPathsOptions { } export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) { + let foundTsConfigPath: string; let matchTsPathEsm: MatchPath; let matchTsPathFallback: MatchPath | undefined; let tsConfigPathsEsm: ConfigLoaderSuccessResult; @@ -79,7 +80,7 @@ export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) { async configResolved(config: any) { projectRoot = config.root; const projectRootFromWorkspaceRoot = relative(workspaceRoot, projectRoot); - let foundTsConfigPath = getTsConfig( + foundTsConfigPath = getTsConfig( process.env.NX_TSCONFIG_PATH ?? join( workspaceRoot, @@ -89,10 +90,8 @@ export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) { 'tsconfig.generated.json' ) ); - if (!foundTsConfigPath) { - throw new Error(stripIndents`Unable to find a tsconfig in the workspace! -There should at least be a tsconfig.base.json or tsconfig.json in the root of the workspace ${workspaceRoot}`); - } + + if (!foundTsConfigPath) return; if ( !options.buildLibsFromSource && @@ -164,6 +163,9 @@ There should at least be a tsconfig.base.json or tsconfig.json in the root of th } }, resolveId(importPath: string) { + // Let other resolvers handle this path. + if (!foundTsConfigPath) return null; + let resolvedFile: string; try { resolvedFile = matchTsPathEsm(importPath); @@ -211,6 +213,7 @@ There should at least be a tsconfig.base.json or tsconfig.json in the root of th resolve(preferredTsConfigPath), resolve(join(workspaceRoot, 'tsconfig.base.json')), resolve(join(workspaceRoot, 'tsconfig.json')), + resolve(join(workspaceRoot, 'jsconfig.json')), ].find((tsPath) => { if (existsSync(tsPath)) { logIt('Found tsconfig at', tsPath);