Repository: microsoft/react-native-test-app
Pull request #1234 (2022-11-29 13:47:07)

ci: add code scanning
### Description

Add [CodeQL](https://codeql.github.com/) to CI to automatically detect common vulnerability and coding errors.

### Platforms affected

- [ ] Android
- [ ] iOS
- [ ] macOS
- [ ] Windows

### Test plan

The new pipeline should be green.


Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..dc689fe
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,36 @@
+name: CodeQL
+on:
+  push:
+    branches:
+      - trunk
+      - releases/*
+  pull_request:
+    branches:
+      - trunk
+  schedule:
+    - cron: 0 4 * * 1
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+    strategy:
+      fail-fast: false
+      matrix:
+        language: ["javascript", "ruby"]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@v2
+        with:
+          languages: ${{ matrix.language }}
+      - name: Autobuild
+        uses: github/codeql-action/autobuild@v2
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@v2
+        with:
+          category: "/language:${{matrix.language}}"
diff --git a/package.json b/package.json
index 5f89ae4..45206e9 100644
--- a/package.json
+++ b/package.json
@@ -27,12 +27,13 @@
     "/android/app/src/!(test)",
     "/android/support/src",
     "/common",
-    "/example/.gitignore",
+    "/example/_gitignore",
     "/example/android/gradle*",
-    "/example/windows/.gitignore",
+    "/example/windows/_gitignore",
     "/example/{metro,react-native}.config.js",
     "/ios",
     "/macos",
+    "/plugins",
     "/schema.json",
     "/scripts/*.js",
     "/scripts/apply-config-plugins.mjs",
@@ -43,6 +44,7 @@
   "main": "scripts/configure.js",
   "bin": {
     "configure-test-app": "scripts/configure.js",
+    "init": "scripts/init.js",
     "init-test-app": "scripts/init.js",
     "install-windows-test-app": "windows/test-app.js"
   },
@@ -62,6 +64,8 @@
     "lint:kt": "ktlint --android --relative 'android/app/src/**/*.kt'",
     "lint:rb": "bundle exec rubocop",
     "lint:swift": "swiftlint",
+    "prepack": "node scripts/pack.mjs pre",
+    "postpack": "node scripts/pack.mjs post",
     "set-react-version": "node scripts/set-react-version.mjs",
     "test:js": "jest",
     "test:rb": "bundle exec ruby -Ilib:test -e \"Dir.glob('test/test_*.rb').each { |file| require_relative file }\""
diff --git a/plugins/index.js b/plugins/index.js
new file mode 100644
index 0000000..d59b7d2
--- /dev/null
+++ b/plugins/index.js
@@ -0,0 +1,38 @@
+// @ts-check
+const { withMod } = require("@expo/config-plugins");
+
+/**
+ * @typedef {import("@expo/config-plugins").ExportedConfig} ExportedConfig
+ * @typedef {import("@expo/config-plugins").Mod} Mod
+ */
+
+/**
+ * Provides the `BridgeDelegate` file for modification.
+ * @param {ExportedConfig} config Exported config
+ * @param {Mod} action Method to run on the mod when the config is compiled
+ * @returns {ExportedConfig} Modified config
+ */
+function withBridgeDelegate(config, action) {
+  return withMod(config, {
+    platform: "ios",
+    mod: "bridgeDelegate",
+    action,
+  });
+}
+
+/**
+ * Provides the `SceneDelegate` file for modification.
+ * @param {ExportedConfig} config Exported config
+ * @param {Mod} action Method to run on the mod when the config is compiled
+ * @returns {ExportedConfig} Modified config
+ */
+function withSceneDelegate(config, action) {
+  return withMod(config, {
+    platform: "ios",
+    mod: "sceneDelegate",
+    action,
+  });
+}
+
+exports.withBridgeDelegate = withBridgeDelegate;
+exports.withSceneDelegate = withSceneDelegate;
diff --git a/plugins/reanimated.js b/plugins/reanimated.js
new file mode 100644
index 0000000..b464da8
--- /dev/null
+++ b/plugins/reanimated.js
@@ -0,0 +1,96 @@
+// @ts-check
+const { createRunOncePlugin } = require("@expo/config-plugins");
+const {
+  mergeContents,
+} = require("@expo/config-plugins/build/utils/generateCode");
+const { withBridgeDelegate } = require("./index");
+
+/**
+ * @typedef {import("@expo/config-plugins").ExportedConfig} ExportedConfig
+ */
+
+const NAME = "react-native-reanimated";
+
+/**
+ * Adds specified contents to an existing file with a generated header.
+ * @param {string} tag Tag used to generate a unique header
+ * @param {string} src Contents of the source file
+ * @param {string} newSrc Contents to be added
+ * @param {RegExp} anchor `RegExp` providing the position at which contents is added
+ * @returns {string} The merged content
+ */
+function addContents(tag, src, newSrc, anchor) {
+  return mergeContents({
+    tag: `${NAME}-${tag}`,
+    src,
+    newSrc,
+    anchor,
+    offset: 1,
+    comment: "//",
+  }).contents;
+}
+
+/**
+ * Plugin to inject Reanimated's JSI executor in the React bridge delegate.
+ *
+ * Only applies to iOS.
+ *
+ * @param {ExportedConfig} config Exported config
+ * @returns {ExportedConfig} Modified config
+ */
+function withReanimatedExecutor(config) {
+  return withBridgeDelegate(config, (config) => {
+    if (config.modResults.language !== "objcpp") {
+      throw new Error(
+        "`BridgeDelegate` is not in Objective-C++ (did that change recently?)"
+      );
+    }
+
+    // Add Reanimated headers
+    config.modResults.contents = addContents(
+      "header",
+      config.modResults.contents,
+      [
+        "#if !USE_TURBOMODULE",
+        "#pragma clang diagnostic push",
+        '#pragma clang diagnostic ignored "-Wnullability-completeness"',
+        "",
+        "#import <RNReanimated/REAInitializer.h>",
+        "",
+        "#if __has_include(<reacthermes/HermesExecutorFactory.h>)",
+        "#import <reacthermes/HermesExecutorFactory.h>",
+        "using ExecutorFactory = HermesExecutorFactory;",
+        "#elif __has_include(<React/HermesExecutorFactory.h>)",
+        "#import <React/HermesExecutorFactory.h>",
+        "using ExecutorFactory = HermesExecutorFactory;",
+        "#else",
+        "#import <React/JSCExecutorFactory.h>",
+        "using ExecutorFactory = JSCExecutorFactory;",
+        "#endif",
+        "",
+        "#pragma clang diagnostic pop",
+        "#endif  // !USE_TURBOMODULE",
+      ].join("\n"),
+      /#import "BridgeDelegate\.h"/
+    );
+
+    // Install Reanimated's JSI executor runtime
+    const indent = "    ";
+    config.modResults.contents = addContents(
+      "executor",
+      config.modResults.contents,
+      [
+        `${indent}const auto installer = reanimated::REAJSIExecutorRuntimeInstaller(bridge, nullptr);`,
+        `${indent}return std::make_unique<ExecutorFactory>(RCTJSIExecutorRuntimeInstaller(installer));`,
+      ].join("\n"),
+      /\/\/ jsExecutorFactoryForBridge: \(USE_TURBOMODULE=0\)/
+    );
+    return config;
+  });
+}
+
+module.exports = createRunOncePlugin(
+  withReanimatedExecutor,
+  NAME,
+  "UNVERSIONED"
+);
diff --git a/scripts/config-plugins/plugins/withIosBaseMods.mjs b/scripts/config-plugins/plugins/withIosBaseMods.mjs
index da547a4..4bc4c23 100644
--- a/scripts/config-plugins/plugins/withIosBaseMods.mjs
+++ b/scripts/config-plugins/plugins/withIosBaseMods.mjs
@@ -30,6 +30,10 @@ const defaultProviders = {
 };
 
 // `react-native-test-app` files
+defaultProviders["bridgeDelegate"] = modifyFilePath(
+  expoProviders.appDelegate,
+  "ReactTestApp/BridgeDelegate.mm"
+);
 defaultProviders["sceneDelegate"] = modifyFilePath(
   expoProviders.appDelegate,
   "ReactTestApp/SceneDelegate.swift"
diff --git a/scripts/configure.js b/scripts/configure.js
index 2222e3c..14cb321 100755
--- a/scripts/configure.js
+++ b/scripts/configure.js
@@ -34,6 +34,7 @@ const semver = require("semver");
  * @typedef {{
  *   name: string;
  *   packagePath: string;
+ *   templatePath?: string;
  *   testAppPath: string;
  *   targetVersion: string;
  *   platforms: Platform[];
@@ -407,18 +408,23 @@ const getConfig = (() => {
       typeof configuration === "undefined" ||
       "JEST_WORKER_ID" in process.env // skip caching when testing
     ) {
-      const { name, testAppPath, flatten, init } = params;
+      // Git ignore files are only renamed when published. `GIT_IGNORE_FILE`
+      // should therefore only be set during testing.
+      const gitignore = process.env["GIT_IGNORE_FILE"] || "_gitignore";
+      const { name, templatePath, testAppPath, flatten, init } = params;
       const projectPathFlag = flatten ? " --project-path ." : "";
       const testAppRelPath = projectRelativePath(params);
-      const templateDir = path.relative(
-        process.cwd(),
-        path.dirname(require.resolve("react-native/template/package.json"))
-      );
+      const templateDir =
+        templatePath ||
+        path.relative(
+          process.cwd(),
+          path.dirname(require.resolve("react-native/template/package.json"))
+        );
       configuration = {
         common: {
           files: {
             ".gitignore": {
-              source: path.join(testAppPath, "example", ".gitignore"),
+              source: path.join(testAppPath, "example", gitignore),
             },
             ".watchmanconfig": {
               source: path.join(templateDir, "_watchmanconfig"),
@@ -628,12 +634,7 @@ const getConfig = (() => {
         windows: {
           files: {
             ".gitignore": {
-              source: path.join(
-                testAppPath,
-                "example",
-                "windows",
-                ".gitignore"
-              ),
+              source: path.join(testAppPath, "example", "windows", gitignore),
             },
           },
           oldFiles: [
diff --git a/scripts/init.js b/scripts/init.js
index 729add5..8658d0d 100755
--- a/scripts/init.js
+++ b/scripts/init.js
@@ -2,10 +2,155 @@
 // @ts-check
 "use strict";
 
-(async () => {
-  const { version: targetVersion } = require("react-native/package.json");
-  const { configure } = require("./configure");
+const { spawnSync } = require("child_process");
+const path = require("path");
+
+/**
+ * @template T
+ * @param {() => T | null} fn
+ * @returns {() => T | null}
+ */
+function memo(fn) {
+  /** @type {T | null} */
+  let result;
+  return () => {
+    if (result === undefined) {
+      result = fn();
+    }
+    return result;
+  };
+}
+
+/**
+ * Invokes `npm`.
+ * @param {...string} args
+ */
+function npm(...args) {
+  const { error, stderr, stdout } = spawnSync("npm", args, {
+    encoding: "utf-8",
+  });
+  if (!stdout) {
+    if (stderr) {
+      console.error(stderr);
+    }
+    throw error;
+  }
+  return stdout.trim();
+}
+
+/**
+ * Invokes `tar xf`.
+ * @param {string} archive
+ */
+function untar(archive) {
+  return spawnSync("tar", ["xf", archive], { cwd: path.dirname(archive) });
+}
+
+/**
+ * Returns the installed `react-native` manifest, if present.
+ * @returns {string | null}
+ */
+const getInstalledReactNativeManifest = memo(() => {
+  try {
+    const options = { paths: [process.cwd()] };
+    return require.resolve("react-native/package.json", options);
+  } catch (_) {
+    return null;
+  }
+});
+
+/**
+ * Returns the installed `react-native` version, if present.
+ * @returns {string | null}
+ */
+const getInstalledVersion = memo(() => {
+  const manifestPath = getInstalledReactNativeManifest();
+  if (manifestPath) {
+    const { version } = require(manifestPath);
+    return version;
+  }
+
+  return null;
+});
+
+/**
+ * Returns the desired `react-native` version.
+ *
+ * Checks the following in order:
+ *
+ *   - Command line flag, e.g. `--version 0.70`
+ *   - Currently installed `react-native` version
+ *   - Latest version from npm
+ *
+ * @returns {string}
+ */
+function getVersion() {
+  const index = process.argv.lastIndexOf("--version");
+  if (index >= 0) {
+    return process.argv[index + 1];
+  }
+
+  const version = getInstalledVersion();
+  if (version) {
+    return version;
+  }
+
+  return npm("view", "react-native", "version");
+}
+
+/**
+ * Returns the React Native version and path to the template.
+ * @returns {Promise<[string] | [string, string]>}
+ */
+function getTemplate() {
+  return new Promise((resolve, reject) => {
+    const version = getVersion();
+    if (getInstalledVersion() === version) {
+      const rnManifest = getInstalledReactNativeManifest();
+      if (rnManifest) {
+        resolve([version, path.join(path.dirname(rnManifest), "template")]);
+        return;
+      }
+    }
+
+    // `npm view` may return an array if there are multiple versions matching
+    // `version`. If there is only one match, the return type is a string.
+    const tarballs = JSON.parse(
+      npm("view", "--json", `react-native@${version}`, "dist.tarball")
+    );
+    const url = Array.isArray(tarballs)
+      ? tarballs[tarballs.length - 1]
+      : tarballs;
+
+    console.log(`Downloading ${path.basename(url)}...`);
+
+    require("https")
+      .get(url, (res) => {
+        const fs = require("fs");
+        const os = require("os");
+
+        const tmpDir = path.join(os.tmpdir(), "react-native-test-app");
+        fs.mkdirSync(tmpDir, { recursive: true });
 
+        const dest = path.join(tmpDir, path.basename(url));
+        const file = fs.createWriteStream(dest);
+        res.pipe(file);
+        file.on("finish", () => {
+          file.close();
+
+          untar(dest);
+
+          const template = path.join(tmpDir, "package", "template");
+          resolve([version, template]);
+        });
+      })
+      .on("error", (err) => {
+        reject(err);
+      });
+  });
+}
+
+async function main() {
   /**
    * @type {{
    *   name?: string;
@@ -17,8 +162,8 @@
     {
       type: "text",
       name: "name",
-      message: "What is the name of your test app?",
-      initial: "TestApp",
+      message: "What is the name of your app?",
+      initial: "Example",
       validate: Boolean,
     },
     {
@@ -43,14 +188,16 @@
   ]);
 
   if (!name || !packagePath || !platforms) {
-    process.exit(1);
+    return 1;
   }
 
-  const path = require("path");
+  const { configure } = require("./configure");
 
-  const result = configure({
+  const [targetVersion, templatePath] = await getTemplate();
+  return configure({
     name,
     packagePath,
+    templatePath,
     testAppPath: path.resolve(__dirname, ".."),
     targetVersion,
     platforms,
@@ -58,7 +205,8 @@
     force: true,
     init: true,
   });
-  if (result !== 0) {
-    process.exit(result);
-  }
-})();
+}
+
+main().then((result) => {
+  process.exitCode = result;
+});
diff --git a/scripts/pack.mjs b/scripts/pack.mjs
new file mode 100644
index 0000000..f092fbf
--- /dev/null
+++ b/scripts/pack.mjs
@@ -0,0 +1,41 @@
+// @ts-check
+import * as fs from "fs";
+import * as path from "path";
+
+const files = ["example/.gitignore", "example/windows/.gitignore"];
+
+/**
+ * Renames `.dotfile` to `_dotfile`.
+ * @param {string} p
+ * @returns {string}
+ */
+function renameDotFile(p) {
+  return path.join(path.dirname(p), "_" + path.basename(p).substring(1));
+}
+
+/**
+ * `npm install` seems to be renaming `.gitignore` files to `.npmignore`.
+ * This breaks our templates (see
+ * https://github.com/microsoft/react-native-test-app/issues/1228). As a
+ * workaround, we'll rename the files ourselves, from `.gitignore` to
+ * `_gitignore`, to avoid any further attempts to mutate the package.
+ */
+const { [2]: script } = process.argv;
+switch (script) {
+  case "pre": {
+    for (const f of files) {
+      fs.renameSync(f, renameDotFile(f));
+    }
+    break;
+  }
+
+  case "post": {
+    for (const f of files) {
+      fs.renameSync(renameDotFile(f), f);
+    }
+    break;
+  }
+
+  default:
+    throw new Error(`No such script: ${script}`);
+}
diff --git a/yarn.lock b/yarn.lock
index ff8f69e..ff4caf2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10942,6 +10942,7 @@ fsevents@^2.3.2:
       optional: true
   bin:
     configure-test-app: scripts/configure.js
+    init: scripts/init.js
     init-test-app: scripts/init.js
     install-windows-test-app: windows/test-app.js
   languageName: unknown


Test patch:
diff --git a/ios/ReactTestApp.xcodeproj/project.pbxproj b/ios/ReactTestApp.xcodeproj/project.pbxproj
index 02ef77e..195f2aa 100644
--- a/ios/ReactTestApp.xcodeproj/project.pbxproj
+++ b/ios/ReactTestApp.xcodeproj/project.pbxproj
@@ -3,12 +3,12 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 52;
+	objectVersion = 54;
 	objects = {
 
 /* Begin PBXBuildFile section */
 		192DD201240FCAF5004E9CEB /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192DD200240FCAF5004E9CEB /* Manifest.swift */; };
-		193B614D27F5CD7D00080064 /* React+TurboModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 193B614B27F5CD7D00080064 /* React+TurboModule.mm */; };
+		193B614D27F5CD7D00080064 /* BridgeDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 193B614B27F5CD7D00080064 /* BridgeDelegate.mm */; };
 		1963A06227C82E730013D276 /* React+Fabric.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1963A06127C82E730013D276 /* React+Fabric.mm */; };
 		196C22622490CB7600449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22602490CB7600449D3C /* React+Compatibility.m */; };
 		196C7215232F1788006556ED /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C7214232F1788006556ED /* ReactInstance.swift */; };
@@ -47,8 +47,8 @@
 		192F052624AD3CC500A48456 /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
 		192F052724AD3CC500A48456 /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
 		192F052824AD3CC500A48456 /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
-		193B614B27F5CD7D00080064 /* React+TurboModule.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "React+TurboModule.mm"; sourceTree = "<group>"; };
-		193B614C27F5CD7D00080064 /* React+TurboModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+TurboModule.h"; sourceTree = "<group>"; };
+		193B614B27F5CD7D00080064 /* BridgeDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BridgeDelegate.mm; sourceTree = "<group>"; };
+		193B614C27F5CD7D00080064 /* BridgeDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BridgeDelegate.h; sourceTree = "<group>"; };
 		1963A06027C82E730013D276 /* React+Fabric.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+Fabric.h"; sourceTree = "<group>"; };
 		1963A06127C82E730013D276 /* React+Fabric.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "React+Fabric.mm"; sourceTree = "<group>"; };
 		196C22602490CB7600449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "React+Compatibility.m"; sourceTree = "<group>"; };
@@ -133,12 +133,12 @@
 				196C7214232F1788006556ED /* ReactInstance.swift */,
 				19A624A3258C95F000032776 /* Session.swift */,
 				196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */,
+				193B614C27F5CD7D00080064 /* BridgeDelegate.h */,
+				193B614B27F5CD7D00080064 /* BridgeDelegate.mm */,
 				196C22612490CB7600449D3C /* React+Compatibility.h */,
 				196C22602490CB7600449D3C /* React+Compatibility.m */,
 				1963A06027C82E730013D276 /* React+Fabric.h */,
 				1963A06127C82E730013D276 /* React+Fabric.mm */,
-				193B614C27F5CD7D00080064 /* React+TurboModule.h */,
-				193B614B27F5CD7D00080064 /* React+TurboModule.mm */,
 				1988282224105BCC005057FF /* UIViewController+ReactTestApp.h */,
 				1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */,
 				19ECD0DB232ED427003D8557 /* Assets.xcassets */,
@@ -325,13 +325,13 @@
 			buildActionMask = 2147483647;
 			files = (
 				19ECD0D6232ED425003D8557 /* AppDelegate.swift in Sources */,
+				193B614D27F5CD7D00080064 /* BridgeDelegate.mm in Sources */,
 				19ECD0DA232ED425003D8557 /* ContentView.swift in Sources */,
 				192DD201240FCAF5004E9CEB /* Manifest.swift in Sources */,
 				197827FA27710D3400AEC655 /* Manifest+Decoder.swift in Sources */,
 				196C724123319A85006556ED /* QRCodeScannerViewController.swift in Sources */,
 				196C22622490CB7600449D3C /* React+Compatibility.m in Sources */,
 				1963A06227C82E730013D276 /* React+Fabric.mm in Sources */,
-				193B614D27F5CD7D00080064 /* React+TurboModule.mm in Sources */,
 				196C7215232F1788006556ED /* ReactInstance.swift in Sources */,
 				19ECD0D8232ED425003D8557 /* SceneDelegate.swift in Sources */,
 				19A624A4258C95F000032776 /* Session.swift in Sources */,
diff --git a/ios/ReactTestApp/React+TurboModule.h b/ios/ReactTestApp/BridgeDelegate.h
similarity index 58%
rename from ios/ReactTestApp/React+TurboModule.h
rename to ios/ReactTestApp/BridgeDelegate.h
index d923770..e3ed8fd 100644
--- a/ios/ReactTestApp/React+TurboModule.h
+++ b/ios/ReactTestApp/BridgeDelegate.h
@@ -1,13 +1,9 @@
-#if USE_TURBOMODULE
-
 #import <React/RCTBridgeDelegate.h>
 
 NS_ASSUME_NONNULL_BEGIN
 
-@interface RTATurboModuleManagerDelegate : NSObject <RCTBridgeDelegate>
+@interface RTABridgeDelegate : NSObject <RCTBridgeDelegate>
 - (instancetype)initWithBridgeDelegate:(id<RCTBridgeDelegate>)bridgeDelegate;
 @end
 
 NS_ASSUME_NONNULL_END
-
-#endif  // USE_TURBOMODULE
diff --git a/ios/ReactTestApp/React+TurboModule.mm b/ios/ReactTestApp/BridgeDelegate.mm
similarity index 65%
rename from ios/ReactTestApp/React+TurboModule.mm
rename to ios/ReactTestApp/BridgeDelegate.mm
index ee07097..187f39c 100644
--- a/ios/ReactTestApp/React+TurboModule.mm
+++ b/ios/ReactTestApp/BridgeDelegate.mm
@@ -1,26 +1,24 @@
-#import "React+TurboModule.h"
+#import "BridgeDelegate.h"
 
-#if USE_TURBOMODULE
+#import <React/RCTCxxBridgeDelegate.h>
 
+#if USE_TURBOMODULE
 #import <React/CoreModulesPlugins.h>
 #import <React/RCTAppSetupUtils.h>
-#import <React/RCTCxxBridgeDelegate.h>
-#import <React/RCTDataRequestHandler.h>
-#import <React/RCTFileRequestHandler.h>
-#import <React/RCTGIFImageDecoder.h>
-#import <React/RCTHTTPRequestHandler.h>
-#import <React/RCTImageLoader.h>
-#import <React/RCTJSIExecutorRuntimeInstaller.h>
-#import <React/RCTLocalAssetImageLoader.h>
-#import <React/RCTNetworking.h>
 #import <ReactCommon/RCTTurboModuleManager.h>
 
-@interface RTATurboModuleManagerDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate>
+@interface RTABridgeDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate>
+@end
+#else
+@interface RTABridgeDelegate () <RCTCxxBridgeDelegate>
 @end
+#endif  // USE_TURBOMODULE
 
-@implementation RTATurboModuleManagerDelegate {
+@implementation RTABridgeDelegate {
     __weak id<RCTBridgeDelegate> _bridgeDelegate;
+#if USE_TURBOMODULE
     RCTTurboModuleManager *_turboModuleManager;
+#endif  // USE_TURBOMODULE
 }
 
 - (instancetype)initWithBridgeDelegate:(id<RCTBridgeDelegate>)bridgeDelegate
@@ -48,15 +46,20 @@
 - (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:
     (RCTBridge *)bridge
 {
-    if (_turboModuleManager == nil) {
-        _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
-                                                                   delegate:self
-                                                                  jsInvoker:bridge.jsCallInvoker];
-    }
+#if USE_TURBOMODULE
+    // jsExecutorFactoryForBridge: (USE_TURBOMODULE=1)
+    _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
+                                                               delegate:self
+                                                              jsInvoker:bridge.jsCallInvoker];
     return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
+#else
+    // jsExecutorFactoryForBridge: (USE_TURBOMODULE=0)
+    return nullptr;
+#endif  // USE_TURBOMODULE
 }
 
 // MARK: - RCTTurboModuleManagerDelegate details
+#if USE_TURBOMODULE
 
 - (Class)getModuleClassFromName:(const char *)name
 {
@@ -75,6 +78,6 @@
     return RCTAppSetupDefaultModuleFromClass(moduleClass);
 }
 
-@end
-
 #endif  // USE_TURBOMODULE
+
+@end
diff --git a/ios/ReactTestApp/ReactInstance.swift b/ios/ReactTestApp/ReactInstance.swift
index 2c788e0..4b7da98 100644
--- a/ios/ReactTestApp/ReactInstance.swift
+++ b/ios/ReactTestApp/ReactInstance.swift
@@ -16,12 +16,9 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
 
     private var surfacePresenterBridgeAdapter: NSObject?
     private(set) var bridge: RCTBridge?
+    private lazy var bridgeDelegate = RTABridgeDelegate(bridgeDelegate: self)
     private var bundleRoot: String?
 
-    #if USE_TURBOMODULE
-    private lazy var turboModuleManagerDelegate = RTATurboModuleManagerDelegate(bridgeDelegate: self)
-    #endif
-
     override init() {
         #if DEBUG
         remoteBundleURL = ReactInstance.jsBundleURL()
@@ -112,17 +109,10 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
             object: nil
         )
 
-        #if USE_TURBOMODULE
-        guard let bridge = RCTBridge(delegate: turboModuleManagerDelegate, launchOptions: nil) else {
-            assertionFailure("Failed to instantiate RCTBridge with TurboModule")
-            return
-        }
-        #else
-        guard let bridge = RCTBridge(delegate: self, launchOptions: nil) else {
+        guard let bridge = RCTBridge(delegate: bridgeDelegate, launchOptions: nil) else {
             assertionFailure("Failed to instantiate RCTBridge")
             return
         }
-        #endif // USE_TURBOMODULE
 
         surfacePresenterBridgeAdapter = RTACreateSurfacePresenterBridgeAdapter(bridge)
         self.bridge = bridge
diff --git a/ios/ReactTestApp/ReactTestApp-Bridging-Header.h b/ios/ReactTestApp/ReactTestApp-Bridging-Header.h
index eca5936..69c3fda 100644
--- a/ios/ReactTestApp/ReactTestApp-Bridging-Header.h
+++ b/ios/ReactTestApp/ReactTestApp-Bridging-Header.h
@@ -25,9 +25,9 @@
 #import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
 #endif  // USE_FLIPPER
 
+#import "BridgeDelegate.h"
 #import "React+Compatibility.h"
 #import "React+Fabric.h"
-#import "React+TurboModule.h"
 #import "UIViewController+ReactTestApp.h"
 
 // Generated by `validate-manifest.js`
diff --git a/macos/ReactTestApp.xcodeproj/project.pbxproj b/macos/ReactTestApp.xcodeproj/project.pbxproj
index 1a8f188..60f73a0 100644
--- a/macos/ReactTestApp.xcodeproj/project.pbxproj
+++ b/macos/ReactTestApp.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 52;
+	objectVersion = 54;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -14,6 +14,7 @@
 		193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF08E247A799D00BE8C79 /* Manifest.swift */; };
 		193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */; };
 		193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF097247B130700BE8C79 /* ReactInstance.swift */; };
+		19510B70291D717200E4CED7 /* BridgeDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19510B6F291D717200E4CED7 /* BridgeDelegate.mm */; };
 		1960F339258C97C400AEC7A2 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1960F338258C97C400AEC7A2 /* Session.swift */; };
 		1963A06527C82F7E0013D276 /* React+Fabric.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1963A06427C82F7E0013D276 /* React+Fabric.mm */; };
 		196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22632490CBAB00449D3C /* React+Compatibility.m */; };
@@ -56,6 +57,8 @@
 		193EF092247A830200BE8C79 /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+ReactTestApp.h"; path = "../ReactTestAppShared/UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
 		193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ReactTestApp-Bridging-Header.h"; path = "../ReactTestAppShared/ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
 		193EF097247B130700BE8C79 /* ReactInstance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReactInstance.swift; path = ../ReactTestAppShared/ReactInstance.swift; sourceTree = "<group>"; };
+		19510B6E291D717200E4CED7 /* BridgeDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BridgeDelegate.h; path = ../ReactTestAppShared/BridgeDelegate.h; sourceTree = "<group>"; };
+		19510B6F291D717200E4CED7 /* BridgeDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BridgeDelegate.mm; path = ../ReactTestAppShared/BridgeDelegate.mm; sourceTree = "<group>"; };
 		1960F338258C97C400AEC7A2 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Session.swift; path = ../ReactTestAppShared/Session.swift; sourceTree = "<group>"; };
 		1963A06327C82F7E0013D276 /* React+Fabric.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "React+Fabric.h"; path = "../ReactTestAppShared/React+Fabric.h"; sourceTree = "<group>"; };
 		1963A06427C82F7E0013D276 /* React+Fabric.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "React+Fabric.mm"; path = "../ReactTestAppShared/React+Fabric.mm"; sourceTree = "<group>"; };
@@ -124,6 +127,8 @@
 				193EF097247B130700BE8C79 /* ReactInstance.swift */,
 				1960F338258C97C400AEC7A2 /* Session.swift */,
 				193EF064247A736200BE8C79 /* ViewController.swift */,
+				19510B6E291D717200E4CED7 /* BridgeDelegate.h */,
+				19510B6F291D717200E4CED7 /* BridgeDelegate.mm */,
 				193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */,
 				196C22642490CBAB00449D3C /* React+Compatibility.h */,
 				196C22632490CBAB00449D3C /* React+Compatibility.m */,
@@ -317,14 +322,15 @@
 			buildActionMask = 2147483647;
 			files = (
 				193EF063247A736200BE8C79 /* AppDelegate.swift in Sources */,
+				19510B70291D717200E4CED7 /* BridgeDelegate.mm in Sources */,
 				193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */,
 				19C4C89327710D8500157870 /* Manifest+Decoder.swift in Sources */,
 				196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */,
 				1963A06527C82F7E0013D276 /* React+Fabric.mm in Sources */,
 				193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */,
 				1960F339258C97C400AEC7A2 /* Session.swift in Sources */,
-				193EF065247A736200BE8C79 /* ViewController.swift in Sources */,
 				193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */,
+				193EF065247A736200BE8C79 /* ViewController.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
diff --git a/scripts/install-test-template.sh b/scripts/install-test-template.sh
index 20805ec..2fd3633 100755
--- a/scripts/install-test-template.sh
+++ b/scripts/install-test-template.sh
@@ -35,7 +35,7 @@ done
 npm pack
 
 yarn
-npx react-native init-test-app --destination template-example --name TemplateExample --platform "$platform"
+GIT_IGNORE_FILE=".gitignore" npx react-native init-test-app --destination template-example --name TemplateExample --platform "$platform"
 
 pushd template-example 1> /dev/null
 
diff --git a/test/android-test-app/test-app-util.test.js b/test/android-test-app/test-app-util.test.js
index 9a723bd..2a550ae 100644
--- a/test/android-test-app/test-app-util.test.js
+++ b/test/android-test-app/test-app-util.test.js
@@ -43,7 +43,12 @@ describe("test-app-util", () => {
     return Number(major) * 10000 + Number(minor) * 100 + Number(patch);
   }
 
+  beforeAll(() => {
+    process.env["GIT_IGNORE_FILE"] = ".gitignore";
+  });
+
   afterAll(() => {
+    delete process.env["GIT_IGNORE_FILE"];
     removeProject(defaultTestProject);
   });
 
diff --git a/test/configure/__snapshots__/gatherConfig.test.js.snap b/test/configure/__snapshots__/gatherConfig.test.js.snap
index 45bbf47..d185a91 100644
--- a/test/configure/__snapshots__/gatherConfig.test.js.snap
+++ b/test/configure/__snapshots__/gatherConfig.test.js.snap
@@ -5,7 +5,7 @@ Object {
   "dependencies": Object {},
   "files": Object {
     ".gitignore": Object {
-      "source": "example/.gitignore",
+      "source": "example/_gitignore",
     },
     ".watchmanconfig": Object {
       "source": "node_modules/react-native/template/_watchmanconfig",
@@ -64,7 +64,7 @@ Object {
   "dependencies": Object {},
   "files": Object {
     ".gitignore": Object {
-      "source": "example/.gitignore",
+      "source": "example/_gitignore",
     },
     ".watchmanconfig": Object {
       "source": "node_modules/react-native/template/_watchmanconfig",
@@ -195,7 +195,7 @@ Object {
   "dependencies": Object {},
   "files": Object {
     ".gitignore": Object {
-      "source": "example/.gitignore",
+      "source": "example/_gitignore",
     },
     ".watchmanconfig": Object {
       "source": "node_modules/react-native/template/_watchmanconfig",
@@ -204,7 +204,7 @@ Object {
       "source": "node_modules/react-native/template/babel.config.js",
     },
     "common/.gitignore": Object {
-      "source": "example/.gitignore",
+      "source": "example/_gitignore",
     },
     "common/.watchmanconfig": Object {
       "source": "node_modules/react-native/template/_watchmanconfig",
@@ -293,7 +293,7 @@ Object {
   "dependencies": Object {},
   "files": Object {
     ".gitignore": Object {
-      "source": "example/.gitignore",
+      "source": "example/_gitignore",
     },
     ".watchmanconfig": Object {
       "source": "node_modules/react-native/template/_watchmanconfig",
@@ -366,7 +366,7 @@ Object {
   },
   "files": Object {
     ".gitignore": Object {
-      "source": "example/.gitignore",
+      "source": "example/_gitignore",
     },
     ".watchmanconfig": Object {
       "source": "node_modules/react-native/template/_watchmanconfig",
@@ -482,7 +482,7 @@ module.exports = {
 };
 ",
     "windows/.gitignore": Object {
-      "source": "example/windows/.gitignore",
+      "source": "example/windows/_gitignore",
     },
   },
   "oldFiles": Array [
@@ -517,7 +517,7 @@ Object {
   "dependencies": Object {},
   "files": Object {
     ".gitignore": Object {
-      "source": "example/.gitignore",
+      "source": "example/_gitignore",
     },
     ".watchmanconfig": Object {
       "source": "node_modules/react-native/template/_watchmanconfig",