import { execSync } from 'child_process'; import { copySync, moveSync, readdirSync, removeSync } from 'fs-extra'; import { fileExists, readJsonFile } from 'nx/src/utils/fileutils'; import { output } from 'nx/src/utils/output'; import { detectPackageManager, getPackageManagerCommand, PackageManagerCommands, } from 'nx/src/utils/package-manager'; import { checkForUncommittedChanges } from './check-for-uncommitted-changes'; import { setupE2eProject } from './setup-e2e-project'; import { readNameFromPackageJson } from './read-name-from-package-json'; import { setupTsConfig } from './tsconfig-setup'; import { writeCracoConfig } from './write-craco-config'; import { cleanUpFiles } from './clean-up-files'; import { writeViteConfig } from './write-vite-config'; import { renameJsToJsx } from './rename-js-to-jsx'; import { writeViteIndexHtml } from './write-vite-index-html'; import { checkForCustomWebpackSetup } from './check-for-custom-webpack-setup'; export interface Options { force: boolean; e2e: boolean; nxCloud: boolean; vite: boolean; integrated: boolean; } interface NormalizedOptions extends Options { packageManager: string; pmc: PackageManagerCommands; appIsJs: boolean; reactAppName: string; isCRA5: boolean; npxYesFlagNeeded: boolean; isVite: boolean; isNested: boolean; } function addDependencies(pmc: PackageManagerCommands, ...deps: string[]) { const depsArg = deps.join(' '); output.log({ title: `๐Ÿ“ฆ Adding dependencies: ${depsArg}` }); execSync(`${pmc.addDev} ${depsArg}`, { stdio: [0, 1, 2] }); } export function normalizeOptions(options: Options): NormalizedOptions { const packageManager = detectPackageManager(); const pmc = getPackageManagerCommand(packageManager); const appIsJs = !fileExists(`tsconfig.json`); const reactAppName = readNameFromPackageJson(); const packageJson = readJsonFile('package.json'); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies, }; const isCRA5 = /^[^~]?5/.test(deps['react-scripts']); const npmVersion = execSync('npm -v').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 isNested = !options.integrated; return { ...options, packageManager, pmc, appIsJs, reactAppName, isCRA5, npxYesFlagNeeded, isVite, isNested, }; } export async function createNxWorkspaceForReact(options: Record) { if (!options.force) { checkForUncommittedChanges(); checkForCustomWebpackSetup(); } output.log({ title: 'โœจ Nx initialization' }); const normalizedOptions = normalizeOptions(options as Options); reorgnizeWorkspaceStructure(normalizedOptions); } async function reorgnizeWorkspaceStructure(options: NormalizedOptions) { createTempWorkspace(options); moveFilesToTempWorkspace(options); await addBundler(options); output.log({ title: '๐Ÿงถ Add all node_modules to .gitignore' }); execSync(`echo "node_modules" >> .gitignore`, { stdio: [0, 1, 2] }); process.chdir('../'); copyFromTempWorkspaceToRoot(); cleanUpUnusedFilesAndAddConfigFiles(options); output.log({ title: '๐Ÿ™‚ Please be patient, one final step remaining!' }); output.log({ title: '๐Ÿงถ Adding npm packages to your new Nx workspace', }); addDependencies( options.pmc, '@testing-library/jest-dom', 'eslint-config-react-app', 'web-vitals', 'jest-watch-typeahead' ); if (options.isVite) { addDependencies(options.pmc, 'vite', 'vitest', '@vitejs/plugin-react'); } else { addDependencies(options.pmc, '@craco/craco', 'cross-env', 'react-scripts'); } output.log({ title: '๐ŸŽ‰ Done!' }); output.note({ title: 'First time using Nx? Check out this interactive Nx tutorial.', bodyLines: [ `https://nx.dev/react-tutorial/1-code-generation`, ` `, `Prefer watching videos? Check out this free Nx course on Egghead.io.`, `https://egghead.io/playlists/scale-react-development-with-nx-4038`, ], }); if (options.isVite) { const indexPath = options.isNested ? 'index.html' : `apps/${options.reactAppName}/index.html`; const oldIndexPath = options.isNested ? 'public/index.html' : `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.`, }); } output.note({ title: 'Or, you can try the commands!', bodyLines: [ `npx nx serve ${options.reactAppName}`, `npx nx build ${options.reactAppName}`, `npx nx test ${options.reactAppName}`, ` `, `https://nx.dev/getting-started/intro#10-try-the-commands`, ], }); } function createTempWorkspace(options: NormalizedOptions) { execSync( `npx ${ options.npxYesFlagNeeded ? '-y' : '' } create-nx-workspace@latest temp-workspace --appName=${ options.reactAppName } --preset=react --style=css --packageManager=${options.packageManager} ${ options.nxCloud ? '--nxCloud' : '--nxCloud=false' }`, { stdio: [0, 1, 2] } ); output.log({ title: '๐Ÿ‘‹ Welcome to Nx!' }); output.log({ title: '๐Ÿงน Clearing unused files' }); copySync( `temp-workspace/apps/${options.reactAppName}/project.json`, 'project.json' ); removeSync(`temp-workspace/apps/${options.reactAppName}/`); removeSync('node_modules'); } function moveFilesToTempWorkspace(options: NormalizedOptions) { output.log({ title: '๐Ÿšš Moving your React app in your new Nx workspace' }); const requiredCraFiles = [ 'project.json', options.isNested ? null : '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, ]; const optionalCraFiles = ['README.md']; const filesToMove = [...requiredCraFiles, ...optionalCraFiles].filter( Boolean ); filesToMove.forEach((f) => { try { moveSync( f, options.isNested ? `temp-workspace/${f}` : `temp-workspace/apps/${options.reactAppName}/${f}`, { overwrite: true, } ); } 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.isNested); writeViteConfig(options.reactAppName, options.isNested, options.appIsJs); writeViteIndexHtml(options.reactAppName, options.isNested, options.appIsJs); renameJsToJsx(options.reactAppName, options.isNested); } else { output.log({ title: '๐Ÿง‘โ€๐Ÿ”ง Setting up craco + Webpack' }); const { addCracoCommandsToPackageScripts } = await import( './add-craco-commands-to-package-scripts' ); addCracoCommandsToPackageScripts(options.reactAppName, options.isNested); writeCracoConfig(options.reactAppName, options.isCRA5, options.isNested); output.log({ title: '๐Ÿ›ฌ Skip CRA preflight check since Nx manages the monorepo', }); execSync(`echo "SKIP_PREFLIGHT_CHECK=true" > .env`, { stdio: [0, 1, 2] }); } } function copyFromTempWorkspaceToRoot() { output.log({ title: '๐Ÿšš Folder restructuring.' }); readdirSync('./temp-workspace').forEach((f) => { moveSync(`temp-workspace/${f}`, `./${f}`, { overwrite: true }); }); } function cleanUpUnusedFilesAndAddConfigFiles(options: NormalizedOptions) { output.log({ title: '๐Ÿงน Cleaning up.' }); cleanUpFiles(options.reactAppName, options.isNested); output.log({ title: "๐Ÿ“ƒ Extend the app's tsconfig.json from the base" }); setupTsConfig(options.reactAppName, options.isNested); if (options.e2e && !options.isNested) { output.log({ title: '๐Ÿ“ƒ Setup e2e tests' }); setupE2eProject(options.reactAppName); } else { removeSync(`apps/${options.reactAppName}-e2e`); execSync(`${options.pmc.rm} @nrwl/cypress eslint-plugin-cypress`); } if (options.isNested) { removeSync('apps'); } }