Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -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/telemetry",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
} from '../../utils/telemetryHelpers';
import {copyAndReplaceWithChangedCallback} from '../../generator-common';
import * as nameHelpers from '../../utils/nameHelpers';
import {showOldArchitectureWarning} from '../../utils/oldArchWarning';
import {promptForArchitectureChoice} from '../../utils/architecturePrompt';
import type {InitOptions} from './initWindowsOptions';
import {initOptions} from './initWindowsOptions';

Expand Down Expand Up @@ -154,6 +156,9 @@ export class InitWindows {
return;
}

const userDidNotPassTemplate = !process.argv.some(arg =>
arg.startsWith('--template'),
);
this.options.template ??=
(this.rnwConfig?.['init-windows']?.template as string | undefined) ??
this.getDefaultTemplateName();
Expand All @@ -166,10 +171,23 @@ 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 isOldArchTemplate = this.options.template.startsWith('old');
const promptFlag = this.options.prompt;

if (isOldArchTemplate) {
showOldArchitectureWarning();

if (userDidNotPassTemplate && promptFlag) {
const promptResult = await promptForArchitectureChoice();

if (
!promptResult.shouldContinueWithOldArch &&
!promptResult.userCancelled
) {
spinner.info('Switching to New Architecture template (cpp-app)...');
this.options.template = 'cpp-app';
}
}
}

const templateConfig = this.templates.get(this.options.template)!;
Expand Down Expand Up @@ -303,6 +321,7 @@ function optionSanitizer(key: keyof InitOptions, value: any): any {
case 'overwrite':
case 'telemetry':
case 'list':
case 'prompt':
return value === undefined ? false : value; // Return value
}
}
Expand All @@ -316,6 +335,19 @@ async function getExtraProps(): Promise<Record<string, any>> {
return extraProps;
}

function sanitizeOptions(
opts: Record<string, any>,
sanitizer: (key: keyof InitOptions, value: any) => any,
): Record<string, any> {
const sanitized: Record<string, any> = {};
for (const key in opts) {
if (Object.prototype.hasOwnProperty.call(opts, key)) {
sanitized[key] = sanitizer(key as keyof InitOptions, opts[key]);
}
}
return sanitized;
}

