Skip to content

Commit 48ba9d5

Browse files
anupriya13Copilotjonthysell
authored
Update CLI to show static warning for old architecture in run-windows and interactive prompt for init-windows (#15038)
* Update CLI to show static warning for old architecture in run-windows and interactive prompt for init-windows (#15029) * Initial plan * Initial plan: Update CLI prompt for architecture selections on old arch Co-authored-by: anupriya13 <[email protected]> * Implement interactive architecture prompt for old architecture templates Co-authored-by: anupriya13 <[email protected]> * Final implementation: Interactive architecture prompt with comprehensive testing Co-authored-by: anupriya13 <[email protected]> * Remove unintended changes from vnext/codegen directory Co-authored-by: anupriya13 <[email protected]> * Add prerelease change file for architecture prompt feature Co-authored-by: anupriya13 <[email protected]> * Replace interactive prompt with static warning for run-windows command Co-authored-by: anupriya13 <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: anupriya13 <[email protected]> * Change files * Remove incorrect change file Co-Authored-By: Jon Thysell <[email protected]> * Address issues * add logs * nit fixes * prompt fix * Update initWindowsOptions.ts * Update initWindows.test.ts * Resolve Comments * Change files --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Jon Thysell <[email protected]>
1 parent 23a9902 commit 48ba9d5

11 files changed

+259
-12
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Update CLI to show static warning for old architecture in run-windows and interactive prompt for init-windows (#15029)",
4+
"packageName": "@react-native-windows/cli",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Update CLI to show static warning for old architecture in run-windows and interactive prompt for init-windows (#15029)",
4+
"packageName": "@react-native-windows/telemetry",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/@react-native-windows/cli/src/commands/initWindows/initWindows.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
} from '../../utils/telemetryHelpers';
3131
import {copyAndReplaceWithChangedCallback} from '../../generator-common';
3232
import * as nameHelpers from '../../utils/nameHelpers';
33+
import {showOldArchitectureWarning} from '../../utils/oldArchWarning';
34+
import {promptForArchitectureChoice} from '../../utils/architecturePrompt';
3335
import type {InitOptions} from './initWindowsOptions';
3436
import {initOptions} from './initWindowsOptions';
3537

@@ -154,6 +156,9 @@ export class InitWindows {
154156
return;
155157
}
156158

159+
const userDidNotPassTemplate = !process.argv.some(arg =>
160+
arg.startsWith('--template'),
161+
);
157162
this.options.template ??=
158163
(this.rnwConfig?.['init-windows']?.template as string | undefined) ??
159164
this.getDefaultTemplateName();
@@ -166,10 +171,23 @@ export class InitWindows {
166171
);
167172
}
168173

169-
if (this.options.template.startsWith('old')) {
170-
spinner.warn(
171-
`The legacy '${this.options.template}' template targets the React Native Old Architecture, which will eventually be deprecated. See https://microsoft.github.io/react-native-windows/docs/new-architecture for details on switching to the New Architecture.`,
172-
);
174+
const isOldArchTemplate = this.options.template.startsWith('old');
175+
const promptFlag = this.options.prompt;
176+
177+
if (isOldArchTemplate) {
178+
showOldArchitectureWarning();
179+
180+
if (userDidNotPassTemplate && promptFlag) {
181+
const promptResult = await promptForArchitectureChoice();
182+
183+
if (
184+
!promptResult.shouldContinueWithOldArch &&
185+
!promptResult.userCancelled
186+
) {
187+
spinner.info('Switching to New Architecture template (cpp-app)...');
188+
this.options.template = 'cpp-app';
189+
}
190+
}
173191
}
174192

175193
const templateConfig = this.templates.get(this.options.template)!;
@@ -303,6 +321,7 @@ function optionSanitizer(key: keyof InitOptions, value: any): any {
303321
case 'overwrite':
304322
case 'telemetry':
305323
case 'list':
324+
case 'prompt':
306325
return value === undefined ? false : value; // Return value
307326
}
308327
}
@@ -316,6 +335,19 @@ async function getExtraProps(): Promise<Record<string, any>> {
316335
return extraProps;
317336
}
318337

338+
function sanitizeOptions(
339+
opts: Record<string, any>,
340+
sanitizer: (key: keyof InitOptions, value: any) => any,
341+
): Record<string, any> {
342+
const sanitized: Record<string, any> = {};
343+
for (const key in opts) {
344+
if (Object.prototype.hasOwnProperty.call(opts, key)) {
345+
sanitized[key] = sanitizer(key as keyof InitOptions, opts[key]);
346+
}
347+
}
348+
return sanitized;
349+
}
350+
319351
/**
320352
* The function run when calling `npx @react-native-community/cli init-windows`.
321353
* @param args Unprocessed args passed from react-native CLI.
@@ -336,6 +368,7 @@ async function initWindows(
336368
);
337369

338370
let initWindowsError: Error | undefined;
371+
339372
try {
340373
await initWindowsInternal(args, config, options);
341374
} catch (ex) {
@@ -344,7 +377,11 @@ async function initWindows(
344377
Telemetry.trackException(initWindowsError);
345378
}
346379

347-
await endTelemetrySession(initWindowsError, getExtraProps);
380+
// Now, instead of custom fields, just pass the final options object
381+
await endTelemetrySession(initWindowsError, getExtraProps, options, opts =>
382+
sanitizeOptions(opts, optionSanitizer),
383+
);
384+
348385
setExitProcessWithError(options.logging, initWindowsError);
349386
}
350387

packages/@react-native-windows/cli/src/commands/initWindows/initWindowsOptions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface InitOptions {
1414
overwrite?: boolean;
1515
telemetry?: boolean;
1616
list?: boolean;
17+
prompt?: boolean;
1718
}
1819

1920
export const initOptions: CommandOption[] = [
@@ -52,4 +53,8 @@ export const initOptions: CommandOption[] = [
5253
description:
5354
'Shows a list with all available templates with their descriptions.',
5455
},
56+
{
57+
name: '--no-prompt',
58+
description: 'Skip any interactive prompts and use default choices.',
59+
},
5560
];

packages/@react-native-windows/cli/src/commands/runWindows/runWindows.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type {RunWindowsOptions} from './runWindowsOptions';
3333
import {runWindowsOptions} from './runWindowsOptions';
3434
import {autolinkWindowsInternal} from '../autolinkWindows/autolinkWindows';
3535
import type {AutoLinkOptions} from '../autolinkWindows/autolinkWindowsOptions';
36+
import {showOldArchitectureWarning} from '../../utils/oldArchWarning';
3637

3738
/**
3839
* Sanitizes the given option for telemetry.
@@ -208,9 +209,7 @@ async function runWindowsInternal(
208209

209210
// Warn about old architecture projects
210211
if (config.project.windows?.rnwConfig?.projectArch === 'old') {
211-
newWarn(
212-
'This project is using the React Native (for Windows) Old Architecture, which will eventually be deprecated. See https://microsoft.github.io/react-native-windows/docs/new-architecture for details on switching to the New Architecture.',
213-
);
212+
showOldArchitectureWarning();
214213
}
215214

216215
// Get the solution file
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
* Licensed under the MIT License.
4+
* @format
5+
*/
6+
7+
import {promptForArchitectureChoice} from '../utils/architecturePrompt';
8+
9+
// Mock prompts module
10+
jest.mock('prompts', () => {
11+
return jest.fn();
12+
});
13+
14+
import prompts from 'prompts';
15+
const mockPrompts = prompts as jest.MockedFunction<typeof prompts>;
16+
17+
describe('architecturePrompt', () => {
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
});
21+
22+
test('returns true when user chooses Y', async () => {
23+
mockPrompts.mockResolvedValue({choice: 'y'});
24+
25+
const result = await promptForArchitectureChoice();
26+
27+
expect(result.shouldContinueWithOldArch).toBe(true);
28+
expect(result.userCancelled).toBe(false);
29+
});
30+
31+
test('returns true when user chooses Y (uppercase)', async () => {
32+
mockPrompts.mockResolvedValue({choice: 'Y'});
33+
34+
const result = await promptForArchitectureChoice();
35+
36+
expect(result.shouldContinueWithOldArch).toBe(true);
37+
expect(result.userCancelled).toBe(false);
38+
});
39+
40+
test('returns false when user chooses N', async () => {
41+
mockPrompts.mockResolvedValue({choice: 'n'});
42+
43+
const result = await promptForArchitectureChoice();
44+
45+
expect(result.shouldContinueWithOldArch).toBe(false);
46+
expect(result.userCancelled).toBe(false);
47+
});
48+
49+
test('returns false when user chooses N (uppercase)', async () => {
50+
mockPrompts.mockResolvedValue({choice: 'N'});
51+
52+
const result = await promptForArchitectureChoice();
53+
54+
expect(result.shouldContinueWithOldArch).toBe(false);
55+
expect(result.userCancelled).toBe(false);
56+
});
57+
58+
test('returns true with userCancelled when user cancels with no input', async () => {
59+
mockPrompts.mockResolvedValue({});
60+
61+
const result = await promptForArchitectureChoice();
62+
63+
expect(result.shouldContinueWithOldArch).toBe(true);
64+
expect(result.userCancelled).toBe(true);
65+
});
66+
67+
test('returns true with userCancelled when prompts throws cancellation error', async () => {
68+
mockPrompts.mockRejectedValue(new Error('User cancelled'));
69+
70+
const result = await promptForArchitectureChoice();
71+
72+
expect(result.shouldContinueWithOldArch).toBe(true);
73+
expect(result.userCancelled).toBe(true);
74+
});
75+
76+
test('returns true and not cancelled on other errors', async () => {
77+
mockPrompts.mockRejectedValue(new Error('Some other error'));
78+
79+
const result = await promptForArchitectureChoice();
80+
81+
expect(result.shouldContinueWithOldArch).toBe(true);
82+
expect(result.userCancelled).toBe(false);
83+
});
84+
});

