// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as fs from "fs"; import * as path from "path"; import * as semver from "semver"; import { OutputChannelLogger } from "../extension/log/OutputChannelLogger"; import { ProjectVersionHelper } from "./projectVersionHelper"; import { FileSystem } from "./node/fileSystem"; import { stripJsonTrailingComma } from "./utils"; export interface ParsedPackage { packageName: string; useSemverCoerce: boolean; } export class ReactNativeProjectHelper { /** * Ensures that we are in a React Native project * Otherwise, displays an error message banner */ public static async isReactNativeProject(projectRoot: string): Promise { if (!projectRoot || !fs.existsSync(path.join(projectRoot, "package.json"))) { return false; } const versions = await ProjectVersionHelper.getReactNativeVersions( projectRoot, undefined, projectRoot, ); return !ProjectVersionHelper.isVersionError(versions.reactNativeVersion); } public static isHaulProject(projectRoot: string): boolean { const packageJsonPath = path.join(projectRoot, "package.json"); if (!projectRoot || !fs.existsSync(packageJsonPath)) { return false; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const haulVersion = packageJson.devDependencies && (packageJson.devDependencies.haul || packageJson.devDependencies["@haul-bundler/cli"]); return !!haulVersion; } public static isAndroidHermesEnabled(projectRoot: string): boolean { const buildGradlePath = path.join(projectRoot, "android", "app", "build.gradle"); if (!projectRoot || !fs.existsSync(buildGradlePath)) { return false; } const buildGradleContent = fs.readFileSync(buildGradlePath, "utf-8"); const hermesEnabled = /enableHermes\s*:\s*true/.test(buildGradleContent); return hermesEnabled; } public static isIOSHermesEnabled(projectRoot: string): boolean { const podfilePath = path.join(projectRoot, "ios", "Podfile"); if (!projectRoot || !fs.existsSync(podfilePath)) { return false; } const podfileContent = fs.readFileSync(podfilePath, "utf-8"); const matches = podfileContent.match(/#?\s*:hermes_enabled\s*=>\s*true/); return !!(matches && !matches[0].startsWith("#")); } public static isMacOSHermesEnabled(projectRoot: string): boolean { const podfilePath = path.join(projectRoot, "macos", "Podfile"); if (!projectRoot || !fs.existsSync(podfilePath)) { return false; } const podfileContent = fs.readFileSync(podfilePath, "utf-8"); let matches = podfileContent.match(/#?\s*:hermes_enabled\s*=>\s*(true|false)/); if (matches && matches.length > 1) { return !matches[0].startsWith("#") && matches[1] === "true"; } matches = podfileContent.match(/#?\s*pod\s*'hermes'/); return !!(matches && !matches[0].startsWith("#")); } public static isWindowsHermesEnabled(projectRoot: string): boolean { const experimentalFeaturesPath = path.join( projectRoot, "windows", "ExperimentalFeatures.props", ); if (!projectRoot || !fs.existsSync(experimentalFeaturesPath)) { return false; } const experimentalFeaturesContent = fs.readFileSync(experimentalFeaturesPath, "utf-8"); const hermesEnabled = /\s*true\s*<\/UseHermes>/.test( experimentalFeaturesContent, ); return hermesEnabled; } public static async UpdateMertoBundlerForExpoWeb(launchArgs: any) { const appJsonPath = path.join(launchArgs.cwd, "app.json"); const fs = new FileSystem(); const appJson = await fs.readFile(appJsonPath).then(content => { return stripJsonTrailingComma(content.toString()); }); if (!appJson.expo.web.bundler) { appJson.expo.web.bundler = "metro"; await fs.writeFile(appJsonPath, JSON.stringify(appJson, null, 2)); } } public static async verifyMetroConfigFile(projectRoot: string) { const logger = OutputChannelLogger.getChannel(OutputChannelLogger.MAIN_CHANNEL_NAME, true); let version; try { version = await ProjectVersionHelper.getReactNativeVersions(projectRoot); } catch { version = await ProjectVersionHelper.getReactNativeVersions( projectRoot, undefined, projectRoot, ); } let content = ""; if (fs.existsSync(path.join(projectRoot, "metro.config.js"))) { content = fs.readFileSync(path.join(projectRoot, "metro.config.js"), "utf-8"); } else if (fs.existsSync(path.join(projectRoot, "metro.config.cjs"))) { content = fs.readFileSync(path.join(projectRoot, "metro.config.cjs"), "utf-8"); } else { return; } const isNewMetroConfig = content.includes("getDefaultConfig"); if (semver.gte(version.reactNativeVersion, "0.73.0") && !isNewMetroConfig) { logger.warning( 'The version of "metro.config" in current project is deprecated, it may cause project build failure. Please update your "metro.config.js" file according to template: https://github.com/facebook/react-native/blob/main/packages/react-native/template/metro.config.js', ); } } }