diff --git a/change/@react-native-windows-cli-df0889c3-e18b-4f66-82ef-3b55c9294c4b.json b/change/@react-native-windows-cli-df0889c3-e18b-4f66-82ef-3b55c9294c4b.json new file mode 100644 index 00000000000..c078196ecfd --- /dev/null +++ b/change/@react-native-windows-cli-df0889c3-e18b-4f66-82ef-3b55c9294c4b.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Update CLI to show static warning for old architecture in run-windows and interactive prompt for init-windows (#15029)", + "packageName": "@react-native-windows/cli", + "email": "54227869+anupriya13@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/cli/src/commands/initWindows/initWindows.ts b/packages/@react-native-windows/cli/src/commands/initWindows/initWindows.ts index 84dd23367f6..128ba3038ad 100644 --- a/packages/@react-native-windows/cli/src/commands/initWindows/initWindows.ts +++ b/packages/@react-native-windows/cli/src/commands/initWindows/initWindows.ts @@ -30,6 +30,7 @@ import { } from '../../utils/telemetryHelpers'; import {copyAndReplaceWithChangedCallback} from '../../generator-common'; import * as nameHelpers from '../../utils/nameHelpers'; +import {promptForArchitectureChoice} from '../../utils/architecturePrompt'; import type {InitOptions} from './initWindowsOptions'; import {initOptions} from './initWindowsOptions'; @@ -167,9 +168,26 @@ export class InitWindows { } if (this.options.template.startsWith('old')) { - spinner.warn( - `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.`, + const promptResult = await promptForArchitectureChoice( + this.options.template, ); + + if ( + !promptResult.shouldContinueWithOldArch && + !promptResult.userCancelled + ) { + // User chose to switch to New Architecture + spinner.info('Switching to New Architecture template (cpp-app)...'); + this.options.template = 'cpp-app'; + + // Verify the new template exists + if (!this.templates.has(this.options.template.replace(/[\\]/g, '/'))) { + throw new CodedError( + 'InvalidTemplateName', + `Unable to find New Architecture template '${this.options.template}'.`, + ); + } + } } const templateConfig = this.templates.get(this.options.template)!; diff --git a/packages/@react-native-windows/cli/src/commands/runWindows/runWindows.ts b/packages/@react-native-windows/cli/src/commands/runWindows/runWindows.ts index cfe606e4f4a..7290b37a6b2 100644 --- a/packages/@react-native-windows/cli/src/commands/runWindows/runWindows.ts +++ b/packages/@react-native-windows/cli/src/commands/runWindows/runWindows.ts @@ -208,9 +208,24 @@ async function runWindowsInternal( // Warn about old architecture projects if (config.project.windows?.rnwConfig?.projectArch === 'old') { - newWarn( - '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.', + console.log( + chalk.yellow( + `āš ļø The 'old architecture project' is based on the React Native Old Architecture, which will eventually be deprecated in future releases.`, + ), ); + console.log(); + console.log( + chalk.cyan( + 'šŸ’” We recommend switching to the New Architecture to take advantage of improved performance, long-term support, and modern capabilities.', + ), + ); + console.log(); + console.log( + chalk.blue( + 'šŸ”— Learn more: https://microsoft.github.io/react-native-windows/docs/new-architecture', + ), + ); + console.log(); } // Get the solution file diff --git a/packages/@react-native-windows/cli/src/e2etest/architecturePrompt.test.ts b/packages/@react-native-windows/cli/src/e2etest/architecturePrompt.test.ts new file mode 100644 index 00000000000..fcd18001c49 --- /dev/null +++ b/packages/@react-native-windows/cli/src/e2etest/architecturePrompt.test.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * @format + */ + +import {promptForArchitectureChoice} from '../utils/architecturePrompt'; + +// Mock prompts module +jest.mock('prompts', () => { + return jest.fn(); +}); + +import prompts from 'prompts'; +const mockPrompts = prompts as jest.MockedFunction; + +describe('architecturePrompt', () => { + beforeEach(() => { + jest.useFakeTimers(); // ensure timers are controlled + jest.clearAllMocks(); + // Mock console methods to prevent output during tests + jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + // flush any pending timers from the timeout in promptForArchitectureChoice + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + jest.restoreAllMocks(); + }); + + test('returns true when user chooses Y', async () => { + mockPrompts.mockResolvedValue({choice: 'y'}); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app'); + + expect(result.shouldContinueWithOldArch).toBe(true); + expect(result.userCancelled).toBe(false); + }); + + test('returns true when user chooses Y (uppercase)', async () => { + mockPrompts.mockResolvedValue({choice: 'Y'}); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app'); + + expect(result.shouldContinueWithOldArch).toBe(true); + expect(result.userCancelled).toBe(false); + }); + + test('returns false when user chooses N', async () => { + mockPrompts.mockResolvedValue({choice: 'n'}); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app'); + + expect(result.shouldContinueWithOldArch).toBe(false); + expect(result.userCancelled).toBe(false); + }); + + test('returns false when user chooses N (uppercase)', async () => { + mockPrompts.mockResolvedValue({choice: 'N'}); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app'); + + expect(result.shouldContinueWithOldArch).toBe(false); + expect(result.userCancelled).toBe(false); + }); + + test('returns true with userCancelled when user cancels', async () => { + mockPrompts.mockResolvedValue({}); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app'); + + expect(result.shouldContinueWithOldArch).toBe(true); + expect(result.userCancelled).toBe(true); + }); + + test('returns true with userCancelled when prompts throws cancellation error', async () => { + mockPrompts.mockRejectedValue(new Error('User cancelled')); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app'); + + expect(result.shouldContinueWithOldArch).toBe(true); + expect(result.userCancelled).toBe(true); + }); + + test('handles max retries for invalid input', async () => { + // First two calls return invalid responses, third call succeeds + mockPrompts + .mockRejectedValueOnce(new Error('Invalid input')) + .mockRejectedValueOnce(new Error('Invalid input')) + .mockResolvedValueOnce({choice: 'y'}); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app', 3); + + expect(result.shouldContinueWithOldArch).toBe(true); + expect(result.userCancelled).toBe(false); + expect(mockPrompts).toHaveBeenCalledTimes(3); + }); + + test('returns true after max retries exceeded', async () => { + // All calls return invalid responses + mockPrompts.mockRejectedValue(new Error('Invalid input')); + + const result = await promptForArchitectureChoice('old/uwp-cpp-app', 2); + + expect(result.shouldContinueWithOldArch).toBe(true); + expect(result.userCancelled).toBe(false); + expect(mockPrompts).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/@react-native-windows/cli/src/utils/architecturePrompt.ts b/packages/@react-native-windows/cli/src/utils/architecturePrompt.ts new file mode 100644 index 00000000000..4cd920db8b8 --- /dev/null +++ b/packages/@react-native-windows/cli/src/utils/architecturePrompt.ts @@ -0,0 +1,142 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * @format + */ + +import prompts from 'prompts'; +import chalk from 'chalk'; + +export interface ArchitecturePromptResult { + shouldContinueWithOldArch: boolean; + userCancelled: boolean; +} + +/** + * Shows an interactive prompt asking the user whether they want to continue with Old Architecture + * or switch to New Architecture. + * @param templateName The name of the old architecture template being used + * @param maxRetries Maximum number of retries for invalid input (default: 3) + * @returns Promise with the user's choice + */ +export async function promptForArchitectureChoice( + templateName: string, + maxRetries: number = 3, +): Promise { + console.log( + chalk.yellow( + `āš ļø The '${templateName}' template is based on the React Native Old Architecture, which will eventually be deprecated in future releases.`, + ), + ); + console.log(); + console.log( + chalk.cyan( + 'šŸ’” We recommend switching to the New Architecture to take advantage of improved performance, long-term support, and modern capabilities.', + ), + ); + console.log(); + console.log( + chalk.blue( + 'šŸ”— Learn more: https://microsoft.github.io/react-native-windows/docs/new-architecture', + ), + ); + console.log(); + + let attempts = 0; + + while (attempts < maxRetries) { + try { + let timeoutId: NodeJS.Timeout | undefined; + + // Wrap prompts in a cancelable promise + const userInputPromise = new Promise<{choice?: string}>( + (resolve, reject) => { + prompts( + { + type: 'text', + name: 'choice', + message: + 'Would you like to continue using the Old Architecture? (Y/N)', + validate: (value: string) => { + const normalized = value.trim().toLowerCase(); + return normalized === 'y' || normalized === 'n' + ? true + : "Invalid input. Please enter 'Y' for Yes or 'N' for No."; + }, + }, + { + onCancel: () => reject(new Error('User cancelled')), + }, + ) + .then(resolve) + .catch(reject); + }, + ); + + // Timeout fallback with clearTimeout support + const timeoutPromise = new Promise<{choice?: string}>(resolve => { + timeoutId = setTimeout(() => { + console.log( + chalk.yellow( + '\nā³ No input received in 3 seconds. Proceeding with Old Architecture by default.', + ), + ); + resolve({choice: 'y'}); + }, 3000); + }); + + const response = await Promise.race([userInputPromise, timeoutPromise]); + + // ensures timeout callback never runs after user input + if (timeoutId) clearTimeout(timeoutId); + + if (!response.choice) { + // User cancelled or no input + return {shouldContinueWithOldArch: true, userCancelled: true}; + } + + const normalizedChoice = response.choice.trim().toLowerCase(); + + if (normalizedChoice === 'y') { + console.log( + chalk.yellow( + 'Proceeding with Old Architecture. You can migrate later using our migration guide: https://microsoft.github.io/react-native-windows/docs/new-architecture', + ), + ); + return {shouldContinueWithOldArch: true, userCancelled: false}; + } else if (normalizedChoice === 'n') { + console.log( + chalk.green( + 'Great choice! Setting up the project with New Architecture support.', + ), + ); + return {shouldContinueWithOldArch: false, userCancelled: false}; + } + } catch (error) { + if ((error as Error).message === 'User cancelled') { + console.log( + chalk.yellow( + '\nNo input received. Proceeding with Old Architecture by default. You can opt into the New Architecture later.', + ), + ); + return {shouldContinueWithOldArch: true, userCancelled: true}; + } + + // retry on invalid input + attempts++; + if (attempts < maxRetries) { + console.log( + chalk.red("Invalid input. Please enter 'Y' for Yes or 'N' for No."), + ); + } + } + } + + // Max retries reached + console.log( + chalk.yellow( + `\nMax retries reached. Proceeding with Old Architecture by default. You can opt into the New Architecture later.`, + ), + ); + return {shouldContinueWithOldArch: true, userCancelled: false}; +}