Repository: microsoft/react-native-test-app
Pull request #1243 (2022-12-07 15:03:22)

chore: refactor scripts
### Description

Extract helper functions to a separate file.

### Platforms affected

- [ ] Android
- [ ] iOS
- [ ] macOS
- [ ] Windows

### Test plan

n/a


Patch:
diff --git a/package.json b/package.json
index 45206e9..fbf15e7 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
     "generate:code": "node scripts/generate-manifest.mjs",
     "generate:schema": "node scripts/generate-schema.mjs",
     "lint:commit": "git log --format='%s' origin/trunk..HEAD | tail -1 | commitlint",
-    "lint:js": "eslint $(git ls-files '*.js') && tsc",
+    "lint:js": "eslint $(git ls-files '*.js' '*.mjs') && tsc",
     "lint:kt": "ktlint --android --relative 'android/app/src/**/*.kt'",
     "lint:rb": "bundle exec rubocop",
     "lint:swift": "swiftlint",
diff --git a/react-native.config.js b/react-native.config.js
index 833fe69..b7471bc 100644
--- a/react-native.config.js
+++ b/react-native.config.js
@@ -1,6 +1,12 @@
 // @ts-check
 "use strict";
 
+const {
+  findNearest,
+  getPackageVersion,
+  readJSONFile,
+} = require("./scripts/helpers");
+
 /**
  * @template Args
  * @typedef {{
@@ -25,13 +31,10 @@
  * @returns {string | undefined}
  */
 function inferProjectName() {
-  const { findNearest } = require("./windows/test-app");
   const packageJson = findNearest("package.json");
   if (packageJson) {
-    const fs = require("fs");
     try {
-      const fileContent = fs.readFileSync(packageJson, { encoding: "utf8" });
-      const name = JSON.parse(fileContent)["name"];
+      const name = readJSONFile(packageJson)["name"];
       return typeof name === "string" && name ? `${name}-test-app` : undefined;
     } catch (_) {
       // Ignore
@@ -66,13 +69,12 @@ module.exports = {
       name: "init-test-app",
       description: "Initializes a new test app project",
       func: (_argv, _config, { destination, name, platform }) => {
-        const { version: targetVersion } = require("react-native/package.json");
         const { configure } = require("./scripts/configure");
         configure({
           name,
           packagePath: destination,
           testAppPath: __dirname,
-          targetVersion,
+          targetVersion: getPackageVersion("react-native"),
           platforms: sanitizePlatformChoice(platform),
           flatten: true,
           force: true,
diff --git a/scripts/configure.js b/scripts/configure.js
index 14cb321..53c1021 100755
--- a/scripts/configure.js
+++ b/scripts/configure.js
@@ -9,6 +9,7 @@ const fs = require("fs");
 const fsp = require("fs/promises");
 const path = require("path");
 const semver = require("semver");
+const { getPackageVersion, readJSONFile } = require("./helpers");
 
 /**
  * @typedef {{ source: string; }} FileCopy;
@@ -140,15 +141,6 @@ function projectRelativePath({
   );
 }
 
-/**
- * Reads and parses JSON file at specified path.
- * @param {fs.PathLike} path
- * @returns {Record<string, unknown>}
- */
-function readJSONFile(path) {
-  return JSON.parse(fs.readFileSync(path, { encoding: "utf-8" }));
-}
-
 /**
  * Returns whether installed package satisfies specified version range.
  * @param {string} pkg
@@ -156,8 +148,7 @@ function readJSONFile(path) {
  * @returns {boolean}
  */
 function packageSatisfiesVersionRange(pkg, versionRange) {
-  const { version } = require(`${pkg}/package.json`);
-  return semver.satisfies(version, versionRange);
+  return semver.satisfies(getPackageVersion(pkg), versionRange);
 }
 
 /**
@@ -905,10 +896,32 @@ function configure(params) {
   return 0;
 }
 
+exports.androidManifestPath = androidManifestPath;
+exports.configure = configure;
+exports.error = error;
+exports.gatherConfig = gatherConfig;
+exports.getAppName = getAppName;
+exports.getConfig = getConfig;
+exports.getPlatformPackage = getPlatformPackage;
+exports.iosProjectPath = iosProjectPath;
+exports.isDestructive = isDestructive;
+exports.isInstalled = isInstalled;
+exports.join = join;
+exports.mergeConfig = mergeConfig;
+exports.packageSatisfiesVersionRange = packageSatisfiesVersionRange;
+exports.projectRelativePath = projectRelativePath;
+exports.reactNativeConfig = reactNativeConfig;
+exports.readJSONFile = readJSONFile;
+exports.removeAllFiles = removeAllFiles;
+exports.sortByKeys = sortByKeys;
+exports.updatePackageManifest = updatePackageManifest;
+exports.warn = warn;
+exports.windowsProjectPath = windowsProjectPath;
+exports.writeAllFiles = writeAllFiles;
+
 if (require.main === module) {
   /** @type {Platform[]} */
   const platformChoices = ["android", "ios", "macos", "windows"];
-  const { version: targetVersion } = require("react-native/package.json");
 
   require("yargs").usage(
     "$0 [options]",
@@ -952,42 +965,16 @@ if (require.main === module) {
       package: packagePath,
       platforms,
     }) => {
-      const result = configure({
+      process.exitCode = configure({
         name: typeof name === "string" && name ? name : getAppName(packagePath),
         packagePath,
         testAppPath: path.dirname(require.resolve("../package.json")),
-        targetVersion,
+        targetVersion: getPackageVersion("react-native"),
         platforms,
         flatten,
         force,
         init,
       });
-      if (result !== 0) {
-        process.exit(result);
-      }
     }
   ).argv;
 }
-
-exports.androidManifestPath = androidManifestPath;
-exports.configure = configure;
-exports.error = error;
-exports.gatherConfig = gatherConfig;
-exports.getAppName = getAppName;
-exports.getConfig = getConfig;
-exports.getPlatformPackage = getPlatformPackage;
-exports.iosProjectPath = iosProjectPath;
-exports.isDestructive = isDestructive;
-exports.isInstalled = isInstalled;
-exports.join = join;
-exports.mergeConfig = mergeConfig;
-exports.packageSatisfiesVersionRange = packageSatisfiesVersionRange;
-exports.projectRelativePath = projectRelativePath;
-exports.reactNativeConfig = reactNativeConfig;
-exports.readJSONFile = readJSONFile;
-exports.removeAllFiles = removeAllFiles;
-exports.sortByKeys = sortByKeys;
-exports.updatePackageManifest = updatePackageManifest;
-exports.warn = warn;
-exports.windowsProjectPath = windowsProjectPath;
-exports.writeAllFiles = writeAllFiles;
diff --git a/scripts/generate-manifest.mjs b/scripts/generate-manifest.mjs
index d07b779..51fdb32 100755
--- a/scripts/generate-manifest.mjs
+++ b/scripts/generate-manifest.mjs
@@ -1,6 +1,5 @@
 #!/usr/bin/env node
 // @ts-check
-"use strict";
 
 import * as fs from "node:fs/promises";
 import * as path from "node:path";
diff --git a/scripts/helpers.js b/scripts/helpers.js
new file mode 100644
index 0000000..006e752
--- /dev/null
+++ b/scripts/helpers.js
@@ -0,0 +1,58 @@
+// @ts-check
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+
+/**
+ * Finds nearest relative path to a file or directory from current path.
+ * @param {string} fileOrDirName Path to the file or directory to find.
+ * @param {string=} currentDir The current working directory. Mostly used for unit tests.
+ * @returns {string | null} Nearest path to given file or directory; null if not found
+ */
+function findNearest(fileOrDirName, currentDir = path.resolve("")) {
+  const rootDirectory =
+    process.platform === "win32"
+      ? currentDir.split(path.sep)[0] + path.sep
+      : "/";
+  while (currentDir !== rootDirectory) {
+    const candidatePath = path.join(currentDir, fileOrDirName);
+    if (fs.existsSync(candidatePath)) {
+      return path.relative("", candidatePath);
+    }
+
+    // Get parent folder
+    currentDir = path.dirname(currentDir);
+  }
+
+  return null;
+}
+
+/**
+ * Reads and parses JSON file at specified path.
+ * @param {fs.PathLike} path
+ * @returns {Record<string, unknown>}
+ */
+function readJSONFile(path) {
+  return JSON.parse(fs.readFileSync(path, { encoding: "utf-8" }));
+}
+
+/**
+ * Returns version string of specified module.
+ * @param {string} module
+ * @returns {string}
+ */
+function getPackageVersion(module, startDir = process.cwd()) {
+  const options = { paths: [startDir] };
+  const mod = readJSONFile(require.resolve(`${module}/package.json`, options));
+  const version = mod["version"];
+  if (typeof version !== "string") {
+    throw new Error(`Invalid version number: ${module}@${version}`);
+  }
+
+  return version;
+}
+
+exports.findNearest = findNearest;
+exports.getPackageVersion = getPackageVersion;
+exports.readJSONFile = readJSONFile;
diff --git a/scripts/prepare-viewfinder.mjs b/scripts/prepare-viewfinder.mjs
index c23a680..442b75c 100755
--- a/scripts/prepare-viewfinder.mjs
+++ b/scripts/prepare-viewfinder.mjs
@@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process";
 import * as fs from "node:fs";
 import * as path from "node:path";
 import { fileURLToPath } from "node:url";
+import { readJSONFile } from "./helpers.js";
 
 const APP_IDENTIFIER = "com.microsoft.ReactNativeViewfinder";
 const PACKAGE_MANAGER = "yarn";
@@ -21,9 +22,7 @@ const PROJECT_MANIFEST = path.resolve(APP_ROOT, "package.json");
  * Configures the app manifest for Viewfinder.
  */
 function configureAppManifest() {
-  const original = JSON.parse(
-    fs.readFileSync(APP_MANIFEST, { encoding: "utf-8" })
-  );
+  const original = readJSONFile(APP_MANIFEST);
 
   const manifest = {
     ...original,
@@ -60,5 +59,5 @@ function run(command, args, options) {
 }
 
 configureAppManifest();
-run("npx", ["@rnx-kit/dep-check", "--write", PROJECT_MANIFEST]);
+run("npx", ["@rnx-kit/align-deps", "--write", PROJECT_MANIFEST]);
 run(PACKAGE_MANAGER, ["install"]);
diff --git a/scripts/set-react-version.mjs b/scripts/set-react-version.mjs
index fccbaa4..c81be78 100755
--- a/scripts/set-react-version.mjs
+++ b/scripts/set-react-version.mjs
@@ -1,6 +1,5 @@
 #!/usr/bin/env node
 // @ts-check
-"use strict";
 
 /**
  * Reminder that this script is meant to be runnable without installing
@@ -11,6 +10,7 @@ import * as fs from "node:fs/promises";
 import * as os from "node:os";
 import * as path from "node:path";
 import { fileURLToPath } from "node:url";
+import { readJSONFile } from "./helpers.js";
 
 /**
  * @typedef {{
@@ -45,7 +45,7 @@ function isValidVersion(v) {
 
 /**
  * Type-safe `Object.keys()`
- * @template T
+ * @template {Record<string, unknown>} T
  * @param {T} obj
  * @returns {(keyof T)[]}
  */
@@ -188,6 +188,10 @@ async function getProfile(v) {
       const { dependencies, peerDependencies } = await fetchPackageInfo(
         "react-native@nightly"
       );
+      if (!dependencies) {
+        throw new Error("Could not determine dependencies");
+      }
+
       const codegen = await fetchPackageInfo(
         "react-native-codegen@" + dependencies["react-native-codegen"]
       );
@@ -247,10 +251,14 @@ if (!isValidVersion(version)) {
 
   const manifests = ["package.json", "example/package.json"];
   for (const manifestPath of manifests) {
-    const content = await fs.readFile(manifestPath, { encoding: "utf-8" });
-    const manifest = JSON.parse(content);
+    const manifest = /** @type {{ devDependencies: Record<string, string>}} */ (
+      readJSONFile(manifestPath)
+    );
     for (const packageName of keys(profile)) {
-      manifest["devDependencies"][packageName] = profile[packageName];
+      const version = profile[packageName];
+      if (version) {
+        manifest["devDependencies"][packageName] = version;
+      }
     }
 
     const tmpFile = `${manifestPath}.tmp`;
diff --git a/scripts/validate-manifest.js b/scripts/validate-manifest.js
index a9ad241..2dc76f8 100755
--- a/scripts/validate-manifest.js
+++ b/scripts/validate-manifest.js
@@ -4,6 +4,7 @@
 
 const fs = require("fs");
 const path = require("path");
+const { readJSONFile } = require("./helpers");
 
 const APP_JSON = "app.json";
 const NODE_MODULES = "node_modules";
@@ -57,8 +58,7 @@ function validateManifest(manifestPath) {
     return 1;
   }
 
-  const content = fs.readFileSync(manifestPath, { encoding: "utf-8" });
-  const manifest = /** @type {Record<string, unknown>} */ (JSON.parse(content));
+  const manifest = readJSONFile(manifestPath);
   const validate = makeValidator();
   if (!validate(manifest)) {
     console.error(


Test patch:
diff --git a/test/configure/packageSatisfiesVersionRange.test.js b/test/configure/packageSatisfiesVersionRange.test.js
index a44a145..c2c00ea 100644
--- a/test/configure/packageSatisfiesVersionRange.test.js
+++ b/test/configure/packageSatisfiesVersionRange.test.js
@@ -5,12 +5,12 @@ describe("packageSatisfiesVersionRange()", () => {
   const { packageSatisfiesVersionRange } = require("../../scripts/configure");
 
   test("returns true when a package satisfies version range", () => {
-    expect(packageSatisfiesVersionRange("..", "^0.0.1-0")).toBe(true);
-    expect(packageSatisfiesVersionRange("..", "~0.0.1-0")).toBe(true);
+    expect(packageSatisfiesVersionRange(".", "^0.0.1-0")).toBe(true);
+    expect(packageSatisfiesVersionRange(".", "~0.0.1-0")).toBe(true);
   });
 
   test("returns false when a package does not satisfy version range", () => {
-    expect(packageSatisfiesVersionRange("..", ">=0.0.1")).toBe(false);
-    expect(packageSatisfiesVersionRange("..", "^0.0.0")).toBe(false);
+    expect(packageSatisfiesVersionRange(".", ">=0.0.1")).toBe(false);
+    expect(packageSatisfiesVersionRange(".", "^0.0.0")).toBe(false);
   });
 });
diff --git a/test/windows-test-app/findNearest.test.js b/test/helpers.test.js
similarity index 58%
rename from test/windows-test-app/findNearest.test.js
rename to test/helpers.test.js
index 7066dbd..af6ee78 100644
--- a/test/windows-test-app/findNearest.test.js
+++ b/test/helpers.test.js
@@ -1,9 +1,10 @@
 // @ts-check
 "use strict";
 
+const { findNearest, getPackageVersion } = require("../scripts/helpers");
+
 describe("findNearest", () => {
   const path = require("path");
-  const { findNearest } = require("../../windows/test-app");
 
   test("returns null for non-existent files", () => {
     expect(findNearest("thisFileShouldNotExist")).toBeNull();
@@ -30,3 +31,28 @@ describe("findNearest", () => {
     ).toBe(path.join(fixturePath, "node_modules", "react-native"));
   });
 });
+
+describe("getPackageVersion", () => {
+  const path = require("path");
+
+  const nodeModulesPath = path.resolve(
+    __dirname,
+    "__fixtures__",
+    "test_app",
+    "node_modules"
+  );
+
+  test("returns version number for specified package", () => {
+    const rnPath = path.join(nodeModulesPath, "react-native");
+    expect(getPackageVersion("react-native", rnPath)).toBe("1000.0.0");
+
+    const rnCliPath = path.join(
+      nodeModulesPath,
+      "@react-native-community",
+      "cli-platform-ios"
+    );
+    expect(
+      getPackageVersion("@react-native-community/cli-platform-ios", rnCliPath)
+    ).toBe("4.10.1");
+  });
+});
diff --git a/test/windows-test-app/getPackageVersion.test.js b/test/windows-test-app/getPackageVersion.test.js
deleted file mode 100644
index 8b45ea7..0000000
--- a/test/windows-test-app/getPackageVersion.test.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// @ts-check
-"use strict";
-
-describe("getPackageVersion", () => {
-  const path = require("path");
-  const { getPackageVersion } = require("../../windows/test-app");
-
-  const nodeModulesPath = path.resolve(
-    __dirname,
-    "..",
-    "__fixtures__",
-    "test_app",
-    "node_modules"
-  );
-
-  test("returns version number for specified package", () => {
-    const rnPath = path.join(nodeModulesPath, "react-native");
-    expect(getPackageVersion(rnPath)).toBe("1000.0.0");
-
-    const rnCliPath = path.join(
-      nodeModulesPath,
-      "@react-native-community",
-      "cli-platform-ios"
-    );
-    expect(getPackageVersion(rnCliPath)).toBe("4.10.1");
-  });
-});
diff --git a/windows/test-app.js b/windows/test-app.js
index 90a44c4..906ff15 100755
--- a/windows/test-app.js
+++ b/windows/test-app.js
@@ -5,6 +5,7 @@
 const fs = require("fs");
 const os = require("os");
 const path = require("path");
+const { findNearest, getPackageVersion } = require("../scripts/helpers");
 
 /**
  * @typedef {{
@@ -58,30 +59,6 @@ function copy(src, dest) {
   });
 }
 
-/**
- * Finds nearest relative path to a file or directory from current path.
- * @param {string} fileOrDirName Path to the file or directory to find.
- * @param {string=} currentDir The current working directory. Mostly used for unit tests.
- * @returns {string | null} Nearest path to given file or directory; null if not found
- */
-function findNearest(fileOrDirName, currentDir = path.resolve("")) {
-  const rootDirectory =
-    process.platform === "win32"
-      ? currentDir.split(path.sep)[0] + path.sep
-      : "/";
-  while (currentDir !== rootDirectory) {
-    const candidatePath = path.join(currentDir, fileOrDirName);
-    if (fs.existsSync(candidatePath)) {
-      return path.relative("", candidatePath);
-    }
-
-    // Get parent folder
-    currentDir = path.dirname(currentDir);
-  }
-
-  return null;
-}
-
 /**
  * Finds all Visual Studio projects in specified directory.
  * @param {string} projectDir
@@ -462,18 +439,6 @@ function getHermesVersion(rnwPath) {
   return m && m[1];
 }
 
-/**
- * Returns the version number the package at specified path.
- * @param {string} packagePath
- * @returns {string}
- */
-function getPackageVersion(packagePath) {
-  const { version } = JSON.parse(
-    fs.readFileSync(path.join(packagePath, "package.json"), textFileReadOptions)
-  );
-  return version;
-}
-
 /**
  * Returns a single number for the specified version, suitable as a value for a
  * preprocessor definition.
@@ -548,7 +513,10 @@ function generateSolution(destPath, { autolink, useHermes, useNuGet }) {
     singleApp,
   } = getBundleResources(manifestFilePath);
 
-  const rnWindowsVersion = getPackageVersion(rnWindowsPath);
+  const rnWindowsVersion = getPackageVersion(
+    "react-native-windows",
+    rnWindowsPath
+  );
   const rnWindowsVersionNumber = getVersionNumber(rnWindowsVersion);
   const hermesVersion = useHermes && getHermesVersion(rnWindowsPath);
   const usePackageReferences =
@@ -774,7 +742,7 @@ function generateSolution(destPath, { autolink, useHermes, useNuGet }) {
         { stdio: "inherit" }
       ).on("close", (code) => {
         if (code !== 0) {
-          process.exit(code || 1);
+          process.exitCode = code || 1;
         }
       });
     });
@@ -785,12 +753,10 @@ function generateSolution(destPath, { autolink, useHermes, useNuGet }) {
 
 exports.copy = copy;
 exports.copyAndReplace = copyAndReplace;
-exports.findNearest = findNearest;
 exports.findUserProjects = findUserProjects;
 exports.generateSolution = generateSolution;
 exports.getBundleResources = getBundleResources;
 exports.getHermesVersion = getHermesVersion;
-exports.getPackageVersion = getPackageVersion;
 exports.getVersionNumber = getVersionNumber;
 exports.nuGetPackage = nuGetPackage;
 exports.parseResources = parseResources;