Skip to content

Commit 0bf51d8

Browse files
JasonVMotido64
andauthored
feat(tools-react-native): Add mergeTransformerConfigs function to @rnx-kit/tools-react-native (#4044)
* add function for merging transformer configs to tools-react-native * add change files * Apply suggestions from code review Co-authored-by: Tommy Nguyen <4123478+tido64@users.noreply.github.com> * address PR feedback --------- Co-authored-by: Tommy Nguyen <4123478+tido64@users.noreply.github.com>
1 parent 4359a29 commit 0bf51d8

9 files changed

Lines changed: 386 additions & 17 deletions

File tree

.changeset/tall-dolls-reply.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rnx-kit/tools-react-native": patch
3+
"@rnx-kit/cli": patch
4+
---
5+
6+
Add function to merge transformer configs to tools-react-native and use it in the cli

packages/cli/src/helpers/metro-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
esbuildTransformerConfig,
99
MetroSerializer as MetroSerializerEsbuild,
1010
} from "@rnx-kit/metro-serializer-esbuild";
11+
import { mergeTransformerConfigs } from "@rnx-kit/tools-react-native/metro-utils";
1112
import type { BundleParameters } from "@rnx-kit/types-bundle-config";
1213
import type { ConfigT, SerializerConfigT } from "metro-config";
1314
import type { WritableDeep } from "type-fest";
@@ -170,7 +171,10 @@ export function customizeMetroConfig(
170171
? extraParams.treeShake
171172
: undefined
172173
);
173-
Object.assign(metroConfig.transformer, esbuildTransformerConfig);
174+
metroConfig.transformer = mergeTransformerConfigs(
175+
metroConfig.transformer,
176+
esbuildTransformerConfig
177+
) as WritableDeep<ConfigT>["transformer"];
174178
} else if (metroPlugins.length > 0) {
175179
// MetroSerializer acts as a CustomSerializer, and it works with both
176180
// older and newer versions of Metro. Older versions expect a return

packages/tools-react-native/README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,22 @@ import * as platformTools from "@rnx-kit/tools-react-native/platform";
2020
<!-- The following table can be updated by running `yarn update-readme` -->
2121
<!-- @rnx-kit/api start -->
2222

23-
| Category | Function | Description |
24-
| -------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
25-
| context | `loadContext(projectRoot)` | Equivalent to calling `loadConfig()` from `@react-native-community/cli`, but the result is cached for faster subsequent accesses. |
26-
| context | `loadContextAsync(projectRoot)` | Equivalent to calling `loadConfigAsync()` (with fallback to `loadConfig()`) from `@react-native-community/cli`, but the result is cached for faster subsequent accesses. |
27-
| context | `resolveCommunityCLI(root, reactNativePath)` | Finds path to `@react-native-community/cli`. |
28-
| metro | `findMetroPath(projectRoot)` | Finds the installation path of Metro. |
29-
| metro | `getMetroVersion(projectRoot)` | Returns Metro version number. |
30-
| metro | `requireModuleFromMetro(moduleName, fromDir)` | Imports specified module starting from the installation directory of the currently used `metro` version. |
31-
| platform | `expandPlatformExtensions(platform, extensions)` | Returns a list of extensions that should be tried for the target platform in prioritized order. |
32-
| platform | `getAvailablePlatforms(startDir)` | Returns a map of available React Native platforms. The result is cached. |
33-
| platform | `getAvailablePlatformsUncached(startDir, platformMap)` | Returns a map of available React Native platforms. The result is NOT cached. |
34-
| platform | `getModuleSuffixes(platform, appendEmpty)` | Get the module suffixes array for a given platform, suitable for use with TypeScript's moduleSuffixes setting in the form of ['.ios', '.native', ''] or ['.windows', '.win', '.native', ''] or similar |
35-
| platform | `parsePlatform(val)` | Parse a string to ensure it maps to a valid react-native platform. |
36-
| platform | `platformExtensions(platform)` | Returns file extensions that can be mapped to the target platform. |
37-
| platform | `platformValues()` | |
38-
| platform | `tryParsePlatform(val)` | |
23+
| Category | Function | Description |
24+
| ----------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25+
| context | `loadContext(projectRoot)` | Equivalent to calling `loadConfig()` from `@react-native-community/cli`, but the result is cached for faster subsequent accesses. |
26+
| context | `loadContextAsync(projectRoot)` | Equivalent to calling `loadConfigAsync()` (with fallback to `loadConfig()`) from `@react-native-community/cli`, but the result is cached for faster subsequent accesses. |
27+
| context | `resolveCommunityCLI(root, reactNativePath)` | Finds path to `@react-native-community/cli`. |
28+
| metro | `findMetroPath(projectRoot)` | Finds the installation path of Metro. |
29+
| metro | `getMetroVersion(projectRoot)` | Returns Metro version number. |
30+
| metro | `requireModuleFromMetro(moduleName, fromDir)` | Imports specified module starting from the installation directory of the currently used `metro` version. |
31+
| metro-utils | `mergeTransformerConfigs(...configs)` | Merges multiple Metro transformer configurations into one. Properties from later configs override earlier ones. If multiple configs provide a `getTransformOptions` function, the returned config wraps them so that each is called in order and their results are deep-merged. |
32+
| platform | `expandPlatformExtensions(platform, extensions)` | Returns a list of extensions that should be tried for the target platform in prioritized order. |
33+
| platform | `getAvailablePlatforms(startDir)` | Returns a map of available React Native platforms. The result is cached. |
34+
| platform | `getAvailablePlatformsUncached(startDir, platformMap)` | Returns a map of available React Native platforms. The result is NOT cached. |
35+
| platform | `getModuleSuffixes(platform, appendEmpty)` | Get the module suffixes array for a given platform, suitable for use with TypeScript's moduleSuffixes setting in the form of ['.ios', '.native', ''] or ['.windows', '.win', '.native', ''] or similar |
36+
| platform | `parsePlatform(val)` | Parse a string to ensure it maps to a valid react-native platform. |
37+
| platform | `platformExtensions(platform)` | Returns file extensions that can be mapped to the target platform. |
38+
| platform | `platformValues()` | |
39+
| platform | `tryParsePlatform(val)` | |
3940

4041
<!-- @rnx-kit/api end -->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { mergeTransformerConfigs } from "./lib/metro-utils.js";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("./lib/metro-utils.js");

packages/tools-react-native/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"platform.js",
2929
"src"
3030
],
31+
"type": "commonjs",
3132
"main": "lib/index.js",
3233
"types": "lib/index.d.ts",
3334
"exports": {
@@ -56,6 +57,11 @@
5657
"typescript": "./src/metro.ts",
5758
"default": "./lib/metro.js"
5859
},
60+
"./metro-utils": {
61+
"types": "./lib/metro-utils.d.ts",
62+
"typescript": "./src/metro-utils.ts",
63+
"default": "./lib/metro-utils.js"
64+
},
5965
"./platform": {
6066
"types": "./lib/platform.d.ts",
6167
"typescript": "./src/platform.ts",

packages/tools-react-native/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
getMetroVersion,
1010
requireModuleFromMetro,
1111
} from "./metro.ts";
12+
export { mergeTransformerConfigs } from "./metro-utils.ts";
1213
export {
1314
expandPlatformExtensions,
1415
getAvailablePlatforms,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { GetTransformOptions, TransformerConfigT } from "metro-config";
2+
3+
/**
4+
* Type guard to check if a value is a plain object (i.e., a record). This is used to ensure that we only attempt to
5+
* recursively merge plain objects in the `simpleObjectMerge` function.
6+
*/
7+
function isRecord(value: unknown): value is Record<string, unknown> {
8+
return value !== null && typeof value === "object" && !Array.isArray(value);
9+
}
10+
11+
/**
12+
* Simple merge helper that recursively merges plain objects. Note that array merges are not supported,
13+
* as the behavior isn't deterministic (e.g., should we concatenate arrays, or override them?). If a property is an array in
14+
* multiple configs, the value from the last config will win.
15+
*/
16+
function simpleObjectMerge(
17+
...options: Record<string, unknown>[]
18+
): Record<string, unknown> {
19+
const result: Record<string, unknown> = {};
20+
for (const option of options) {
21+
for (const [key, value] of Object.entries(option)) {
22+
if (isRecord(value) && isRecord(result[key])) {
23+
result[key] = simpleObjectMerge(result[key], value);
24+
} else {
25+
result[key] = value;
26+
}
27+
}
28+
}
29+
return result;
30+
}
31+
32+
/**
33+
* Creates a function that sequentially calls multiple `GetTransformOptions` functions and merges their results.
34+
*/
35+
function createGetTransformOptions(
36+
...subFunctions: GetTransformOptions[]
37+
): GetTransformOptions {
38+
if (subFunctions.length === 0) {
39+
throw new Error("At least one getTransformOptions function is required");
40+
} else if (subFunctions.length === 1) {
41+
return subFunctions[0];
42+
} else {
43+
return async (entryPoints, options, getDepsOf) => {
44+
const results = await Promise.all(
45+
subFunctions.map((fn) => fn(entryPoints, options, getDepsOf))
46+
);
47+
return simpleObjectMerge(...results);
48+
};
49+
}
50+
}
51+
52+
/**
53+
* Merges multiple Metro transformer configurations into one. Properties from later configs override earlier
54+
* ones. If multiple configs provide a `getTransformOptions` function, the returned config wraps them so that each
55+
* is called in order and their results are deep-merged.
56+
*
57+
* @param configs one or more transformer config objects to merge. Later configs take precedence over earlier ones.
58+
* @returns transformer configuration suitable for use by Metro
59+
*/
60+
export function mergeTransformerConfigs(
61+
...configs: Partial<TransformerConfigT>[]
62+
): Partial<TransformerConfigT> {
63+
// collect the getTransformOptions functions from all configs, and if there are multiple, we'll create a wrapper function for them
64+
const getTransformOptionsFns = configs.reduce<
65+
TransformerConfigT["getTransformOptions"][]
66+
>((result, config) => {
67+
const getTransformOptions = config?.getTransformOptions;
68+
if (typeof getTransformOptions === "function") {
69+
result.push(getTransformOptions);
70+
}
71+
return result;
72+
}, []);
73+
74+
// if there are multiple getTransformOptions functions, create a wrapper function that calls in sequence and merges their results
75+
if (getTransformOptionsFns.length > 1) {
76+
configs.push({
77+
getTransformOptions: createGetTransformOptions(...getTransformOptionsFns),
78+
});
79+
}
80+
81+
return Object.assign({}, ...configs);
82+
}

0 commit comments

Comments
 (0)