/**
* The function run when calling `npx @react-native-community/cli init-windows`.
* @param args Unprocessed args passed from react-native CLI.
Expand All @@ -336,6 +368,7 @@ async function initWindows(
);

let initWindowsError: Error | undefined;

try {
await initWindowsInternal(args, config, options);
} catch (ex) {
Expand All @@ -344,7 +377,11 @@ async function initWindows(
Telemetry.trackException(initWindowsError);
}

await endTelemetrySession(initWindowsError, getExtraProps);
// Now, instead of custom fields, just pass the final options object
await endTelemetrySession(initWindowsError, getExtraProps, options, opts =>
sanitizeOptions(opts, optionSanitizer),
);

setExitProcessWithError(options.logging, initWindowsError);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface InitOptions {
overwrite?: boolean;
telemetry?: boolean;
list?: boolean;
prompt?: boolean;
}

export const initOptions: CommandOption[] = [
Expand Down Expand Up @@ -52,4 +53,8 @@ export const initOptions: CommandOption[] = [
description:
'Shows a list with all available templates with their descriptions.',
},
{
name: '--no-prompt',
description: 'Skip any interactive prompts and use default choices.',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {RunWindowsOptions} from './runWindowsOptions';
import {runWindowsOptions} from './runWindowsOptions';
import {autolinkWindowsInternal} from '../autolinkWindows/autolinkWindows';
import type {AutoLinkOptions} from '../autolinkWindows/autolinkWindowsOptions';
import {showOldArchitectureWarning} from '../../utils/oldArchWarning';

/**
* Sanitizes the given option for telemetry.
Expand Down Expand Up @@ -208,9 +209,7 @@ 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.',
);
showOldArchitectureWarning();
}

// Get the solution file
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Copyright (c) Microsoft Corporation.
* 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<typeof prompts>;

describe('architecturePrompt', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('returns true when user chooses Y', async () => {
mockPrompts.mockResolvedValue({choice: 'y'});

const result = await promptForArchitectureChoice();

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();

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();

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();

expect(result.shouldContinueWithOldArch).toBe(false);
expect(result.userCancelled).toBe(false);
});

test('returns true with userCancelled when user cancels with no input', async () => {
mockPrompts.mockResolvedValue({});

const result = await promptForArchitectureChoice();

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();

expect(result.shouldContinueWithOldArch).toBe(true);
expect(result.userCancelled).toBe(true);
});

test('returns true and not cancelled on other errors', async () => {
mockPrompts.mockRejectedValue(new Error('Some other error'));

const result = await promptForArchitectureChoice();

expect(result.shouldContinueWithOldArch).toBe(true);
expect(result.userCancelled).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function validateOptionName(
case 'overwrite':
case 'telemetry':
case 'list':
case 'prompt':
return true;
}
throw new Error(
Expand Down Expand Up @@ -55,6 +56,7 @@ test('initOptions - validate options', () => {

// Validate all command options are present in InitOptions
const optionName = commanderNameToOptionName(commandOption.name);

expect(
validateOptionName(commandOption.name, optionName as keyof InitOptions),
).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyright notice is inconsistent with other test files. It should be 'Copyright (c) Microsoft Corporation. All rights reserved.' to match the pattern used in the test file.

Suggested change
* Licensed under the MIT License.
* Copyright (c) Microsoft Corporation. All rights reserved.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disregard this review comment. The correct copyright that we use in all files is:

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

export async function promptForArchitectureChoice(): Promise<ArchitecturePromptResult> {
try {
const response = await 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();
if (normalized === 'y' || normalized === 'n') {
return true;
}
return "Invalid input. Please enter 'Y' for Yes or 'N' for No.";
},
},
{
onCancel: () => {
throw new Error('User cancelled');
},
},
);

if (!response.choice) {
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 {
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') {
return {shouldContinueWithOldArch: true, userCancelled: true};
}
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};
}
}
31 changes: 31 additions & 0 deletions packages/@react-native-windows/cli/src/utils/oldArchWarning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
* @format
*/

import chalk from 'chalk';

/**
* Displays a warning message about the Old Architecture template.
*/
export function showOldArchitectureWarning(): void {
console.log(
chalk.yellow(
`⚠️ This project is using the React Native (for Windows) Old Architecture. The old architecture will begin to be removed starting with [email protected].`,
),
);
console.log();
console.log(
chalk.cyan(
'💡 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.',
),
);
console.log();
console.log(
chalk.blue(
'🔗 Learn more: https://microsoft.github.io/react-native-windows/docs/new-architecture',
),
);
console.log();
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ export async function startTelemetrySession(
export async function endTelemetrySession(
error?: Error,
getExtraProps?: () => Promise<Record<string, any>>,
finalOptions?: Record<string, any>,
optionSanitizer?: (opts: Record<string, any>) => Record<string, any>,
) {
if (!Telemetry.isEnabled()) {
// Bail early so don't waste time here
Expand All @@ -166,8 +168,13 @@ export async function endTelemetrySession(
error instanceof CodedError ? (error as CodedError).type : 'Unknown';
}

Telemetry.endCommand(
endInfo,
getExtraProps ? await getExtraProps() : undefined,
);
// Get extra properties
const extraProps = getExtraProps ? await getExtraProps() : undefined;

// Sanitize and attach finalOptions if provided
if (finalOptions && optionSanitizer) {
endInfo.finalOptions = optionSanitizer(finalOptions);
}

Telemetry.endCommand(endInfo, extraProps);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface CommandStartInfo {

export interface CommandEndInfo {
resultCode: errorUtils.CodedErrorType;
finalOptions?: Record<string, any>;
}

interface CommandInfo {
Expand Down
Loading