packages/@react-native-windows/cli/src/e2etest/initWindows.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function validateOptionName(
2626
case 'overwrite':
2727
case 'telemetry':
2828
case 'list':
29+
case 'prompt':
2930
return true;
3031
}
3132
throw new Error(
@@ -55,6 +56,7 @@ test('initOptions - validate options', () => {
5556

5657
// Validate all command options are present in InitOptions
5758
const optionName = commanderNameToOptionName(commandOption.name);
59+
5860
expect(
5961
validateOptionName(commandOption.name, optionName as keyof InitOptions),
6062
).toBe(true);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
* Licensed under the MIT License.
4+
* @format
5+
*/
6+
7+
import prompts from 'prompts';
8+
import chalk from 'chalk';
9+
export interface ArchitecturePromptResult {
10+
shouldContinueWithOldArch: boolean;
11+
userCancelled: boolean;
12+
}
13+
14+
export async function promptForArchitectureChoice(): Promise<ArchitecturePromptResult> {
15+
try {
16+
const response = await prompts(
17+
{
18+
type: 'text',
19+
name: 'choice',
20+
message: 'Would you like to continue using the Old Architecture? (Y/N)',
21+
validate: (value: string) => {
22+
const normalized = value.trim().toLowerCase();
23+
if (normalized === 'y' || normalized === 'n') {
24+
return true;
25+
}
26+
return "Invalid input. Please enter 'Y' for Yes or 'N' for No.";
27+
},
28+
},
29+
{
30+
onCancel: () => {
31+
throw new Error('User cancelled');
32+
},
33+
},
34+
);
35+
36+
if (!response.choice) {
37+
return {shouldContinueWithOldArch: true, userCancelled: true};
38+
}
39+
40+
const normalizedChoice = response.choice.trim().toLowerCase();
41+
if (normalizedChoice === 'y') {
42+
console.log(
43+
chalk.yellow(
44+
'Proceeding with Old Architecture. You can migrate later using our migration guide: https://microsoft.github.io/react-native-windows/docs/new-architecture',
45+
),
46+
);
47+
return {shouldContinueWithOldArch: true, userCancelled: false};
48+
} else {
49+
console.log(
50+
chalk.green(
51+
'Great choice! Setting up the project with New Architecture support.',
52+
),
53+
);
54+
return {shouldContinueWithOldArch: false, userCancelled: false};
55+
}
56+
} catch (error) {
57+
if ((error as Error).message === 'User cancelled') {
58+
return {shouldContinueWithOldArch: true, userCancelled: true};
59+
}
60+
console.log(
61+
chalk.yellow(
62+
'Proceeding with Old Architecture. You can migrate later using our migration guide: https://microsoft.github.io/react-native-windows/docs/new-architecture',
63+
),
64+
);
65+
return {shouldContinueWithOldArch: true, userCancelled: false};
66+
}
67+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
* Licensed under the MIT License.
4+
* @format
5+
*/
6+
7+
import chalk from 'chalk';
8+
9+
/**
10+
* Displays a warning message about the Old Architecture template.
11+
*/
12+
export function showOldArchitectureWarning(): void {
13+
console.log(
14+
chalk.yellow(
15+
`⚠️ This project is using the React Native (for Windows) Old Architecture. The old architecture will begin to be removed starting with [email protected].`,
16+
),
17+
);
18+
console.log();
19+
console.log(
20+
chalk.cyan(
21+
'💡 It is strongly recommended to move to the new architecture as soon as possible to take advantage of improved performance, long-term support, and modern capabilities.',
22+
),
23+
);
24+
console.log();
25+
console.log(
26+
chalk.blue(
27+
'🔗 Learn more: https://microsoft.github.io/react-native-windows/docs/new-architecture',
28+
),
29+
);
30+
console.log();
31+
}

packages/@react-native-windows/cli/src/utils/telemetryHelpers.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ export async function startTelemetrySession(
151151
export async function endTelemetrySession(
152152
error?: Error,
153153
getExtraProps?: () => Promise<Record<string, any>>,
154+
finalOptions?: Record<string, any>,
155+
optionSanitizer?: (opts: Record<string, any>) => Record<string, any>,
154156
) {
155157
if (!Telemetry.isEnabled()) {
156158
// Bail early so don't waste time here
@@ -166,8 +168,13 @@ export async function endTelemetrySession(
166168
error instanceof CodedError ? (error as CodedError).type : 'Unknown';
167169
}
168170

169-
Telemetry.endCommand(
170-
endInfo,
171-
getExtraProps ? await getExtraProps() : undefined,
172-
);
171+
// Get extra properties
172+
const extraProps = getExtraProps ? await getExtraProps() : undefined;
173+
174+
// Sanitize and attach finalOptions if provided
175+
if (finalOptions && optionSanitizer) {
176+
endInfo.finalOptions = optionSanitizer(finalOptions);
177+
}
178+
179+
Telemetry.endCommand(endInfo, extraProps);
173180
}

0 commit comments

Comments
 (0)