Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions .changeset/tall-dolls-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rnx-kit/tools-react-native": patch
"@rnx-kit/cli": patch
---

Add function to merge transformer configs to tools-react-native and use it in the cli
6 changes: 5 additions & 1 deletion packages/cli/src/helpers/metro-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
esbuildTransformerConfig,
MetroSerializer as MetroSerializerEsbuild,
} from "@rnx-kit/metro-serializer-esbuild";
import { mergeTransformerConfigs } from "@rnx-kit/tools-react-native/metro-utils";
import type { BundleParameters } from "@rnx-kit/types-bundle-config";
import type { ConfigT, SerializerConfigT } from "metro-config";
import type { WritableDeep } from "type-fest";
Expand Down Expand Up @@ -170,7 +171,10 @@ export function customizeMetroConfig(
? extraParams.treeShake
: undefined
);
Object.assign(metroConfig.transformer, esbuildTransformerConfig);
metroConfig.transformer = mergeTransformerConfigs(
metroConfig.transformer,
esbuildTransformerConfig
) as WritableDeep<ConfigT>["transformer"];
} else if (metroPlugins.length > 0) {
// MetroSerializer acts as a CustomSerializer, and it works with both
// older and newer versions of Metro. Older versions expect a return
Expand Down
33 changes: 17 additions & 16 deletions packages/tools-react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@ import * as platformTools from "@rnx-kit/tools-react-native/platform";
<!-- The following table can be updated by running `yarn update-readme` -->
<!-- @rnx-kit/api start -->

| Category | Function | Description |
| -------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| context | `loadContext(projectRoot)` | Equivalent to calling `loadConfig()` from `@react-native-community/cli`, but the result is cached for faster subsequent accesses. |
| 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. |
| context | `resolveCommunityCLI(root, reactNativePath)` | Finds path to `@react-native-community/cli`. |
| metro | `findMetroPath(projectRoot)` | Finds the installation path of Metro. |
| metro | `getMetroVersion(projectRoot)` | Returns Metro version number. |
| metro | `requireModuleFromMetro(moduleName, fromDir)` | Imports specified module starting from the installation directory of the currently used `metro` version. |
| platform | `expandPlatformExtensions(platform, extensions)` | Returns a list of extensions that should be tried for the target platform in prioritized order. |
| platform | `getAvailablePlatforms(startDir)` | Returns a map of available React Native platforms. The result is cached. |
| platform | `getAvailablePlatformsUncached(startDir, platformMap)` | Returns a map of available React Native platforms. The result is NOT cached. |
| 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 |
| platform | `parsePlatform(val)` | Parse a string to ensure it maps to a valid react-native platform. |
| platform | `platformExtensions(platform)` | Returns file extensions that can be mapped to the target platform. |
| platform | `platformValues()` | |
| platform | `tryParsePlatform(val)` | |
| Category | Function | Description |
| ----------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| context | `loadContext(projectRoot)` | Equivalent to calling `loadConfig()` from `@react-native-community/cli`, but the result is cached for faster subsequent accesses. |
| 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. |
| context | `resolveCommunityCLI(root, reactNativePath)` | Finds path to `@react-native-community/cli`. |
| metro | `findMetroPath(projectRoot)` | Finds the installation path of Metro. |
| metro | `getMetroVersion(projectRoot)` | Returns Metro version number. |
| metro | `requireModuleFromMetro(moduleName, fromDir)` | Imports specified module starting from the installation directory of the currently used `metro` version. |
| 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. |
| platform | `expandPlatformExtensions(platform, extensions)` | Returns a list of extensions that should be tried for the target platform in prioritized order. |
| platform | `getAvailablePlatforms(startDir)` | Returns a map of available React Native platforms. The result is cached. |
| platform | `getAvailablePlatformsUncached(startDir, platformMap)` | Returns a map of available React Native platforms. The result is NOT cached. |
| 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 |
| platform | `parsePlatform(val)` | Parse a string to ensure it maps to a valid react-native platform. |
| platform | `platformExtensions(platform)` | Returns file extensions that can be mapped to the target platform. |
| platform | `platformValues()` | |
| platform | `tryParsePlatform(val)` | |

<!-- @rnx-kit/api end -->
1 change: 1 addition & 0 deletions packages/tools-react-native/metro-utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { mergeTransformerConfigs } from "./lib/metro-utils.js";
1 change: 1 addition & 0 deletions packages/tools-react-native/metro-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("./lib/metro-utils.js");
5 changes: 5 additions & 0 deletions packages/tools-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
"typescript": "./src/metro.ts",
"default": "./lib/metro.js"
},
"./metro-utils": {
"types": "./lib/metro-utils.d.ts",
"typescript": "./src/metro-utils.ts",
"default": "./lib/metro-utils.js"
},
"./platform": {
"types": "./lib/platform.d.ts",
"typescript": "./src/platform.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/tools-react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
getMetroVersion,
requireModuleFromMetro,
} from "./metro.ts";
export { mergeTransformerConfigs } from "./metro-utils.ts";
export {
expandPlatformExtensions,
getAvailablePlatforms,
Expand Down
82 changes: 82 additions & 0 deletions packages/tools-react-native/src/metro-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { GetTransformOptions, TransformerConfigT } from "metro-config";

/**
* 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
* recursively merge plain objects in the `simpleObjectMerge` function.
*/
function isRecord(value: unknown): value is Record<string, unknown> {
Comment thread
JasonVMo marked this conversation as resolved.
return value !== null && typeof value === "object" && !Array.isArray(value);
}

/**
* Simple merge helper that recursively merges plain objects. Note that array merges are not supported,
* as the behavior isn't deterministic (e.g., should we concatenate arrays, or override them?). If a property is an array in
* multiple configs, the value from the last config will win.
*/
function simpleObjectMerge(
...options: Record<string, unknown>[]
): Record<string, unknown> {
/** @type {Record<string, unknown>} */
Comment thread
JasonVMo marked this conversation as resolved.
Outdated
const result: Record<string, unknown> = {};
for (const option of options) {
for (const [key, value] of Object.entries(option)) {
if (isRecord(value) && isRecord(result[key])) {
result[key] = simpleObjectMerge(result[key], value);
} else {
result[key] = value;
}
}
}
return result;
}

/**
* Creates a function that sequentially calls multiple `GetTransformOptions` functions and merges their results.
*/
function createGetTransformOptions(
...subFunctions: GetTransformOptions[]
): GetTransformOptions {
if (subFunctions.length === 0) {
throw new Error("At least one getTransformOptions function is required");
} else if (subFunctions.length === 1) {
return subFunctions[0];
} else {
return async (entryPoints, options, getDepsOf) => {
const results = await Promise.all(
subFunctions.map((fn) => fn(entryPoints, options, getDepsOf))
);
return simpleObjectMerge(...results);
};
}
}

/**
* 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.
*
* @param configs one or more transformer config objects to merge. Later configs take precedence over earlier ones.
* @returns transformer configuration suitable for use by Metro
*/
export function mergeTransformerConfigs(
...configs: Partial<TransformerConfigT>[]
): Partial<TransformerConfigT> {
// collect the getTransformOptions functions from all configs, and if there are multiple, we'll create a wrapper function for them
const getTransformOptionsFns = configs
.reduce<TransformerConfigT["getTransformOptions"][]>((result, config) => {
const getTransformOptions = config?.getTransformOptions;
if (typeof getTransformOptions === "function") {
result.push(getTransformOptions);
}
return result;
}, []);
Comment thread
JasonVMo marked this conversation as resolved.
Outdated

// if there are multiple getTransformOptions functions, create a wrapper function that calls in sequence and merges their results
if (getTransformOptionsFns.length > 1) {
configs.push({
getTransformOptions: createGetTransformOptions(...getTransformOptionsFns),
});
}

return Object.assign({}, ...configs);
}
Loading
Loading