Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(jest-preset): support ESM config files #3433

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-eyes-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/tools-react-native": patch
---

Decouple `getAvailablePlatforms()` from `@react-native-community/cli`
5 changes: 5 additions & 0 deletions .changeset/strong-cups-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/jest-preset": patch
---

Support ESM config files
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,6 @@
"tsx"
]
},
"packages/eslint-plugin": {
"ignoreDependencies": [
"eslint-plugin-react-hooks"
]
},
"packages/jest-preset": {
"ignoreDependencies": [
"react",
Expand Down
3 changes: 1 addition & 2 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
},
"devDependencies": {
"@rnx-kit/eslint-config": "*",
"@rnx-kit/jest-preset": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@types/eslint": "^9.0.0",
Expand All @@ -73,6 +72,6 @@
"node": ">=16.17"
},
"jest": {
"preset": "@rnx-kit/jest-preset/private"
"preset": "../jest-preset/private/jest-preset.js"
}
}
1 change: 1 addition & 0 deletions packages/jest-preset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-typescript": "^7.0.0",
"@rnx-kit/tools-react-native": "^2.0.2",
"find-up": "^5.0.0"
},
"peerDependencies": {
Expand Down
77 changes: 29 additions & 48 deletions packages/jest-preset/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const findUp = require("find-up");
const path = require("path");
const path = require("node:path");

/**
* @typedef {import("@jest/types").Config.HasteConfig} HasteConfig
Expand All @@ -9,24 +9,6 @@ const path = require("path");
* @typedef {[string | undefined, string | undefined]} PlatformPath
*/

/**
* Resolve the path to a dependency given a chain of dependencies leading up to
* it.
*
* Note: This is a copy of the function in `@rnx-kit/tools-node` to avoid
* circular dependency.
*
* @param {string[]} chain Chain of dependencies leading up to the target dependency.
* @param {string=} startDir Optional starting directory for the search. If not given, the current directory is used.
* @returns Path to the final dependency's directory.
*/
function resolveDependencyChain(chain, startDir = process.cwd()) {
return chain.reduce((startDir, module) => {
const p = require.resolve(`${module}/package.json`, { paths: [startDir] });
return path.dirname(p);
}, startDir);
}

/**
* Returns the current package directory.
* @returns {string | undefined}
Expand All @@ -47,15 +29,26 @@ function getReactNativePlatformPath(rootDir = getPackageDirectory()) {
throw new Error("Failed to resolve current package root");
}

const fs = require("fs");
// This should only throw because we haven't built `tools-react-native` yet,
// which can happen if we're running repo-wide tools (like Knip).
const readReactNativeConfig = (() => {
try {
const {
readReactNativeConfig,
} = require("@rnx-kit/tools-react-native/context");
return readReactNativeConfig;
} catch (_) {
return () => undefined;
}
})();

const rnConfigPath = path.join(rootDir, "react-native.config.js");
if (!fs.existsSync(rnConfigPath)) {
const config = readReactNativeConfig(rootDir);
if (!config) {
return [undefined, undefined];
}

const { platforms, reactNativePath } = require(rnConfigPath);
if (reactNativePath) {
const { platforms, reactNativePath } = config;
if (reactNativePath && typeof reactNativePath === "string") {
const resolvedPath = /^\.?\.[/\\]/.test(reactNativePath)
? path.resolve(rootDir, reactNativePath)
: path.dirname(
Expand All @@ -68,15 +61,16 @@ function getReactNativePlatformPath(rootDir = getPackageDirectory()) {
}
}

if (platforms) {
const names = Object.keys(platforms).filter(
(name) => typeof platforms[name].npmPackageName === "string"
if (platforms && typeof platforms === "object") {
const names = Object.entries(platforms).filter(
([, info]) => typeof info.npmPackageName === "string"
);
if (names.length > 1) {
console.warn(`Multiple platforms found; picking the first one: ${names}`);
const found = names.map(([key]) => key).join(", ");
console.warn(`Multiple platforms found; picking the first one: ${found}`);
}

return [names[0], rootDir];
return [names[0][0], rootDir];
}

console.warn("No platforms found");
Expand All @@ -96,33 +90,20 @@ function getTargetPlatform(defaultPlatform, searchPaths) {
return getReactNativePlatformPath();
}

/** @type {(config?: {projectRoot?: string; selectedPlatform?: string; }) => CLIConfig} */
const loadConfig = (() => {
const rnCliPath = resolveDependencyChain([
"react-native",
"@react-native-community/cli",
]);
return (
require(rnCliPath).loadConfig ||
require(`${rnCliPath}/build/tools/config`).default
);
})();

// .length on a function returns the number of formal parameters.
// fixes https://github.com/react-native-community/cli/pull/2379 changing the number of parameters.
const platforms =
loadConfig.length == 1 ? loadConfig({}).platforms : loadConfig().platforms;
const {
getAvailablePlatforms,
} = require("@rnx-kit/tools-react-native/platform");

const targetPlatformConfig = platforms[defaultPlatform];
if (!targetPlatformConfig) {
const platforms = getAvailablePlatforms(searchPaths.paths[0]);
const npmPackageName = platforms[defaultPlatform];
if (typeof npmPackageName !== "string") {
const availablePlatforms = Object.keys(platforms).join(", ");
throw new Error(
`'${defaultPlatform}' was not found among available platforms: ${availablePlatforms}`
);
}

// `npmPackageName` is unset if target platform is in core.
const { npmPackageName } = targetPlatformConfig;
return [
defaultPlatform,
npmPackageName
Expand Down
1 change: 1 addition & 0 deletions packages/tools-react-native/context.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {
loadContext,
loadContextAsync,
readReactNativeConfig,
resolveCommunityCLI,
} from "./lib/context";
9 changes: 5 additions & 4 deletions packages/tools-react-native/src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Config } from "@react-native-community/cli-types";
import { findUp } from "@rnx-kit/tools-node/path";
import * as crypto from "crypto";
import * as nodefs from "fs";
import * as path from "path";
import * as crypto from "node:crypto";
import * as nodefs from "node:fs";
import * as path from "node:path";
import { REACT_NATIVE_CONFIG_FILES } from "./context";

const HASH_ALGO = "sha256";
const UTF8 = { encoding: "utf-8" as const };
Expand Down Expand Up @@ -45,7 +46,7 @@ function updateHash(
export function getCurrentState(projectRoot: string): string {
const sha2 = crypto.createHash(HASH_ALGO);

const configFiles = ["package.json", "react-native.config.js"];
const configFiles = ["package.json", ...REACT_NATIVE_CONFIG_FILES];
updateHash(sha2, configFiles, projectRoot, "all");

const lockfiles = [
Expand Down
42 changes: 42 additions & 0 deletions packages/tools-react-native/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
findPackageDependencyDir,
readPackage,
} from "@rnx-kit/tools-node/package";
import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import {
getCurrentState,
getSavedState,
Expand All @@ -15,6 +18,13 @@ import {
// `react-native`. Consumers have to take a direct dependency on CLI instead.
const RN_CLI_DECOUPLED = 76;

export const REACT_NATIVE_CONFIG_FILES = [
"react-native.config.ts",
"react-native.config.mjs",
"react-native.config.cjs",
"react-native.config.js",
];

function toNumber(version: string): number {
const [major, minor = 0] = version.split(".");
return Number(major) * 1000 + Number(minor);
Expand Down Expand Up @@ -115,3 +125,35 @@ export async function loadContextAsync(
saveConfigToCache(projectRoot, state, config);
return config;
}

export function readReactNativeConfig(
packageDir: string,
cwd = process.cwd()
): Record<string, unknown> | undefined {
for (const configFile of REACT_NATIVE_CONFIG_FILES) {
const configPath = path.join(packageDir, configFile);
if (fs.existsSync(configPath)) {
const url =
process.platform === "win32"
? `file://${configPath.replaceAll("\\", "/")}`
: configPath;
const args = [
"--no-warnings",
"--eval",
`import("${url}").then((config) => console.log(JSON.stringify(config.default ?? config)));`,
];

const { stderr, stdout } = spawnSync(process.argv0, args, { cwd });

const errors = stderr?.toString().trim();
if (errors) {
console.error(`${configPath}: ${errors}`);
}

const json = stdout?.toString().trim();
return json ? JSON.parse(json) : undefined;
}
}

return undefined;
}
7 changes: 6 additions & 1 deletion packages/tools-react-native/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { loadContext, loadContextAsync, resolveCommunityCLI } from "./context";
export {
loadContext,
loadContextAsync,
readReactNativeConfig,
resolveCommunityCLI,
} from "./context";
export {
findMetroPath,
getMetroVersion,
Expand Down
Loading
Loading