fix(core): re-enable CRA migration to Vite (#30082)

This PR brings back the CRA migration that was missing since Nx 18.

## Current Behavior
<!-- This is the behavior we have today -->
`nx init` does not migrate CRA apps 

## Expected Behavior
`nx init` migrates CRA apps to Vite since CRA is deprecated

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Jack Hsu 2025-02-25 14:08:13 -05:00 committed by GitHub
parent 32341d5efc
commit 202b49bdbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 190 additions and 495 deletions

View File

@ -17,10 +17,11 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
## Options ## Options
| Option | Type | Description | | Option | Type | Description |
| ------------------------ | ------- | --------------------------------------------------------------------------------------------------- | | ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--help` | boolean | Show help. | | `--force` | boolean | Force the migration to continue and ignore custom webpack setup or uncommitted changes. Only for CRA projects. (Default: `false`) |
| `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) | | `--help` | boolean | Show help. |
| `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. | | `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) |
| `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) | | `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. |
| `--version` | boolean | Show version number. | | `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) |
| `--version` | boolean | Show version number. |

View File

@ -17,10 +17,11 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
## Options ## Options
| Option | Type | Description | | Option | Type | Description |
| ------------------------ | ------- | --------------------------------------------------------------------------------------------------- | | ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--help` | boolean | Show help. | | `--force` | boolean | Force the migration to continue and ignore custom webpack setup or uncommitted changes. Only for CRA projects. (Default: `false`) |
| `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) | | `--help` | boolean | Show help. |
| `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. | | `--interactive` | boolean | When false disables interactive input prompts for options. (Default: `true`) |
| `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) | | `--nxCloud` | boolean | Set up distributed caching with Nx Cloud. |
| `--version` | boolean | Show version number. | | `--useDotNxInstallation` | boolean | Initialize an Nx workspace setup in the .nx directory of the current repository. (Default: `false`) |
| `--version` | boolean | Show version number. |

View File

@ -1,6 +1,7 @@
import { import {
checkFilesDoNotExist, checkFilesDoNotExist,
checkFilesExist, checkFilesExist,
createFile,
getPackageManagerCommand, getPackageManagerCommand,
getPublishedVersion, getPublishedVersion,
getSelectedPackageManager, getSelectedPackageManager,
@ -19,118 +20,26 @@ import {
updateJson, updateJson,
} from '../../utils'; } from '../../utils';
describe('nx init (for React - legacy)', () => { describe('nx init (React)', () => {
let pmc: ReturnType<typeof getPackageManagerCommand>; let pmc: ReturnType<typeof getPackageManagerCommand>;
beforeAll(() => { beforeAll(() => {
pmc = getPackageManagerCommand({ pmc = getPackageManagerCommand({
packageManager: getSelectedPackageManager(), packageManager: getSelectedPackageManager(),
}); });
process.env.NX_ADD_PLUGINS = 'false';
}); });
afterAll(() => { it('should convert a CRA project to Vite', () => {
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)', () => {
const appName = 'my-app'; const appName = 'my-app';
createReactApp(appName); createReactApp(appName);
const craToNxOutput = runCommand( const craToNxOutput = runCommand(
`${ `${
pmc.runUninstalledPackage pmc.runUninstalledPackage
} nx@${getPublishedVersion()} init --no-interactive --integrated --vite=false` } nx@${getPublishedVersion()} init --no-interactive`
); );
expect(craToNxOutput).toContain('🎉 Done!'); 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!');
checkFilesDoNotExist( checkFilesDoNotExist(
'libs/.gitkeep', 'libs/.gitkeep',
@ -144,8 +53,8 @@ describe('nx init (for React - legacy)', () => {
expect(packageJson.dependencies['redux']).toBeDefined(); expect(packageJson.dependencies['redux']).toBeDefined();
expect(packageJson.name).toEqual(appName); expect(packageJson.name).toEqual(appName);
const viteConfig = readFile(`vite.config.js`); const viteConfig = readFile(`vite.config.mjs`);
expect(viteConfig).toContain('port: 4200'); // default port expect(viteConfig).toContain('port: 3000'); // default port
runCLI(`build ${appName}`); runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}/index.html`); checkFilesExist(`dist/${appName}/index.html`);
@ -154,6 +63,41 @@ describe('nx init (for React - legacy)', () => {
expect(unitTestsOutput).toContain('Successfully ran target test'); 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 = () => <p>Foo</p>;`);
updateFile(
'src/App.js',
`
import { Foo } from 'foo/Foo';
function App() {
return <Foo />;
}
export default App;
`
);
runCommand(
`${
pmc.runUninstalledPackage
} nx@${getPublishedVersion()} init --no-interactive`
);
expect(() => runCLI(`build ${appName}`)).not.toThrow();
});
function createReactApp(appName: string) { function createReactApp(appName: string) {
createNonNxProjectDirectory(); createNonNxProjectDirectory();
const projPath = tmpProjPath(); const projPath = tmpProjPath();

View File

@ -49,6 +49,12 @@ async function withInitOptions(yargs: Argv) {
description: description:
'Initialize an Nx workspace setup in the .nx directory of the current repository.', 'Initialize an Nx workspace setup in the .nx directory of the current repository.',
default: false, 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 { } else {
return yargs return yargs

View File

@ -10,10 +10,12 @@ export function addViteCommandsToPackageScripts(
const packageJson = readJsonFile(packageJsonPath); const packageJson = readJsonFile(packageJsonPath);
packageJson.scripts = { packageJson.scripts = {
...packageJson.scripts, ...packageJson.scripts,
start: 'nx exec -- vite', // These should be replaced by the vite init generator later.
serve: 'nx exec -- vite', start: 'vite',
build: `nx exec -- vite build`, test: 'vitest',
test: 'nx exec -- vitest', dev: 'vite',
build: 'vite build',
eject: undefined,
}; };
writeJsonFile(packageJsonPath, packageJson, { spaces: 2 }); writeJsonFile(packageJsonPath, packageJson, { spaces: 2 });
} }

View File

@ -1,29 +1,20 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { cpSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs'; import { join } from 'path';
import { dirname, join } from 'path'; import { appendFileSync } from 'fs';
import { InitArgs } from '../../init-v1'; import { InitArgs } from '../../init-v1';
import { import { fileExists } from '../../../../utils/fileutils';
fileExists,
readJsonFile,
writeJsonFile,
} from '../../../../utils/fileutils';
import { output } from '../../../../utils/output'; import { output } from '../../../../utils/output';
import { import {
detectPackageManager, detectPackageManager,
getPackageManagerCommand, getPackageManagerCommand,
PackageManagerCommands, PackageManagerCommands,
} from '../../../../utils/package-manager'; } from '../../../../utils/package-manager';
import { PackageJson } from '../../../../utils/package-json';
import { checkForCustomWebpackSetup } from './check-for-custom-webpack-setup'; 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 { readNameFromPackageJson } from './read-name-from-package-json';
import { renameJsToJsx } from './rename-js-to-jsx'; 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 { writeViteConfig } from './write-vite-config';
import { writeViteIndexHtml } from './write-vite-index-html'; import { writeViteIndexHtml } from './write-vite-index-html';
import { connectExistingRepoToNxCloudPrompt } from '../../../connect/connect-to-nx-cloud';
type Options = InitArgs; type Options = InitArgs;
@ -32,41 +23,52 @@ type NormalizedOptions = Options & {
pmc: PackageManagerCommands; pmc: PackageManagerCommands;
appIsJs: boolean; appIsJs: boolean;
reactAppName: string; reactAppName: string;
isCRA5: boolean;
npxYesFlagNeeded: boolean;
isVite: boolean;
isStandalone: boolean; isStandalone: boolean;
}; };
export async function addNxToCraRepo(options: Options) { export async function addNxToCraRepo(_options: Options) {
if (!options.force) { if (!_options.force) {
checkForUncommittedChanges();
checkForCustomWebpackSetup(); checkForCustomWebpackSetup();
} }
output.log({ title: '🐳 Nx initialization' }); const options = await normalizeOptions(_options);
const normalizedOptions = await normalizeOptions(options); await addBundler(options);
await reorgnizeWorkspaceStructure(normalizedOptions);
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) { function installDependencies(options: NormalizedOptions) {
const dependencies = [ const dependencies = [
'@rollup/plugin-replace',
'@testing-library/jest-dom', '@testing-library/jest-dom',
'@vitejs/plugin-react',
'eslint-config-react-app', 'eslint-config-react-app',
'web-vitals', 'web-vitals',
'jest-watch-typeahead', '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(' ')}`, { execSync(`${options.pmc.addDev} ${dependencies.join(' ')}`, {
stdio: [0, 1, 2], stdio: [0, 1, 2],
@ -77,282 +79,29 @@ function installDependencies(options: NormalizedOptions) {
async function normalizeOptions(options: Options): Promise<NormalizedOptions> { async function normalizeOptions(options: Options): Promise<NormalizedOptions> {
const packageManager = detectPackageManager(); const packageManager = detectPackageManager();
const pmc = getPackageManagerCommand(packageManager); const pmc = getPackageManagerCommand(packageManager);
const appIsJs = !fileExists(`tsconfig.json`); const appIsJs = !fileExists(`tsconfig.json`);
const reactAppName = readNameFromPackageJson(); 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 isStandalone = !options.integrated;
const nxCloud =
options.nxCloud ??
(options.interactive ? await connectExistingRepoToNxCloudPrompt() : false);
return { return {
...options, ...options,
nxCloud,
packageManager, packageManager,
pmc, pmc,
appIsJs, appIsJs,
reactAppName, reactAppName,
isCRA5,
npxYesFlagNeeded,
isVite,
isStandalone, 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) { async function addBundler(options: NormalizedOptions) {
if (options.isVite) { const { addViteCommandsToPackageScripts } = await import(
output.log({ title: '🧑‍🔧 Setting up Vite' }); './add-vite-commands-to-package-scripts'
const { addViteCommandsToPackageScripts } = await import( );
'./add-vite-commands-to-package-scripts' addViteCommandsToPackageScripts(options.reactAppName, options.isStandalone);
); writeViteConfig(options.reactAppName, options.isStandalone, options.appIsJs);
addViteCommandsToPackageScripts(options.reactAppName, options.isStandalone); writeViteIndexHtml(
writeViteConfig( options.reactAppName,
options.reactAppName, options.isStandalone,
options.isStandalone, options.appIsJs
options.appIsJs );
); await renameJsToJsx(options.reactAppName, options.isStandalone);
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 });
}
} }

View File

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

View File

@ -5,7 +5,7 @@ export function writeViteConfig(
isStandalone: boolean, isStandalone: boolean,
isJs: boolean isJs: boolean
) { ) {
let port = 4200; let port = 3000;
// Use PORT from .env file if it exists in project. // Use PORT from .env file if it exists in project.
if (existsSync(`../.env`)) { if (existsSync(`../.env`)) {
@ -18,9 +18,21 @@ export function writeViteConfig(
} }
writeFileSync( writeFileSync(
isStandalone ? 'vite.config.js' : `apps/${appName}/vite.config.js`, isStandalone ? 'vite.config.mjs' : `apps/${appName}/vite.config.mjs`,
`import { defineConfig } from 'vite' `import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' 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/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@ -39,7 +51,11 @@ export default defineConfig({
setupFiles: 'src/setupTests.${isJs ? 'js' : 'ts'}', setupFiles: 'src/setupTests.${isJs ? 'js' : 'ts'}',
css: true, css: true,
}, },
plugins: [react()], plugins: [
react(),
replace({ values: craEnvVars, preventAssignment: true }),
nxViteTsPaths(),
],
}); });
` `
); );

View File

@ -3,6 +3,7 @@ import { join } from 'path';
import { NxJsonConfiguration } from '../../../config/nx-json'; import { NxJsonConfiguration } from '../../../config/nx-json';
import { import {
directoryExists,
fileExists, fileExists,
readJsonFile, readJsonFile,
writeJsonFile, writeJsonFile,
@ -336,3 +337,18 @@ export function isMonorepo(packageJson: PackageJson) {
return false; 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')
);
}

View File

@ -8,7 +8,10 @@ import { readJsonFile } from '../../utils/fileutils';
import { getPackageNameFromImportPath } from '../../utils/get-package-name-from-import-path'; import { getPackageNameFromImportPath } from '../../utils/get-package-name-from-import-path';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { PackageJson } from '../../utils/package-json'; 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 { nxVersion } from '../../utils/versions';
import { globWithWorkspaceContextSync } from '../../utils/workspace-context'; import { globWithWorkspaceContextSync } from '../../utils/workspace-context';
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud'; import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
@ -24,10 +27,12 @@ import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
import { import {
createNxJsonFile, createNxJsonFile,
initCloud, initCloud,
isCRA,
isMonorepo, isMonorepo,
printFinalMessage, printFinalMessage,
updateGitIgnore, updateGitIgnore,
} from './implementation/utils'; } from './implementation/utils';
import { addNxToCraRepo } from './implementation/react';
export interface InitArgs { export interface InitArgs {
interactive: boolean; interactive: boolean;
@ -35,6 +40,7 @@ export interface InitArgs {
useDotNxInstallation?: boolean; useDotNxInstallation?: boolean;
integrated?: boolean; // For Angular projects only integrated?: boolean; // For Angular projects only
verbose?: boolean; verbose?: boolean;
force?: boolean;
} }
export async function initHandler(options: InitArgs): Promise<void> { export async function initHandler(options: InitArgs): Promise<void> {
@ -85,6 +91,7 @@ export async function initHandler(options: InitArgs): Promise<void> {
const packageJson: PackageJson = readJsonFile('package.json'); const packageJson: PackageJson = readJsonFile('package.json');
const _isTurborepo = existsSync('turbo.json'); const _isTurborepo = existsSync('turbo.json');
const _isMonorepo = isMonorepo(packageJson); const _isMonorepo = isMonorepo(packageJson);
const _isCRA = isCRA(packageJson);
const learnMoreLink = _isTurborepo const learnMoreLink = _isTurborepo
? 'https://nx.dev/recipes/adopting-nx/from-turborepo' ? 'https://nx.dev/recipes/adopting-nx/from-turborepo'
@ -108,7 +115,18 @@ export async function initHandler(options: InitArgs): Promise<void> {
return; 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({ await addNxToMonorepo({
interactive: options.interactive, interactive: options.interactive,
nxCloud: false, nxCloud: false,
@ -125,7 +143,6 @@ export async function initHandler(options: InitArgs): Promise<void> {
(options.interactive ? await connectExistingRepoToNxCloudPrompt() : false); (options.interactive ? await connectExistingRepoToNxCloudPrompt() : false);
const repoRoot = process.cwd(); const repoRoot = process.cwd();
const pmc = getPackageManagerCommand();
createNxJsonFile(repoRoot, [], [], {}); createNxJsonFile(repoRoot, [], [], {});
updateGitIgnore(repoRoot); updateGitIgnore(repoRoot);
@ -134,10 +151,18 @@ export async function initHandler(options: InitArgs): Promise<void> {
output.log({ title: '🧐 Checking dependencies' }); output.log({ title: '🧐 Checking dependencies' });
const { plugins, updatePackageScripts } = await detectPlugins( let plugins: string[];
nxJson, let updatePackageScripts: boolean;
options.interactive
); 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' }); output.log({ title: '📦 Installing Nx' });

View File

@ -48,6 +48,7 @@ export interface nxViteTsPathsOptions {
} }
export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) { export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) {
let foundTsConfigPath: string;
let matchTsPathEsm: MatchPath; let matchTsPathEsm: MatchPath;
let matchTsPathFallback: MatchPath | undefined; let matchTsPathFallback: MatchPath | undefined;
let tsConfigPathsEsm: ConfigLoaderSuccessResult; let tsConfigPathsEsm: ConfigLoaderSuccessResult;
@ -79,7 +80,7 @@ export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) {
async configResolved(config: any) { async configResolved(config: any) {
projectRoot = config.root; projectRoot = config.root;
const projectRootFromWorkspaceRoot = relative(workspaceRoot, projectRoot); const projectRootFromWorkspaceRoot = relative(workspaceRoot, projectRoot);
let foundTsConfigPath = getTsConfig( foundTsConfigPath = getTsConfig(
process.env.NX_TSCONFIG_PATH ?? process.env.NX_TSCONFIG_PATH ??
join( join(
workspaceRoot, workspaceRoot,
@ -89,10 +90,8 @@ export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) {
'tsconfig.generated.json' 'tsconfig.generated.json'
) )
); );
if (!foundTsConfigPath) {
throw new Error(stripIndents`Unable to find a tsconfig in the workspace! if (!foundTsConfigPath) return;
There should at least be a tsconfig.base.json or tsconfig.json in the root of the workspace ${workspaceRoot}`);
}
if ( if (
!options.buildLibsFromSource && !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) { resolveId(importPath: string) {
// Let other resolvers handle this path.
if (!foundTsConfigPath) return null;
let resolvedFile: string; let resolvedFile: string;
try { try {
resolvedFile = matchTsPathEsm(importPath); 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(preferredTsConfigPath),
resolve(join(workspaceRoot, 'tsconfig.base.json')), resolve(join(workspaceRoot, 'tsconfig.base.json')),
resolve(join(workspaceRoot, 'tsconfig.json')), resolve(join(workspaceRoot, 'tsconfig.json')),
resolve(join(workspaceRoot, 'jsconfig.json')),
].find((tsPath) => { ].find((tsPath) => {
if (existsSync(tsPath)) { if (existsSync(tsPath)) {
logIt('Found tsconfig at', tsPath); logIt('Found tsconfig at', tsPath);