Skip to content

Commit cd5ebac

Browse files
authored
Merge branch 'master' into cdp-node-version-compatibility
2 parents 94e8327 + 5aa8bf8 commit cd5ebac

7 files changed

Lines changed: 94 additions & 11 deletions

File tree

src/common/nodeHelper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export async function getNodeVersion(projectPath: string, env: object) {
1010
return "";
1111
}
1212
}
13+
14+
// Note: Additional helpers are not required for CDP messaging improvement.

src/common/projectVersionHelper.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,32 @@ export function RNPackageVersionsToPackageVersion(
5050

5151
export class ProjectVersionHelper {
5252
private static SEMVER_INVALID = "SemverInvalid";
53+
/**
54+
* Reads required Node.js semver range from react-native's package.json engines.node.
55+
* Falls back to null if not found.
56+
*/
57+
public static async getReactNativeRequiredNodeRange(
58+
projectRoot: string,
59+
nodeModulesRoot?: string,
60+
): Promise<string | null> {
61+
try {
62+
if (!nodeModulesRoot) {
63+
nodeModulesRoot = AppLauncher.getNodeModulesRootByProjectPath(projectRoot);
64+
}
65+
const rnPackage = new Package(nodeModulesRoot);
66+
const rnPkgJson = await rnPackage
67+
.dependencyPackage(REACT_NATIVE_PACKAGES.REACT_NATIVE.packageName)
68+
.parsePackageInformation();
69+
const engines = rnPkgJson && rnPkgJson.engines;
70+
const nodeRange = engines && engines.node;
71+
if (typeof nodeRange === "string" && nodeRange.trim().length > 0) {
72+
return nodeRange.trim();
73+
}
74+
return null;
75+
} catch {
76+
return null;
77+
}
78+
}
5379

5480
public static getRNVersionsWithBrokenMetroBundler(): string[] {
5581
// https://github.com/microsoft/vscode-react-native/issues/660 for details

src/debugger/rnDebugSession.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { logger } from "@vscode/debugadapter";
88
import { DebugProtocol } from "vscode-debugprotocol";
99
import * as nls from "vscode-nls";
1010
import { ProjectVersionHelper } from "../common/projectVersionHelper";
11+
import { InternalErrorCode } from "../common/error/internalErrorCode";
12+
import * as semver from "semver";
1113
import { TelemetryHelper } from "../common/telemetryHelper";
1214
import { RnCDPMessageHandler } from "../cdp-proxy/CDPMessageHandlers/rnCDPMessageHandler";
1315
import { ErrorHelper } from "../common/error/errorHelper";
14-
import { InternalErrorCode } from "../common/error/internalErrorCode";
16+
1517
import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
1618
import { MultipleLifetimesAppWorker } from "./appWorker";
1719
import {
@@ -60,6 +62,21 @@ export class RNDebugSession extends DebugSessionBase {
6062
if (launchArgs.platform != "exponent") {
6163
await ReactNativeProjectHelper.verifyMetroConfigFile(launchArgs.cwd);
6264
}
65+
const requiredNodeRange =
66+
await ProjectVersionHelper.getReactNativeRequiredNodeRange(launchArgs.cwd);
67+
if (requiredNodeRange) {
68+
const currentNode = process.version;
69+
const coerced = semver.coerce(currentNode);
70+
if (
71+
!coerced ||
72+
!semver.satisfies(coerced, requiredNodeRange, { includePrerelease: true })
73+
) {
74+
throw ErrorHelper.getInternalError(
75+
InternalErrorCode.CouldNotAttachToDebugger,
76+
`Incompatible Node.js for React Native: detected ${currentNode}; required ${requiredNodeRange}. Install a supported Node.js or change React Native version.`,
77+
);
78+
}
79+
}
6380
await this.initializeSettings(launchArgs);
6481
logger.log("Launching the application");
6582
logger.verbose(`Launching the application: ${JSON.stringify(launchArgs, null, 2)}`);
@@ -102,6 +119,24 @@ export class RNDebugSession extends DebugSessionBase {
102119

103120
return new Promise<void>(async (resolve, reject) => {
104121
try {
122+
const requiredNodeRange =
123+
await ProjectVersionHelper.getReactNativeRequiredNodeRange(attachArgs.cwd);
124+
if (requiredNodeRange) {
125+
const currentNode = process.version;
126+
const coerced = semver.coerce(currentNode);
127+
if (
128+
!coerced ||
129+
!semver.satisfies(coerced, requiredNodeRange, { includePrerelease: true })
130+
) {
131+
reject(
132+
ErrorHelper.getInternalError(
133+
InternalErrorCode.CouldNotAttachToDebugger,
134+
`Incompatible Node.js for React Native: detected ${currentNode}; required ${requiredNodeRange}. Install a supported Node.js or change React Native version.`,
135+
),
136+
);
137+
return;
138+
}
139+
}
105140
await this.initializeSettings(attachArgs);
106141
logger.log("Attaching to the application");
107142
logger.verbose(

src/extension/services/validationService/checks/nodeJS.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for details.
33

4+
import * as path from "path";
45
import * as nls from "vscode-nls";
56
import {
67
basicCheck,
78
createNotFoundMessage,
89
createVersionErrorMessage,
910
parseVersion,
1011
} from "../util";
12+
import { ProjectVersionHelper } from "../../../../common/projectVersionHelper";
1113
import { ValidationCategoryE, IValidation, ValidationResultT } from "./types";
1214

1315
nls.config({
@@ -20,9 +22,19 @@ const toLocale = nls.loadMessageBundle();
2022
const label = "Node.JS";
2123

2224
async function test(): Promise<ValidationResultT> {
25+
// Determine RN-required Node range if available
26+
const projectRoot = process.cwd();
27+
const nodeModulesRoot = path.join(projectRoot, "node_modules");
28+
const rnNodeRange = await ProjectVersionHelper.getReactNativeRequiredNodeRange(
29+
projectRoot,
30+
nodeModulesRoot,
31+
);
32+
33+
const requiredRange = rnNodeRange || "12.0.0";
34+
2335
const result = await basicCheck({
2436
command: "node",
25-
versionRange: "12.0.0",
37+
versionRange: requiredRange,
2638
getVersion: parseVersion.bind(null, "node --version"),
2739
});
2840

@@ -43,9 +55,9 @@ async function test(): Promise<ValidationResultT> {
4355
if (result.versionCompare === -1) {
4456
return {
4557
status: "failure",
46-
comment:
47-
"Detected version is older than 12.0.0 " +
48-
`Minimal required version is 12.0.0. Please update your ${label} installation`,
58+
comment: rnNodeRange
59+
? `Detected Node.js version is incompatible with React Native. Required range: ${requiredRange}. Please install a supported Node.js version for your project's React Native.`
60+
: "Detected version is older than 12.0.0 Minimal required version is 12.0.0. Please update your Node.JS installation",
4961
};
5062
}
5163

@@ -56,7 +68,7 @@ const main: IValidation = {
5668
label,
5769
description: toLocale(
5870
"NodejsCheckDescription",
59-
"Required for code execution. Minimal version is 12",
71+
"Required for code execution. Uses React Native's required Node range when available; otherwise minimal version is 12",
6072
),
6173
category: ValidationCategoryE.Common,
6274
exec: test,

test/extension/tipsNotificationService/tipsNotificationService.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ suite("tipNotificationService", function () {
3434
let tipNotificationService: TipNotificationService;
3535

3636
setup(async function () {
37+
this.timeout(5000);
3738
tipNotificationService = TipNotificationService.getInstance();
3839
config.delete(tipsConfigName);
3940
await SettingsHelper.setShowTips(true);
@@ -47,7 +48,8 @@ suite("tipNotificationService", function () {
4748
});
4849

4950
suite("initializeTipsConfig", function () {
50-
test("should create correct tips config", async () => {
51+
test("should create correct tips config", async function () {
52+
this.timeout(5000);
5153
await (<any>tipNotificationService).initializeTipsConfig();
5254
const tipsConfig: TipsConfig = (<any>tipNotificationService).tipsConfig;
5355

test/smoke/suites/activation.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ export function startExtensionActivationTests(): void {
4242
try {
4343
await ElementHelper.WaitElementSelectorVisible(
4444
`[id="${Constant.previewExtensionId}"]`,
45-
2000,
45+
10000,
4646
);
4747
SmokeTestLogger.info("React-native preview extension is activated");
4848
isActivited = true;
4949
} catch {
5050
try {
5151
await ElementHelper.WaitElementSelectorVisible(
5252
`[id="${Constant.prodExtensionId}"]`,
53-
2000,
53+
10000,
5454
);
5555
SmokeTestLogger.info("React-native prod extension is activated");
5656
isActivited = true;

test/smoke/suites/helper/application.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class Application {
5454
timeout: 10000,
5555
});
5656

57-
await utilities.sleep(5000);
57+
await utilities.sleep(10000);
5858
return this.mainPage;
5959
}
6060

@@ -74,7 +74,13 @@ export class Application {
7474

7575
async installExtensionFromVSIX(vscodeExecutablePath: string): Promise<void> {
7676
const [cliPath, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
77-
args.push(`--extensions-dir=${this.extensionDirectory}`);
77+
// Remove existing --extensions-dir if present to avoid duplication
78+
const extensionsDirIndex = args.findIndex(arg => arg.startsWith("--extensions-dir="));
79+
if (extensionsDirIndex >= 0) {
80+
args[extensionsDirIndex] = `--extensions-dir=${this.extensionDirectory}`;
81+
} else {
82+
args.push(`--extensions-dir=${this.extensionDirectory}`);
83+
}
7884
let extensionFile = utilities.findFile(this.vsixDirectory, /.*\.(vsix)/);
7985
if (!extensionFile) {
8086
throw new Error(`React Native extension .vsix is not found in ${this.vsixDirectory}`);

0 commit comments

Comments
 (0)