feat(core): support nested structure for cra-to-nx (#13253)
This commit is contained in:
parent
2772fab0e7
commit
590a3dc769
@ -43,7 +43,7 @@ In this article, you’ll learn how to:
|
|||||||
- Convert CRA scripts for use in Nx
|
- Convert CRA scripts for use in Nx
|
||||||
- Create a library and use it in your application
|
- Create a library and use it in your application
|
||||||
|
|
||||||
For this example, you’ll be migrating the default CRA typescript template app into an Nx workspace. This is the code that is generated when you run `yarn create react-app webapp --template typescript`.
|
For this example, you’ll be migrating the default CRA typescript template app into an Nx workspace. This is the code that is generated when you run `npx create-react-app webapp --template typescript`.
|
||||||
|
|
||||||
There is also a [repo](https://github.com/nrwl/cra-to-nx-migration) that shows the finished result of this guide and for each step a [diff](https://github.com/nrwl/cra-to-nx-migration/commits/main) will be provided to see the exact code changes that occur for that step.
|
There is also a [repo](https://github.com/nrwl/cra-to-nx-migration) that shows the finished result of this guide and for each step a [diff](https://github.com/nrwl/cra-to-nx-migration/commits/main) will be provided to see the exact code changes that occur for that step.
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ describe('cra-to-nx', () => {
|
|||||||
const craToNxOutput = runCommand(
|
const craToNxOutput = runCommand(
|
||||||
`${
|
`${
|
||||||
pmc.runUninstalledPackage
|
pmc.runUninstalledPackage
|
||||||
} cra-to-nx@${getPublishedVersion()} --nxCloud=false`
|
} cra-to-nx@${getPublishedVersion()} --nxCloud=false --integrated`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(craToNxOutput).toContain('🎉 Done!');
|
expect(craToNxOutput).toContain('🎉 Done!');
|
||||||
@ -46,7 +46,7 @@ describe('cra-to-nx', () => {
|
|||||||
const craToNxOutput = runCommand(
|
const craToNxOutput = runCommand(
|
||||||
`${
|
`${
|
||||||
pmc.runUninstalledPackage
|
pmc.runUninstalledPackage
|
||||||
} cra-to-nx@${getPublishedVersion()} --nxCloud=false --vite`
|
} cra-to-nx@${getPublishedVersion()} --nxCloud=false --vite --integrated`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(craToNxOutput).toContain('🎉 Done!');
|
expect(craToNxOutput).toContain('🎉 Done!');
|
||||||
@ -56,6 +56,9 @@ describe('cra-to-nx', () => {
|
|||||||
|
|
||||||
runCLI(`build ${appName}`);
|
runCLI(`build ${appName}`);
|
||||||
checkFilesExist(`dist/apps/${appName}/index.html`);
|
checkFilesExist(`dist/apps/${appName}/index.html`);
|
||||||
|
|
||||||
|
const unitTestsOutput = runCLI(`test ${appName}`);
|
||||||
|
expect(unitTestsOutput).toContain('Successfully ran target test');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should convert to an integrated workspace with Vite with custom port', () => {
|
it('should convert to an integrated workspace with Vite with custom port', () => {
|
||||||
@ -66,10 +69,53 @@ describe('cra-to-nx', () => {
|
|||||||
runCommand(
|
runCommand(
|
||||||
`${
|
`${
|
||||||
pmc.runUninstalledPackage
|
pmc.runUninstalledPackage
|
||||||
} cra-to-nx@${getPublishedVersion()} --nxCloud=false --vite --force`
|
} cra-to-nx@${getPublishedVersion()} --nxCloud=false --vite --force --integrated`
|
||||||
);
|
);
|
||||||
|
|
||||||
const viteConfig = readFile(`apps/${appName}/vite.config.js`);
|
const viteConfig = readFile(`apps/${appName}/vite.config.js`);
|
||||||
expect(viteConfig).toContain('port: 3000');
|
expect(viteConfig).toContain('port: 3000');
|
||||||
|
|
||||||
|
const unitTestsOutput = runCLI(`test ${appName}`);
|
||||||
|
expect(unitTestsOutput).toContain('Successfully ran target test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert to a nested workspace with craco (webpack)', () => {
|
||||||
|
const appName = 'my-app';
|
||||||
|
createReactApp(appName);
|
||||||
|
|
||||||
|
const craToNxOutput = runCommand(
|
||||||
|
`${
|
||||||
|
pmc.runUninstalledPackage
|
||||||
|
} cra-to-nx@${getPublishedVersion()} --nxCloud=false`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(craToNxOutput).toContain('🎉 Done!');
|
||||||
|
|
||||||
|
runCLI(`build ${appName}`);
|
||||||
|
checkFilesExist(`public/index.html`, `dist/asset-manifest.json`);
|
||||||
|
const manifest = readJson(`dist/asset-manifest.json`);
|
||||||
|
checkFilesExist(...manifest['entrypoints'].map((f) => `dist/${f}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert to an nested workspace with Vite', () => {
|
||||||
|
const appName = 'my-app';
|
||||||
|
createReactApp(appName);
|
||||||
|
|
||||||
|
const craToNxOutput = runCommand(
|
||||||
|
`${
|
||||||
|
pmc.runUninstalledPackage
|
||||||
|
} cra-to-nx@${getPublishedVersion()} --nxCloud=false --vite`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(craToNxOutput).toContain('🎉 Done!');
|
||||||
|
|
||||||
|
const viteConfig = readFile(`vite.config.js`);
|
||||||
|
expect(viteConfig).toContain('port: 4200'); // default port
|
||||||
|
|
||||||
|
runCLI(`build ${appName}`);
|
||||||
|
checkFilesExist(`dist/index.html`);
|
||||||
|
|
||||||
|
const unitTestsOutput = runCLI(`test ${appName}`);
|
||||||
|
expect(unitTestsOutput).toContain('Successfully ran target test');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,6 +27,11 @@ export const commandsObject = yargs
|
|||||||
describe: 'Use Vite and Vitest (instead of Webpack and Jest)',
|
describe: 'Use Vite and Vitest (instead of Webpack and Jest)',
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
.option('integrated', {
|
||||||
|
type: 'boolean',
|
||||||
|
describe: 'Use integrated folder structure, with apps folder',
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
.help();
|
.help();
|
||||||
|
|
||||||
createNxWorkspaceForReact(commandsObject.argv).catch((e) => {
|
createNxWorkspaceForReact(commandsObject.argv).catch((e) => {
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
import { readJsonFile, writeJsonFile } from 'nx/src/utils/fileutils';
|
import { readJsonFile, writeJsonFile } from 'nx/src/utils/fileutils';
|
||||||
|
|
||||||
export function addCracoCommandsToPackageScripts(appName: string) {
|
export function addCracoCommandsToPackageScripts(
|
||||||
const packageJson = readJsonFile(`apps/${appName}/package.json`);
|
appName: string,
|
||||||
|
isNested: boolean
|
||||||
|
) {
|
||||||
|
const packageJsonPath = isNested
|
||||||
|
? 'package.json'
|
||||||
|
: `apps/${appName}/package.json`;
|
||||||
|
const distPath = isNested ? 'dist' : `../../dist/apps/${appName}`;
|
||||||
|
const packageJson = readJsonFile(packageJsonPath);
|
||||||
packageJson.scripts = {
|
packageJson.scripts = {
|
||||||
...packageJson.scripts,
|
...packageJson.scripts,
|
||||||
start: 'craco start',
|
start: 'craco start',
|
||||||
serve: 'npm start',
|
serve: 'npm start',
|
||||||
build: `cross-env BUILD_PATH=../../dist/apps/${appName} craco build`,
|
build: `cross-env BUILD_PATH=${distPath} craco build`,
|
||||||
test: 'craco test',
|
test: 'craco test',
|
||||||
};
|
};
|
||||||
writeJsonFile(`apps/${appName}/package.json`, packageJson);
|
writeJsonFile(packageJsonPath, packageJson);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { readJsonFile, writeJsonFile } from 'nx/src/utils/fileutils';
|
import { readJsonFile, writeJsonFile } from 'nx/src/utils/fileutils';
|
||||||
|
|
||||||
export function addViteCommandsToPackageScripts(appName: string) {
|
export function addViteCommandsToPackageScripts(
|
||||||
const packageJson = readJsonFile(`apps/${appName}/package.json`);
|
appName: string,
|
||||||
|
isNested: boolean
|
||||||
|
) {
|
||||||
|
const packageJsonPath = isNested
|
||||||
|
? 'package.json'
|
||||||
|
: `apps/${appName}/package.json`;
|
||||||
|
const packageJson = readJsonFile(packageJsonPath);
|
||||||
packageJson.scripts = {
|
packageJson.scripts = {
|
||||||
...packageJson.scripts,
|
...packageJson.scripts,
|
||||||
start: 'vite',
|
start: 'vite',
|
||||||
@ -9,5 +15,5 @@ export function addViteCommandsToPackageScripts(appName: string) {
|
|||||||
build: `vite build`,
|
build: `vite build`,
|
||||||
test: 'vitest',
|
test: 'vitest',
|
||||||
};
|
};
|
||||||
writeJsonFile(`apps/${appName}/package.json`, packageJson, { spaces: 2 });
|
writeJsonFile(packageJsonPath, packageJson, { spaces: 2 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,25 @@
|
|||||||
import { removeSync } from 'fs-extra';
|
import { removeSync } from 'fs-extra';
|
||||||
import { readJsonFile, writeJsonFile } from 'nx/src/utils/fileutils';
|
import { readJsonFile, writeJsonFile } from 'nx/src/utils/fileutils';
|
||||||
|
|
||||||
export function cleanUpFiles(appName: string) {
|
export function cleanUpFiles(appName: string, isNested: boolean) {
|
||||||
// Delete targets from project since we delegate to npm scripts.
|
// Delete targets from project since we delegate to npm scripts.
|
||||||
const json = readJsonFile(`apps/${appName}/project.json`);
|
const projectJsonPath = isNested
|
||||||
|
? 'project.json'
|
||||||
|
: `apps/${appName}/project.json`;
|
||||||
|
const json = readJsonFile(projectJsonPath);
|
||||||
delete json.targets;
|
delete json.targets;
|
||||||
writeJsonFile(`apps/${appName}/project.json`, json);
|
if (isNested) {
|
||||||
|
if (json.sourceRoot) {
|
||||||
|
json.sourceRoot = json.sourceRoot.replace(`apps/${appName}/`, '');
|
||||||
|
}
|
||||||
|
if (json['$schema']) {
|
||||||
|
json['$schema'] = json['$schema'].replace(
|
||||||
|
'../../node_modules',
|
||||||
|
'node_modules'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeJsonFile(projectJsonPath, json);
|
||||||
|
|
||||||
removeSync('temp-workspace');
|
removeSync('temp-workspace');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,27 +20,35 @@ import { renameJsToJsx } from './rename-js-to-jsx';
|
|||||||
import { writeViteIndexHtml } from './write-vite-index-html';
|
import { writeViteIndexHtml } from './write-vite-index-html';
|
||||||
import { checkForCustomWebpackSetup } from './check-for-custom-webpack-setup';
|
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[]) {
|
function addDependencies(pmc: PackageManagerCommands, ...deps: string[]) {
|
||||||
const depsArg = deps.join(' ');
|
const depsArg = deps.join(' ');
|
||||||
output.log({ title: `📦 Adding dependencies: ${depsArg}` });
|
output.log({ title: `📦 Adding dependencies: ${depsArg}` });
|
||||||
execSync(`${pmc.addDev} ${depsArg}`, { stdio: [0, 1, 2] });
|
execSync(`${pmc.addDev} ${depsArg}`, { stdio: [0, 1, 2] });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createNxWorkspaceForReact(options: Record<string, any>) {
|
export function normalizeOptions(options: Options): NormalizedOptions {
|
||||||
if (!options.force) {
|
|
||||||
checkForUncommittedChanges();
|
|
||||||
checkForCustomWebpackSetup();
|
|
||||||
}
|
|
||||||
const packageManager = detectPackageManager();
|
const packageManager = detectPackageManager();
|
||||||
const pmc = getPackageManagerCommand(packageManager);
|
const pmc = getPackageManagerCommand(packageManager);
|
||||||
|
|
||||||
output.log({ title: '✨ Nx initialization' });
|
const appIsJs = !fileExists(`tsconfig.json`);
|
||||||
|
|
||||||
let appIsJs = true;
|
|
||||||
|
|
||||||
if (fileExists(`tsconfig.json`)) {
|
|
||||||
appIsJs = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reactAppName = readNameFromPackageJson();
|
const reactAppName = readNameFromPackageJson();
|
||||||
const packageJson = readJsonFile('package.json');
|
const packageJson = readJsonFile('package.json');
|
||||||
@ -52,82 +60,40 @@ export async function createNxWorkspaceForReact(options: Record<string, any>) {
|
|||||||
const npmVersion = execSync('npm -v').toString();
|
const npmVersion = execSync('npm -v').toString();
|
||||||
// Should remove this check 04/2023 once Node 14 & npm 6 reach EOL
|
// 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 npxYesFlagNeeded = !npmVersion.startsWith('6'); // npm 7 added -y flag to npx
|
||||||
const isVite = options.vite || options.bundler === 'vite';
|
const isVite = options.vite;
|
||||||
|
const isNested = !options.integrated;
|
||||||
|
|
||||||
execSync(
|
return {
|
||||||
`npx ${
|
...options,
|
||||||
npxYesFlagNeeded ? '-y' : ''
|
packageManager,
|
||||||
} create-nx-workspace@latest temp-workspace --appName=${reactAppName} --preset=react --style=css --packageManager=${packageManager} ${
|
pmc,
|
||||||
options.nxCloud ? '--nxCloud' : '--nxCloud=false'
|
appIsJs,
|
||||||
}`,
|
reactAppName,
|
||||||
{ stdio: [0, 1, 2] }
|
isCRA5,
|
||||||
);
|
npxYesFlagNeeded,
|
||||||
|
isVite,
|
||||||
|
isNested,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
output.log({ title: '👋 Welcome to Nx!' });
|
export async function createNxWorkspaceForReact(options: Record<string, any>) {
|
||||||
|
if (!options.force) {
|
||||||
output.log({ title: '🧹 Clearing unused files' });
|
checkForUncommittedChanges();
|
||||||
|
checkForCustomWebpackSetup();
|
||||||
copySync(`temp-workspace/apps/${reactAppName}/project.json`, 'project.json');
|
|
||||||
removeSync(`temp-workspace/apps/${reactAppName}/`);
|
|
||||||
removeSync('node_modules');
|
|
||||||
|
|
||||||
output.log({ title: '🚚 Moving your React app in your new Nx workspace' });
|
|
||||||
|
|
||||||
const requiredCraFiles = [
|
|
||||||
'project.json',
|
|
||||||
'package.json',
|
|
||||||
'src',
|
|
||||||
'public',
|
|
||||||
appIsJs ? null : 'tsconfig.json',
|
|
||||||
packageManager === 'yarn' ? 'yarn.lock' : null,
|
|
||||||
packageManager === 'pnpm' ? 'pnpm-lock.yaml' : null,
|
|
||||||
packageManager === 'npm' ? 'package-lock.json' : null,
|
|
||||||
];
|
|
||||||
|
|
||||||
const optionalCraFiles = ['README.md'];
|
|
||||||
|
|
||||||
const filesToMove = [...requiredCraFiles, ...optionalCraFiles].filter(
|
|
||||||
Boolean
|
|
||||||
);
|
|
||||||
|
|
||||||
filesToMove.forEach((f) => {
|
|
||||||
try {
|
|
||||||
moveSync(f, `temp-workspace/apps/${reactAppName}/${f}`, {
|
|
||||||
overwrite: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (requiredCraFiles.includes(f)) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
process.chdir('temp-workspace/');
|
output.log({ title: '✨ Nx initialization' });
|
||||||
|
|
||||||
if (isVite) {
|
const normalizedOptions = normalizeOptions(options as Options);
|
||||||
output.log({ title: '🧑🔧 Setting up Vite' });
|
reorgnizeWorkspaceStructure(normalizedOptions);
|
||||||
const { addViteCommandsToPackageScripts } = await import(
|
}
|
||||||
'./add-vite-commands-to-package-scripts'
|
|
||||||
);
|
|
||||||
addViteCommandsToPackageScripts(reactAppName);
|
|
||||||
writeViteConfig(reactAppName);
|
|
||||||
writeViteIndexHtml(reactAppName);
|
|
||||||
renameJsToJsx(reactAppName);
|
|
||||||
} else {
|
|
||||||
output.log({ title: '🧑🔧 Setting up craco + Webpack' });
|
|
||||||
const { addCracoCommandsToPackageScripts } = await import(
|
|
||||||
'./add-craco-commands-to-package-scripts'
|
|
||||||
);
|
|
||||||
addCracoCommandsToPackageScripts(reactAppName);
|
|
||||||
|
|
||||||
writeCracoConfig(reactAppName, isCRA5);
|
async function reorgnizeWorkspaceStructure(options: NormalizedOptions) {
|
||||||
|
createTempWorkspace(options);
|
||||||
|
|
||||||
output.log({
|
moveFilesToTempWorkspace(options);
|
||||||
title: '🛬 Skip CRA preflight check since Nx manages the monorepo',
|
|
||||||
});
|
|
||||||
|
|
||||||
execSync(`echo "SKIP_PREFLIGHT_CHECK=true" > .env`, { stdio: [0, 1, 2] });
|
await addBundler(options);
|
||||||
}
|
|
||||||
|
|
||||||
output.log({ title: '🧶 Add all node_modules to .gitignore' });
|
output.log({ title: '🧶 Add all node_modules to .gitignore' });
|
||||||
|
|
||||||
@ -135,27 +101,9 @@ export async function createNxWorkspaceForReact(options: Record<string, any>) {
|
|||||||
|
|
||||||
process.chdir('../');
|
process.chdir('../');
|
||||||
|
|
||||||
output.log({ title: '🚚 Folder restructuring.' });
|
copyFromTempWorkspaceToRoot();
|
||||||
|
|
||||||
readdirSync('./temp-workspace').forEach((f) => {
|
cleanUpUnusedFilesAndAddConfigFiles(options);
|
||||||
moveSync(`temp-workspace/${f}`, `./${f}`, { overwrite: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
output.log({ title: '🧹 Cleaning up.' });
|
|
||||||
|
|
||||||
cleanUpFiles(reactAppName);
|
|
||||||
|
|
||||||
output.log({ title: "📃 Extend the app's tsconfig.json from the base" });
|
|
||||||
|
|
||||||
setupTsConfig(reactAppName);
|
|
||||||
|
|
||||||
if (options.e2e) {
|
|
||||||
output.log({ title: '📃 Setup e2e tests' });
|
|
||||||
setupE2eProject(reactAppName);
|
|
||||||
} else {
|
|
||||||
removeSync(`apps/${reactAppName}-e2e`);
|
|
||||||
execSync(`${pmc.rm} @nrwl/cypress eslint-plugin-cypress`);
|
|
||||||
}
|
|
||||||
|
|
||||||
output.log({ title: '🙂 Please be patient, one final step remaining!' });
|
output.log({ title: '🙂 Please be patient, one final step remaining!' });
|
||||||
|
|
||||||
@ -164,17 +112,17 @@ export async function createNxWorkspaceForReact(options: Record<string, any>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addDependencies(
|
addDependencies(
|
||||||
pmc,
|
options.pmc,
|
||||||
'@testing-library/jest-dom',
|
'@testing-library/jest-dom',
|
||||||
'eslint-config-react-app',
|
'eslint-config-react-app',
|
||||||
'web-vitals',
|
'web-vitals',
|
||||||
'jest-watch-typeahead'
|
'jest-watch-typeahead'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isVite) {
|
if (options.isVite) {
|
||||||
addDependencies(pmc, 'vite', 'vitest', '@vitejs/plugin-react');
|
addDependencies(options.pmc, 'vite', 'vitest', '@vitejs/plugin-react');
|
||||||
} else {
|
} else {
|
||||||
addDependencies(pmc, '@craco/craco', 'cross-env', 'react-scripts');
|
addDependencies(options.pmc, '@craco/craco', 'cross-env', 'react-scripts');
|
||||||
}
|
}
|
||||||
|
|
||||||
output.log({ title: '🎉 Done!' });
|
output.log({ title: '🎉 Done!' });
|
||||||
@ -188,20 +136,148 @@ export async function createNxWorkspaceForReact(options: Record<string, any>) {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isVite) {
|
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({
|
output.note({
|
||||||
title: `A new apps/${reactAppName}/index.html has been created. Compare it to the previous apps/${reactAppName}/public/index.html file and make any changes needed, then delete the previous file.`,
|
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({
|
output.note({
|
||||||
title: 'Or, you can try the commands!',
|
title: 'Or, you can try the commands!',
|
||||||
bodyLines: [
|
bodyLines: [
|
||||||
`npx nx serve ${reactAppName}`,
|
`npx nx serve ${options.reactAppName}`,
|
||||||
`npx nx build ${reactAppName}`,
|
`npx nx build ${options.reactAppName}`,
|
||||||
`npx nx test ${reactAppName}`,
|
`npx nx test ${options.reactAppName}`,
|
||||||
` `,
|
` `,
|
||||||
`https://nx.dev/getting-started/intro#10-try-the-commands`,
|
`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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { sync } from 'glob';
|
import { sync } from 'glob';
|
||||||
import { renameSync, readFileSync } from 'fs-extra';
|
import { renameSync, readFileSync } from 'fs-extra';
|
||||||
import { performance } from 'perf_hooks';
|
|
||||||
|
|
||||||
// Vite cannot process JSX like <div> or <Header> unless the file is named .jsx or .tsx
|
// Vite cannot process JSX like <div> or <Header> unless the file is named .jsx or .tsx
|
||||||
export function renameJsToJsx(appName: string) {
|
export function renameJsToJsx(appName: string, isNested: boolean) {
|
||||||
const files = sync(`apps/${appName}/src/**/*.js`);
|
const files = sync(isNested ? 'src/**/*.js' : `apps/${appName}/src/**/*.js`);
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const content = readFileSync(file).toString();
|
const content = readFileSync(file).toString();
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import {
|
|||||||
readJsonFile,
|
readJsonFile,
|
||||||
writeJsonFile,
|
writeJsonFile,
|
||||||
} from 'nx/src/utils/fileutils';
|
} from 'nx/src/utils/fileutils';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
const defaultTsConfig = {
|
const defaultTsConfig = (relativePathToRoot: string) => ({
|
||||||
extends: '../../tsconfig.base.json',
|
extends: join(relativePathToRoot, 'tsconfig.base.json'),
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
jsx: 'react',
|
jsx: 'react',
|
||||||
allowJs: true,
|
allowJs: true,
|
||||||
@ -22,26 +23,26 @@ const defaultTsConfig = {
|
|||||||
path: './tsconfig.spec.json',
|
path: './tsconfig.spec.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
|
|
||||||
const defaultTsConfigApp = {
|
const defaultTsConfigApp = (relativePathToRoot: string) => ({
|
||||||
extends: './tsconfig.json',
|
extends: './tsconfig.json',
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
outDir: '../../dist/out-tsc',
|
outDir: join(relativePathToRoot, 'dist/out-tsc'),
|
||||||
types: ['node'],
|
types: ['node'],
|
||||||
},
|
},
|
||||||
files: [
|
files: [
|
||||||
'../../node_modules/@nrwl/react/typings/cssmodule.d.ts',
|
join(relativePathToRoot, 'node_modules/@nrwl/react/typings/cssmodule.d.ts'),
|
||||||
'../../node_modules/@nrwl/react/typings/image.d.ts',
|
join(relativePathToRoot, 'node_modules/@nrwl/react/typings/image.d.ts'),
|
||||||
],
|
],
|
||||||
exclude: ['**/*.spec.ts', '**/*.spec.tsx'],
|
exclude: ['**/*.spec.ts', '**/*.spec.tsx'],
|
||||||
include: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
|
include: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
|
||||||
};
|
});
|
||||||
|
|
||||||
const defaultTsConfigSpec = {
|
const defaultTsConfigSpec = (relativePathToRoot: string) => ({
|
||||||
extends: './tsconfig.json',
|
extends: './tsconfig.json',
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
outDir: '../../dist/out-tsc',
|
outDir: join(relativePathToRoot, 'dist/out-tsc'),
|
||||||
module: 'commonjs',
|
module: 'commonjs',
|
||||||
types: ['jest', 'node'],
|
types: ['jest', 'node'],
|
||||||
},
|
},
|
||||||
@ -53,15 +54,28 @@ const defaultTsConfigSpec = {
|
|||||||
'**/*.d.ts',
|
'**/*.d.ts',
|
||||||
],
|
],
|
||||||
files: [
|
files: [
|
||||||
'../../node_modules/@nrwl/react/typings/cssmodule.d.ts',
|
join(relativePathToRoot, 'node_modules/@nrwl/react/typings/cssmodule.d.ts'),
|
||||||
'../../node_modules/@nrwl/react/typings/image.d.ts',
|
join(relativePathToRoot, 'node_modules/@nrwl/react/typings/image.d.ts'),
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
|
|
||||||
export function setupTsConfig(appName: string) {
|
export function setupTsConfig(appName: string, isNested: boolean) {
|
||||||
if (fileExists(`apps/${appName}/tsconfig.json`)) {
|
const tsconfigPath = isNested
|
||||||
const json = readJsonFile(`apps/${appName}/tsconfig.json`);
|
? 'tsconfig.json'
|
||||||
json.extends = '../../tsconfig.base.json';
|
: `apps/${appName}/tsconfig.json`;
|
||||||
|
const tsconfigAppPath = isNested
|
||||||
|
? 'tsconfig.app.json'
|
||||||
|
: `apps/${appName}/tsconfig.app.json`;
|
||||||
|
const tsconfiSpecPath = isNested
|
||||||
|
? 'tsconfig.spec.json'
|
||||||
|
: `apps/${appName}/tsconfig.spec.json`;
|
||||||
|
const tsconfigBasePath = isNested
|
||||||
|
? './tsconfig.base.json'
|
||||||
|
: '../../tsconfig.base.json';
|
||||||
|
const relativePathToRoot = isNested ? '.' : '../../';
|
||||||
|
if (fileExists(tsconfigPath)) {
|
||||||
|
const json = readJsonFile(tsconfigPath);
|
||||||
|
json.extends = tsconfigBasePath;
|
||||||
if (json.compilerOptions) {
|
if (json.compilerOptions) {
|
||||||
json.compilerOptions.jsx = 'react';
|
json.compilerOptions.jsx = 'react';
|
||||||
} else {
|
} else {
|
||||||
@ -72,24 +86,24 @@ export function setupTsConfig(appName: string) {
|
|||||||
allowSyntheticDefaultImports: true,
|
allowSyntheticDefaultImports: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
writeJsonFile(`apps/${appName}/tsconfig.json`, json);
|
writeJsonFile(tsconfigPath, json);
|
||||||
} else {
|
} else {
|
||||||
writeJsonFile(`apps/${appName}/tsconfig.json`, defaultTsConfig);
|
writeJsonFile(tsconfigPath, defaultTsConfig(relativePathToRoot));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileExists(`apps/${appName}/tsconfig.app.json`)) {
|
if (fileExists(tsconfigAppPath)) {
|
||||||
const json = readJsonFile(`apps/${appName}/tsconfig.app.json`);
|
const json = readJsonFile(tsconfigAppPath);
|
||||||
json.extends = './tsconfig.json';
|
json.extends = './tsconfig.json';
|
||||||
writeJsonFile(`apps/${appName}/tsconfig.app.json`, json);
|
writeJsonFile(tsconfigAppPath, json);
|
||||||
} else {
|
} else {
|
||||||
writeJsonFile(`apps/${appName}/tsconfig.app.json`, defaultTsConfigApp);
|
writeJsonFile(tsconfigAppPath, defaultTsConfigApp(relativePathToRoot));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileExists(`apps/${appName}/tsconfig.spec.json`)) {
|
if (fileExists(tsconfiSpecPath)) {
|
||||||
const json = readJsonFile(`apps/${appName}/tsconfig.spec.json`);
|
const json = readJsonFile(tsconfiSpecPath);
|
||||||
json.extends = './tsconfig.json';
|
json.extends = './tsconfig.json';
|
||||||
writeJsonFile(`apps/${appName}/tsconfig.spec.json`, json);
|
writeJsonFile(tsconfiSpecPath, json);
|
||||||
} else {
|
} else {
|
||||||
writeJsonFile(`apps/${appName}/tsconfig.spec.json`, defaultTsConfigSpec);
|
writeJsonFile(tsconfiSpecPath, defaultTsConfigSpec(relativePathToRoot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { writeFileSync } from 'fs';
|
import { writeFileSync } from 'fs';
|
||||||
|
|
||||||
export function writeCracoConfig(appName: string, isCRA5: boolean) {
|
export function writeCracoConfig(
|
||||||
|
appName: string,
|
||||||
|
isCRA5: boolean,
|
||||||
|
isNested: boolean
|
||||||
|
) {
|
||||||
const configOverride = `
|
const configOverride = `
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const TsConfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
const TsConfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||||
@ -57,5 +61,8 @@ export function writeCracoConfig(appName: string, isCRA5: boolean) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
writeFileSync(`apps/${appName}/craco.config.js`, configOverride);
|
writeFileSync(
|
||||||
|
isNested ? 'craco.config.js' : `apps/${appName}/craco.config.js`,
|
||||||
|
configOverride
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
||||||
export function writeViteConfig(appName: string) {
|
export function writeViteConfig(
|
||||||
|
appName: string,
|
||||||
|
isNested: boolean,
|
||||||
|
isJs: boolean
|
||||||
|
) {
|
||||||
let port = 4200;
|
let port = 4200;
|
||||||
|
|
||||||
// Use PORT from .env file if it exists in project.
|
// Use PORT from .env file if it exists in project.
|
||||||
@ -14,14 +18,14 @@ export function writeViteConfig(appName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
`apps/${appName}/vite.config.js`,
|
isNested ? 'vite.config.js' : `apps/${appName}/vite.config.js`,
|
||||||
`import { defineConfig } from 'vite'
|
`import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
outDir: '../../dist/apps/${appName}'
|
outDir: ${isNested ? `'./dist'` : `'../../dist/apps/${appName}'`}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: ${port},
|
port: ${port},
|
||||||
@ -30,7 +34,7 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: 'src/setupTests.js',
|
setupFiles: 'src/setupTests.${isJs ? 'js' : 'ts'}',
|
||||||
css: true,
|
css: true,
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
|||||||
@ -1,8 +1,17 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
||||||
export function writeViteIndexHtml(appName: string) {
|
export function writeViteIndexHtml(
|
||||||
|
appName: string,
|
||||||
|
isNested: boolean,
|
||||||
|
isJs: boolean
|
||||||
|
) {
|
||||||
|
const indexPath = isNested ? 'index.html' : `apps/${appName}/index.html`;
|
||||||
|
if (fs.existsSync(indexPath)) {
|
||||||
|
fs.copyFileSync(indexPath, indexPath + '.old');
|
||||||
|
}
|
||||||
|
const indexFile = isJs ? '/src/index.jsx' : '/src/index.tsx';
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
`apps/${appName}/index.html`,
|
indexPath,
|
||||||
`<!DOCTYPE html>
|
`<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@ -13,7 +22,7 @@ export function writeViteIndexHtml(appName: string) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/index.jsx"></script>
|
<script type="module" src="${indexFile}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user