Skip to content

Commit 2a222b2

Browse files
Saadnajmiclaude
andauthored
fix(macos-init): improve init flow and drop chalk dependency (#2858)
## Summary - Remove redundant dependency install (`generate-macos.js` was running `npm i`/`yarn` a second time after `cli.ts` already installed `react-native-macos`) - Show actual npm/yarn errors on install failure instead of swallowing them with `--silent` - Validate peer dependencies before install and warn on version mismatch - Fix finish message: remove nonexistent `yarn start:macos`, show correct `pod install`, `npx react-native run-macos` and `npx react-native start` - Replace `chalk` with Node's built-in `node:util` `styleText` (available since Node 20.12), add `engines.node` field - Re-enable integration test CI without Verdaccio (fixes #2344) — installs local package directly via `npm install <path>` ## Motivation Users hitting `npx react-native-macos-init` frequently encounter silent failures from peer dependency mismatches (e.g. `react-native-macos@0.81.4` requires `react-native@0.81.6` exactly, but `--version 0.81` installs the latest patch). The `--silent` flag hid the actual error. The finish message referenced a nonexistent `yarn start:macos` script. Addresses the same root issue as #2785 and #2793 but with a broader fix. ## Test plan - [x] Created a new project with `npx @react-native-community/cli init testapp --version 0.81.6` - [x] Ran modified init — happy path: no peer dep warning, correct finish message - [x] Tested mismatch path (`react-native@0.81.5` vs required `0.81.6`) — warning fires before install with clear message - [x] TypeScript compiles cleanly - [x] Re-enabled CI integration test (no longer depends on Verdaccio) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 62b2545 commit 2a222b2

File tree

8 files changed

+150
-118
lines changed

8 files changed

+150
-118
lines changed

.github/workflows/microsoft-pr.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,11 @@ jobs:
132132
permissions: {}
133133
uses: ./.github/workflows/microsoft-build-rntester.yml
134134

135-
# https://github.com/microsoft/react-native-macos/issues/2344
136-
# Disable these tests because verdaccio hangs
137-
# test-react-native-macos-init:
138-
# name: "Test react-native-macos init"
139-
# permissions: {}
140-
# if: ${{ endsWith(github.base_ref, '-stable') }}
141-
# uses: ./.github/workflows/microsoft-test-react-native-macos-init.yml
135+
test-react-native-macos-init:
136+
name: "Test react-native-macos init"
137+
permissions: {}
138+
if: ${{ endsWith(github.base_ref, '-stable') }}
139+
uses: ./.github/workflows/microsoft-test-react-native-macos-init.yml
142140

143141
# https://github.com/microsoft/react-native-macos/issues/2344
144142
# Disable these tests because verdaccio hangs
@@ -158,7 +156,7 @@ jobs:
158156
- yarn-constraints
159157
- javascript-tests
160158
- build-rntester
161-
# - test-react-native-macos-init
159+
- test-react-native-macos-init
162160
# - react-native-test-app-integration
163161
steps:
164162
- name: All required jobs passed

.github/workflows/microsoft-test-react-native-macos-init.yml

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,53 +32,36 @@ jobs:
3232
working-directory: packages/react-native-macos-init
3333
run: yarn build
3434

35-
- name: Start Verdaccio server
35+
- name: Determine React Native version
36+
id: rn-version
3637
run: |
37-
set -euo pipefail
38-
nohup npx --yes verdaccio --config .ado/verdaccio/config.yaml >/dev/null 2>&1 &
39-
echo $! > $RUNNER_TEMP/verdaccio.pid
40-
- name: Wait for Verdaccio to be ready
41-
run: node .ado/scripts/waitForVerdaccio.mjs http://localhost:4873
42-
43-
- name: Configure npm for Verdaccio
44-
run: .ado/scripts/verdaccio.sh init
45-
46-
- name: Publish to Verdaccio
47-
run: .ado/scripts/verdaccio.sh publish --branch origin/${{ github.base_ref }}
48-
49-
- name: Export versions
50-
run: node .ado/scripts/export-versions.mjs
38+
RN_VERSION=$(node -e "
39+
const pkg = require('./packages/react-native/package.json');
40+
console.log(pkg.peerDependencies['react-native']);
41+
")
42+
echo "version=$RN_VERSION" >> "$GITHUB_OUTPUT"
5143
5244
- name: Initialize new project
5345
run: |
5446
set -eox pipefail
55-
npx --yes @react-native-community/cli init testcli --version $(cat .react_native_version) --skip-install
47+
npx --yes @react-native-community/cli init testcli --version ${{ steps.rn-version.outputs.version }}
5648
working-directory: ${{ runner.temp }}
5749

58-
- name: Install dependencies in new project
50+
- name: Install local react-native-macos
5951
working-directory: ${{ runner.temp }}/testcli
6052
run: |
6153
set -eox pipefail
62-
yarn install --mode=update-lockfile
63-
yarn install
54+
npm install ${{ github.workspace }}/packages/react-native
6455
6556
- name: Apply macOS template
6657
working-directory: ${{ runner.temp }}/testcli
6758
run: |
6859
set -eox pipefail
69-
${{ github.workspace }}/.ado/scripts/verdaccio.sh configure
70-
node ${{ github.workspace }}/packages/react-native-macos-init/bin.js --verbose --version latest --overwrite --prerelease
60+
node ${{ github.workspace }}/packages/react-native-macos-init/bin.js --verbose --overwrite --prerelease
7161
pod install --project-directory=macos
7262
7363
- name: Build macOS app
7464
working-directory: ${{ runner.temp }}/testcli
7565
run: |
7666
set -eox pipefail
7767
npx react-native build-macos
78-
79-
- name: Stop Verdaccio
80-
if: always()
81-
run: |
82-
if [ -f "$RUNNER_TEMP/verdaccio.pid" ]; then
83-
kill "$(cat $RUNNER_TEMP/verdaccio.pid)" || true
84-
fi

packages/react-native-macos-init/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
"prepublishOnly": "npm run build"
1818
},
1919
"bin": "./bin.js",
20+
"engines": {
21+
"node": ">=20.12.0"
22+
},
2023
"dependencies": {
21-
"chalk": "^3",
2224
"find-up": "^4.1.0",
2325
"npm-registry-fetch": "^14.0.0",
2426
"prompts": "^2.3.0",
@@ -27,7 +29,7 @@
2729
},
2830
"devDependencies": {
2931
"@rnx-kit/tsconfig": "^2.0.0",
30-
"@types/chalk": "^2.2.0",
32+
"@types/node": "^22.0.0",
3133
"@types/npm-registry-fetch": "^8.0.0",
3234
"@types/prompts": "^2.0.3",
3335
"@types/semver": "^7.1.0",

packages/react-native-macos-init/src/cli.ts

Lines changed: 107 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
* @format
66
*/
77

8-
import chalk from 'chalk';
98
import {execSync} from 'child_process';
109
import * as findUp from 'find-up';
10+
import {styleText} from 'node:util';
1111
import * as fs from 'fs';
1212
import * as npmFetch from 'npm-registry-fetch';
1313
import prompts from 'prompts';
@@ -60,7 +60,9 @@ function getNpmRegistryUrl(): string {
6060
}
6161

6262
function getReactNativeAppName() {
63-
console.log(`Reading ${chalk.cyan('application name')} from package.json…`);
63+
console.log(
64+
`Reading ${styleText('cyan', 'application name')} from package.json…`,
65+
);
6466
const cwd = process.cwd();
6567
const pkgJsonPath = findUp.sync('package.json', {cwd});
6668
if (!pkgJsonPath) {
@@ -73,7 +75,9 @@ function getReactNativeAppName() {
7375
if (!name) {
7476
const appJsonPath = findUp.sync('app.json', {cwd});
7577
if (appJsonPath) {
76-
console.log(`Reading ${chalk.cyan('application name')} from app.json…`);
78+
console.log(
79+
`Reading ${styleText('cyan', 'application name')} from app.json…`,
80+
);
7781
name = JSON.parse(fs.readFileSync(appJsonPath, 'utf8')).name;
7882
}
7983
}
@@ -84,7 +88,9 @@ function getReactNativeAppName() {
8488
}
8589

8690
function getPackageVersion(packageName: string, exitOnError: boolean = true) {
87-
console.log(`Reading ${chalk.cyan(packageName)} version from node_modules…`);
91+
console.log(
92+
`Reading ${styleText('cyan', packageName)} version from node_modules…`,
93+
);
8894

8995
try {
9096
const pkgJsonPath = require.resolve(`${packageName}/package.json`, {
@@ -112,8 +118,8 @@ function getReactNativeMacOSVersion() {
112118
}
113119

114120
function errorOutOnUnsupportedVersionOfReactNative(rnVersion: string): never {
115-
const version = chalk.cyan(rnVersion);
116-
const supportedVersions = chalk.cyan('>=0.60');
121+
const version = styleText('cyan', rnVersion);
122+
const supportedVersions = styleText('cyan', '>=0.60');
117123
printError(
118124
`Unsupported version of ${RNPKG}: ${version}\n${MACOSPKG} supports ${RNPKG} versions ${supportedVersions}`,
119125
);
@@ -208,16 +214,52 @@ function isProjectUsingYarn(cwd: string) {
208214
* Outputs decorated version of the package for the CLI
209215
*/
210216
function printPkg(name: string, version?: string) {
211-
return `${chalk.yellow(name)}${
212-
version ? `${chalk.grey('@')}${chalk.cyan(version)}` : ''
217+
return `${styleText('yellow', name)}${
218+
version ? `${styleText('gray', '@')}${styleText('cyan', version)}` : ''
213219
}`;
214220
}
215221

216222
/**
217223
* Prints decorated version of console.error to the CLI
218224
*/
219225
function printError(message: string, ...optionalParams: any[]) {
220-
console.error(chalk.red(chalk.bold(message)), ...optionalParams);
226+
console.error(styleText(['red', 'bold'], message), ...optionalParams);
227+
}
228+
229+
/**
230+
* Checks if the resolved react-native-macos version's peer dependency on
231+
* react-native is compatible with the installed version. Warns if not.
232+
*/
233+
async function validatePeerDependencies(
234+
macosVersion: string,
235+
installedRNVersion: string,
236+
): Promise<void> {
237+
try {
238+
const npmResponse = await npmFetch.json(`${MACOSPKG}/${macosVersion}`, {
239+
registry: getNpmRegistryUrl(),
240+
});
241+
const peerDeps = (npmResponse as any).peerDependencies;
242+
if (peerDeps && peerDeps[RNPKG]) {
243+
const requiredRN = peerDeps[RNPKG];
244+
if (!semver.satisfies(installedRNVersion, requiredRN)) {
245+
console.warn(
246+
styleText(
247+
'yellow',
248+
`\n${styleText('bold', 'Warning:')} ${printPkg(
249+
MACOSPKG,
250+
macosVersion,
251+
)} requires ${printPkg(RNPKG, requiredRN)}, ` +
252+
`but you have ${printPkg(
253+
RNPKG,
254+
installedRNVersion,
255+
)} installed.\n`,
256+
),
257+
);
258+
}
259+
}
260+
} catch {
261+
// Non-fatal — if we can't check, proceed anyway
262+
}
221263
}
222264

223265
(async () => {
@@ -243,29 +285,38 @@ function printError(message: string, ...optionalParams: any[]) {
243285

244286
if (!argv.version) {
245287
console.log(
246-
`Latest matching version of ${chalk.green(MACOSPKG)} for ${printPkg(
247-
RNPKG,
248-
reactNativeVersion,
249-
)} is ${printPkg(MACOSPKG, reactNativeMacOSResolvedVersion)}.`,
288+
`Latest matching version of ${styleText(
289+
'green',
290+
MACOSPKG,
291+
)} for ${printPkg(RNPKG, reactNativeVersion)} is ${printPkg(
292+
MACOSPKG,
293+
reactNativeMacOSResolvedVersion,
294+
)}.`,
250295
);
251296

252297
if (semver.prerelease(reactNativeMacOSResolvedVersion)) {
253298
console.warn(
254299
`
255-
${printPkg(MACOSPKG, reactNativeMacOSResolvedVersion)} is a ${chalk.bgYellow(
300+
${printPkg(MACOSPKG, reactNativeMacOSResolvedVersion)} is a ${styleText(
301+
'bgYellow',
256302
'pre-release',
257303
)} version.
258304
The latest supported version is ${printPkg(
259305
MACOSPKG,
260306
reactNativeMacOSLatestVersion,
261307
)}.
262-
You can either downgrade your version of ${chalk.yellow(RNPKG)} to ${chalk.cyan(
308+
You can either downgrade your version of ${styleText(
309+
'yellow',
310+
RNPKG,
311+
)} to ${styleText(
312+
'cyan',
263313
getMatchingReactNativeSemVerForReactNativeMacOSVersion(
264314
reactNativeMacOSLatestVersion,
265315
),
266-
)}, or continue with a ${chalk.bgYellow(
316+
)}, or continue with a ${styleText(
317+
'bgYellow',
267318
'pre-release',
268-
)} version of ${chalk.yellow(MACOSPKG)}.
319+
)} version of ${styleText('yellow', MACOSPKG)}.
269320
`,
270321
);
271322

@@ -288,6 +339,11 @@ You can either downgrade your version of ${chalk.yellow(RNPKG)} to ${chalk.cyan(
288339
}
289340
}
290341

342+
await validatePeerDependencies(
343+
reactNativeMacOSResolvedVersion,
344+
reactNativeVersion,
345+
);
346+
291347
const pkgLatest = printPkg(MACOSPKG, version);
292348

293349
if (reactNativeMacOSResolvedVersion !== reactNativeMacOSVersion) {
@@ -298,15 +354,44 @@ You can either downgrade your version of ${chalk.yellow(RNPKG)} to ${chalk.cyan(
298354
);
299355

300356
const pkgmgr = isProjectUsingYarn(process.cwd())
301-
? `yarn add${verbose ? '' : ' --silent'}`
302-
: `npm install --save${verbose ? '' : ' --silent'}`;
357+
? 'yarn add'
358+
: 'npm install --save';
303359
const execOptions = verbose ? {stdio: 'inherit' as const} : {};
304-
execSync(`${pkgmgr} "${MACOSPKG}@${version}"`, execOptions);
305360

306-
console.log(`${pkgLatest} ${chalk.green('successfully installed!')}`);
361+
try {
362+
execSync(`${pkgmgr} "${MACOSPKG}@${version}"`, execOptions);
363+
} catch (e: any) {
364+
// When not verbose, execSync captures output in the error object
365+
if (!verbose) {
366+
if (e.stderr) {
367+
console.error(e.stderr.toString());
368+
}
369+
if (e.stdout) {
370+
console.log(e.stdout.toString());
371+
}
372+
}
373+
printError(
374+
`Failed to install ${printPkg(MACOSPKG, version)}.\n` +
375+
`This can happen if there is a peer dependency mismatch between ` +
376+
`${RNPKG} and ${MACOSPKG}.\n` +
377+
`Check that your installed version of ${styleText(
378+
'yellow',
379+
RNPKG,
380+
)} is compatible ` +
381+
`with ${printPkg(MACOSPKG, version)}.`,
382+
);
383+
process.exit(EXITCODE_UNKNOWN_ERROR);
384+
}
385+
386+
console.log(
387+
`${pkgLatest} ${styleText('green', 'successfully installed!')}`,
388+
);
307389
} else {
308390
console.log(
309-
`${chalk.green('Latest version')} of ${pkgLatest} already installed.`,
391+
`${styleText(
392+
'green',
393+
'Latest version',
394+
)} of ${pkgLatest} already installed.`,
310395
);
311396
}
312397

packages/react-native/local-cli/generate-macos.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
const {
55
copyProjectTemplateAndReplace,
6-
installDependencies,
76
printFinishMessage,
87
} = require('./generator-macos');
98
const fs = require('fs');
@@ -21,8 +20,6 @@ function generateMacOS (projectDir, name, options) {
2120
fs.mkdirSync(projectDir);
2221
}
2322

24-
installDependencies(options);
25-
2623
copyProjectTemplateAndReplace(
2724
path.join(__dirname, 'generator-macos', 'templates'),
2825
projectDir,

packages/react-native/local-cli/generator-common/index.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @ts-check
22

33
// @noflow
4-
const chalk = require('chalk');
54
const fs = require('fs');
5+
const { styleText } = require('node:util');
66
const path = require('path');
77

88
// Copied from `copyAndReplace` in react-native-community/cli as it's deleted in newer versions
@@ -330,11 +330,11 @@ function alwaysOverwriteContentChangedCallback(
330330
contentChanged
331331
) {
332332
if (contentChanged === 'new') {
333-
console.log(`${chalk.bold('new')} ${relativeDestPath}`);
333+
console.log(`${styleText('bold','new')} ${relativeDestPath}`);
334334
return 'overwrite';
335335
}
336336
if (contentChanged === 'changed') {
337-
console.log(`${chalk.bold('changed')} ${relativeDestPath} ${chalk.yellow('[overwriting]')}`);
337+
console.log(`${styleText('bold','changed')} ${relativeDestPath} ${styleText('yellow','[overwriting]')}`);
338338
return 'overwrite';
339339
}
340340
if (contentChanged === 'identical') {
@@ -351,12 +351,12 @@ function upgradeFileContentChangedCallback(
351351
contentChanged
352352
) {
353353
if (contentChanged === 'new') {
354-
console.log(`${chalk.bold('new')} ${relativeDestPath}`);
354+
console.log(`${styleText('bold','new')} ${relativeDestPath}`);
355355
return 'overwrite';
356356
}
357357
if (contentChanged === 'changed') {
358358
console.log(
359-
`${chalk.bold(relativeDestPath)} ` +
359+
`${styleText('bold',relativeDestPath)} ` +
360360
`has changed in the new version.\nDo you want to keep your ${relativeDestPath} or replace it with the ` +
361361
'latest version?\nIf you ever made any changes ' +
362362
'to this file, you\'ll probably want to keep it.\n' +

0 commit comments

Comments
 (0)