diff --git a/package.json b/package.json index 7b4927c16..125b69555 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "onDebugResolve:reactnative", "onDebugResolve:reactnativedirect", "onDebugDynamicConfigurations:reactnative", - "onDebugInitialConfigurations" + "onDebugInitialConfigurations", + "onCommand:reactNative.cleanRestartPackager" ], "main": "./dist/rn-extension", "contributes": { @@ -187,6 +188,12 @@ "category": "React Native", "enablement": "!config.security.workspace.trust.enabled || isWorkspaceTrusted" }, + { + "command": "reactNative.cleanRestartPackager", + "title": "%reactNative.command.cleanRestartPackager.title%", + "category": "React Native", + "enablement": "!config.security.workspace.trust.enabled || isWorkspaceTrusted" + }, { "command": "reactNative.publishToExpHost", "title": "%reactNative.command.publishToExpHost.title%", diff --git a/package.nls.json b/package.nls.json index c64b159b8..ca2fe7fdf 100644 --- a/package.nls.json +++ b/package.nls.json @@ -17,6 +17,7 @@ "reactNative.command.startPackager.title": "Start Packager", "reactNative.command.stopPackager.title": "Stop Packager", "reactNative.command.restartPackager.title": "Restart Packager", + "reactNative.command.cleanRestartPackager.title": "Clean & Restart Packager (Metro)", "reactNative.command.publishToExpHost.title": "Expo - Publish", "reactNative.command.createExpoEASBuildConfigFile.title": "Expo - Create EAS config file", "reactNative.command.openEASProjectInWebPage.title": "Expo - Open the EAS project in a web page", diff --git a/src/common/error/internalErrorCode.ts b/src/common/error/internalErrorCode.ts index 2b7e18aa0..29a4af525 100644 --- a/src/common/error/internalErrorCode.ts +++ b/src/common/error/internalErrorCode.ts @@ -12,33 +12,34 @@ export enum InternalErrorCode { FailedToStopPackager = 107, PackagerRunningInDifferentPort = 108, FailedToRestartPackager = 109, - FailedToRunExponent = 110, - FailedToPublishToExpHost = 111, - UnsupportedCommandStatus = 112, - CommandFailedWithDetails = 113, - FailedToRunOnWindows = 114, - FailedToRunOnMacOS = 115, - DebuggingCommandFailed = 116, - FailedToTestDevEnvironment = 117, - CommandCanceled = 118, - FailedToConfigEASBuild = 119, - FailedToOpenProjectPage = 120, - FailedToRevertOpenModule = 121, - FailedToOpenRNUpgradeHelper = 122, - FailedToInstallExpoGo = 123, - FailedToLaunchExpoWeb = 124, - FailedToRunRNDoctor = 125, - FailedToRunExpoDoctor = 126, - FailedToRunPrebuild = 127, - FailedToRunPrebuildClean = 128, - FailedToReopenQRCode = 129, - FailedToEnableHermes = 130, - FailedToEnableExpoHermes = 131, - FailedToOpenExpoUpgradeHelper = 132, - FailedToKillPort = 133, - FaiedToSetNewArch = 134, - FailedToToggleNetworkView = 135, - FailedToRunEasBuild = 136, + FailedToCleanRestartPackager = 110, + FailedToRunExponent = 111, + FailedToPublishToExpHost = 112, + UnsupportedCommandStatus = 113, + CommandFailedWithDetails = 114, + FailedToRunOnWindows = 115, + FailedToRunOnMacOS = 116, + DebuggingCommandFailed = 117, + FailedToTestDevEnvironment = 118, + CommandCanceled = 119, + FailedToConfigEASBuild = 120, + FailedToOpenProjectPage = 121, + FailedToRevertOpenModule = 122, + FailedToOpenRNUpgradeHelper = 123, + FailedToInstallExpoGo = 124, + FailedToLaunchExpoWeb = 125, + FailedToRunRNDoctor = 126, + FailedToRunExpoDoctor = 127, + FailedToRunPrebuild = 128, + FailedToRunPrebuildClean = 129, + FailedToReopenQRCode = 130, + FailedToEnableHermes = 131, + FailedToEnableExpoHermes = 132, + FailedToOpenExpoUpgradeHelper = 133, + FailedToKillPort = 134, + FaiedToSetNewArch = 135, + FailedToToggleNetworkView = 136, + FailedToRunEasBuild = 137, // Device Deployer errors IOSDeployNotFound = 201, diff --git a/src/extension/commands/cleanRestartPackager.ts b/src/extension/commands/cleanRestartPackager.ts new file mode 100644 index 000000000..772ce227f --- /dev/null +++ b/src/extension/commands/cleanRestartPackager.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import * as assert from "assert"; +import * as path from "path"; +import * as fs from "fs"; +import { ErrorHelper } from "../../common/error/errorHelper"; +import { InternalErrorCode } from "../../common/error/internalErrorCode"; +import { ProjectVersionHelper } from "../../common/projectVersionHelper"; +import { SettingsHelper } from "../settingsHelper"; +import { ReactNativeCommand } from "./util/reactNativeCommand"; +import { ChildProcess } from "../../common/node/childProcess"; +import { OutputChannelLogger } from "../log/OutputChannelLogger"; +import { HostPlatform, HostPlatformId } from "../../common/hostPlatform"; + +const logger = OutputChannelLogger.getMainChannel(); + +export class CleanRestartPackager extends ReactNativeCommand { + codeName = "cleanRestartPackager"; + label = "Clean & Restart Packager"; + error = ErrorHelper.getInternalError(InternalErrorCode.FailedToCleanRestartPackager); + + async baseFn(): Promise { + assert(this.project); + const nodeModulesRoot = this.project.getOrUpdateNodeModulesRoot(); + await ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(nodeModulesRoot); + + const projectPath = this.project.getPackager().getProjectPath(); + const packagerPort = SettingsHelper.getPackagerPort( + this.project.getWorkspaceFolderUri().fsPath, + ); + + logger.info("Starting Metro Packager cleanup and restart..."); + + // Step 1: Kill Metro process on port 8081 + await this.killMetroProcess(packagerPort); + + // Step 2: Clean Metro cache + await this.cleanMetroCache(projectPath); + + // Step 3: Clean Watchman cache (if available) + await this.cleanWatchmanCache(); + + // Step 4: Restart packager with reset cache + logger.info("Restarting Packager with clean cache..."); + await this.project.getPackager().restart(packagerPort); + + logger.info("Metro Packager cleanup and restart completed successfully."); + } + + private async killMetroProcess(port: number): Promise { + logger.info(`Step 1/3: Terminating Metro process on port ${port}...`); + + try { + const platformId = HostPlatform.getPlatformId(); + const childProcess = new ChildProcess(); + + if (platformId === HostPlatformId.WINDOWS) { + // Windows: Use netstat and taskkill + try { + const netstatResult = await childProcess.exec( + `netstat -ano | findstr :${port}`, + ); + const outcome = await netstatResult.outcome; + + if (outcome) { + // Extract PID from netstat output + const lines = outcome.split("\n"); + for (const line of lines) { + const match = line.match(/\s+LISTENING\s+(\d+)/); + if (match && match[1]) { + const pid = match[1]; + logger.info(`Found Metro process with PID: ${pid}`); + await childProcess.exec(`taskkill /PID ${pid} /F /T`); + logger.info(`Successfully terminated process ${pid}`); + } + } + } + } catch (error) { + logger.info(`No Metro process found on port ${port}`); + } + } else { + // macOS/Linux: Use lsof and kill + try { + const lsofResult = await childProcess.exec(`lsof -ti:${port}`); + const outcome = await lsofResult.outcome; + + if (outcome && outcome.trim()) { + const pid = outcome.trim(); + logger.info(`Found Metro process with PID: ${pid}`); + await childProcess.exec(`kill -9 ${pid}`); + logger.info(`Successfully terminated process ${pid}`); + } + } catch (error) { + logger.info(`No Metro process found on port ${port}`); + } + } + } catch (error) { + logger.warning(`Failed to kill Metro process: ${error}`); + } + } + + private async cleanMetroCache(projectPath: string): Promise { + logger.info("Step 2/3: Cleaning Metro cache..."); + + const metroCachePath = path.join(projectPath, "node_modules", ".cache", "metro"); + + try { + if (fs.existsSync(metroCachePath)) { + // Use recursive directory deletion + await this.deleteDirectory(metroCachePath); + logger.info(`Successfully cleaned Metro cache at: ${metroCachePath}`); + } else { + logger.info("Metro cache directory not found, skipping..."); + } + } catch (error) { + logger.warning(`Failed to clean Metro cache: ${error}`); + } + } + + private async cleanWatchmanCache(): Promise { + logger.info("Step 3/3: Cleaning Watchman cache..."); + + try { + const childProcess = new ChildProcess(); + const watchmanResult = await childProcess.exec("watchman watch-del-all"); + await watchmanResult.outcome; + logger.info("Successfully cleaned Watchman cache"); + } catch (error) { + logger.info("Watchman not available or failed to clean cache, continuing..."); + } + } + + private async deleteDirectory(dirPath: string): Promise { + if (fs.existsSync(dirPath)) { + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = path.join(dirPath, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + await this.deleteDirectory(filePath); + } else { + fs.unlinkSync(filePath); + } + } + + fs.rmdirSync(dirPath); + } + } +} diff --git a/src/extension/commands/index.ts b/src/extension/commands/index.ts index 3493b00c6..5a9e5c822 100644 --- a/src/extension/commands/index.ts +++ b/src/extension/commands/index.ts @@ -11,6 +11,7 @@ export * from "./networkInspector"; export * from "./publishToExpHost"; export * from "./reloadApp"; export * from "./restartPackager"; +export * from "./cleanRestartPackager"; export * from "./runAndroid"; export * from "./runExponent"; export * from "./runIos"; diff --git a/test/extension/rn-extension.test.ts b/test/extension/rn-extension.test.ts index 0164d6b98..8efc5e1dc 100644 --- a/test/extension/rn-extension.test.ts +++ b/test/extension/rn-extension.test.ts @@ -154,6 +154,7 @@ suite("rn-extension", function () { "reactNative.publishToExpHost", "reactNative.reloadApp", "reactNative.restartPackager", + "reactNative.cleanRestartPackager", "reactNative.runAndroidDevice", "reactNative.runAndroidSimulator", "reactNative.runExponent",