Skip to content
Closed
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/gentle-hands-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/metro-transformer": major
---

Add @rnx-kit/metro-transformer with support for transformer merging and selecting babel transformers based on file type
5 changes: 5 additions & 0 deletions .changeset/ninety-cows-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/metro-serializer": patch
---

Use new types package for serializer types
6 changes: 6 additions & 0 deletions .changeset/thirty-jeans-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rnx-kit/cli": minor
"@rnx-kit/metro-serializer": patch
---

Consume new metro-transformer package
5 changes: 5 additions & 0 deletions .changeset/true-symbols-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/types-metro-config": major
---

Add types package for common rnx-kit metro configuration types
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
"**/lib-amd": true
},

"js/ts.experimental.useTsgo": true,
"js/ts.experimental.useTsgo": false,
"js/ts.preferences.quoteStyle": "single",

"js/ts.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": false,
"js/ts.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false,
"js/ts.tsdk.path": "./node_modules/@typescript/native-preview/lib",
"js/ts.tsdk.path": "./node_modules/typescript/lib",

"search.exclude": {
"**/node_modules": true,
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@
"@babel/preset-env"
]
},
"packages/metro-transformer": {
"ignoreDependencies": [
"@react-native/metro-babel-transformer"
]
},
"packages/oxlint-config": {
"entry": [
"src/**/*.ts"
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@rnx-kit/metro-serializer": "^2.0.0",
"@rnx-kit/metro-serializer-esbuild": "^0.3.1",
"@rnx-kit/metro-service": "^4.1.3",
"@rnx-kit/metro-transformer": "^0.0.1",
"@rnx-kit/third-party-notices": "^2.0.0",
"@rnx-kit/tools-android": "^0.2.2",
"@rnx-kit/tools-apple": "^0.2.2",
Expand All @@ -58,6 +59,7 @@
"@rnx-kit/tools-react-native": "^2.3.4",
"@rnx-kit/types-bundle-config": "^1.0.0",
"@rnx-kit/types-kit-config": "^1.0.0",
"@rnx-kit/types-metro-config": "^0.0.1",
"@rnx-kit/types-node": "^1.0.0",
"commander": "^11.1.0",
"ora": "^5.4.1",
Expand Down
42 changes: 34 additions & 8 deletions packages/cli/src/helpers/metro-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ import { warn } from "@rnx-kit/console";
import { CyclicDependencies } from "@rnx-kit/metro-plugin-cyclic-dependencies-detector";
import { DuplicateDependencies } from "@rnx-kit/metro-plugin-duplicates-checker";
import { TypeScriptPlugin } from "@rnx-kit/metro-plugin-typescript";
import type { MetroPlugin } from "@rnx-kit/metro-serializer";
import { MetroSerializer } from "@rnx-kit/metro-serializer";
import {
esbuildTransformerConfig,
MetroSerializer as MetroSerializerEsbuild,
} from "@rnx-kit/metro-serializer-esbuild";
import { MetroTransformer } from "@rnx-kit/metro-transformer";
import type { BundleParameters } from "@rnx-kit/types-bundle-config";
import type { ConfigT, SerializerConfigT } from "metro-config";
import type {
TransformerPlugin,
SerializerHookPlugin,
SerializerPlugin,
ExtendedTransformerConfig,
} from "@rnx-kit/types-metro-config";
import type {
ConfigT,
SerializerConfigT,
TransformerConfigT,
} from "metro-config";
import type { WritableDeep } from "type-fest";
import { getDefaultBundlerPlugins } from "../bundle/defaultPlugins.ts";

Expand Down Expand Up @@ -104,11 +114,11 @@ export function customizeMetroConfig(
// with readonly props to a type where the props are writeable.
const metroConfig = metroConfigReadonly as WritableDeep<ConfigT>;

const metroPlugins: MetroPlugin[] = [];
const serializerHooks: Record<
string,
SerializerConfigT["experimentalSerializerHook"]
> = {};
const metroPlugins: SerializerPlugin[] = [];
const serializerHooks: Record<string, SerializerHookPlugin> = {};
const transformers: ExtendedTransformerConfig[] = metroConfig.transformer
? [metroConfig.transformer]
: [];
Comment on lines +119 to +121
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transformer is always set because Metro requires it. This also means that we will always run the merger logic below.

I tested this locally with only tree shaking enabled and this is what it returned:

  transformOptions: {
    transform: { experimentalImportSupport: false, inlineRequires: false },
    customTransformerOptions: {
      upstreamTransformerPath: '/~/node_modules/.store/@react-native-metro-babel-transformer-virtual-38655d5e06/package/src/index.js'
    }
  }

Ideally, we don't want this plugin to be running at all if it's not set.


const legacyWarning = readLegacyOptions(extraParams);
if (legacyWarning) {
Expand Down Expand Up @@ -152,6 +162,15 @@ export function customizeMetroConfig(
serializerHooks[module] = plugin(options, print);
break;

case "transformer":
{
const transformerPlugin: TransformerPlugin = plugin(options, print);
if (transformerPlugin.transformer) {
transformers.unshift(transformerPlugin.transformer);
}
}
break;

Comment thread
JasonVMo marked this conversation as resolved.
default:
throw new Error(`${module}: unknown plugin type: ${plugin.type}`);
}
Expand All @@ -170,7 +189,8 @@ export function customizeMetroConfig(
? extraParams.treeShake
: undefined
);
Object.assign(metroConfig.transformer, esbuildTransformerConfig);
// otherwise, add it to the list to be merged by the MetroTransformer
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why otherwise?

transformers.push(esbuildTransformerConfig);
} 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 All @@ -189,6 +209,12 @@ export function customizeMetroConfig(
// We don't want this set if unused
delete metroConfig.serializer.customSerializer;
}
// Use the MetroTransformer to resolve the final transformer, if there are no valid transformers it will return an empty
// config. If there is one and it is already valid it will return it as is, if there are multiple or if they use extended
// options it will merge and transform them.
metroConfig.transformer = MetroTransformer(
...transformers
) as WritableDeep<TransformerConfigT>;

const hooks = Object.values(serializerHooks);
switch (hooks.length) {
Expand Down
3 changes: 2 additions & 1 deletion packages/metro-serializer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"test": "rnx-kit-scripts test"
},
"dependencies": {
"@rnx-kit/tools-react-native": "^2.3.0"
"@rnx-kit/tools-react-native": "^2.3.0",
"@rnx-kit/types-metro-config": "^0.0.1"
},
"devDependencies": {
"@rnx-kit/scripts": "*",
Expand Down
39 changes: 12 additions & 27 deletions packages/metro-serializer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,19 @@ import {
requireModuleFromMetro,
} from "@rnx-kit/tools-react-native/metro";
import type {
MixedOutput,
Module,
ReadOnlyGraph,
SerializerOptions,
} from "metro";

export type MetroPlugin<T = MixedOutput> = (
entryPoint: string,
preModules: readonly Module<T>[],
graph: ReadOnlyGraph<T>,
options: SerializerOptions<T>
) => void;

export type Bundle = {
modules: readonly [number, string][];
post: string;
pre: string;
Bundle,
CustomSerializer,
CustomSerializerResult,
SerializerPlugin,
} from "@rnx-kit/types-metro-config";
import type { Module, ReadOnlyGraph, SerializerOptions } from "metro";
export type {
Bundle,
CustomSerializer,
CustomSerializerResult,
SerializerPlugin as MetroPlugin,
};

export type CustomSerializerResult = string | { code: string; map: string };

export type CustomSerializer = (
entryPoint: string,
preModules: readonly Module[],
graph: ReadOnlyGraph,
options: SerializerOptions
) => Promise<CustomSerializerResult> | CustomSerializerResult;

export type TestMocks = {
baseJSBundle?: (
entryPoint: string,
Expand Down Expand Up @@ -61,7 +46,7 @@ function getMetroVersion(): number {
* @see https://github.com/facebook/metro/blob/af23a1b27bcaaff2e43cb795744b003e145e78dd/packages/metro/src/Server.js#L228
*/
export function MetroSerializer(
plugins: MetroPlugin[],
plugins: SerializerPlugin[],
__mocks: TestMocks = {}
): CustomSerializer {
const baseJSBundle =
Expand Down
78 changes: 78 additions & 0 deletions packages/metro-transformer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# @rnx-kit/metro-transformer

[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
[![npm version](https://img.shields.io/npm/v/@rnx-kit/metro-transformer)](https://www.npmjs.com/package/@rnx-kit/metro-transformer)

`@rnx-kit/metro-transformer` is a pluggable Metro transformer that lets you
route files to different Babel transformers based on glob patterns and merge transformer configuration
from multiple sources.

## Installation

```sh
yarn add @rnx-kit/metro-transformer --dev
```

## Usage

Use `MetroTransformer` to build the `transformer` section of your Metro config:

```js
// metro.config.js
const { makeMetroConfig } = require("@rnx-kit/metro-config");
const { MetroTransformer } = require("@rnx-kit/metro-transformer");
const { resolve } = require("node:path");

module.exports = makeMetroConfig({
transformer: MetroTransformer({
// any standard options can be used...
getTransformOptions: async () => ({
// options here
}),
// add in a specific file babel transformer
babelTransformers: {
// Route .svg files through react-native-svg-transformer
"**/*.svg": resolve(
require.resolve("react-native-svg-transformer/package.json"),
"../transformer.js"
),
},
}),
});
```

## Configuration

`MetroTransformer(config)` accepts any number of `ExtendedTransformerConfig` objects
and returns a `TransformerConfigT` suitable for Metro's `transformer` field.

### `babelTransformers`

`Record<string, string>` — Maps glob patterns to absolute paths of Babel
transformers. When Metro processes a file, patterns are tested in insertion
order and the first match wins. Patterns are matched with
[micromatch](https://github.com/micromatch/micromatch) against the full file
path, so use `**` to match across directories (e.g. `"**/*.svg"` rather than
`"*.svg"`).

```js
MetroTransformer({
babelTransformers: {
"**/*.svg": require.resolve("react-native-svg-transformer/transformer"),
"**/*.png": require.resolve("./myImageTransformer"),
},
});
```

### `customTransformerOptions`

`Record<string, unknown>` — Arbitrary options merged into
`customTransformOptions` and forwarded to every Babel transformer. Useful for
passing configuration through to delegate transformers.

### Standard Metro transformer options

Any other field accepted by Metro's `TransformerConfigT` (e.g.
`getTransformOptions`, `babelTransformerPath`) can be included and will be
merged into the final config. Multiple configs are merged left-to-right, with
later values overwriting earlier ones.
47 changes: 47 additions & 0 deletions packages/metro-transformer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@rnx-kit/metro-transformer",
"version": "0.0.1",
"description": "metro-transformer",
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/packages/metro-transformer#readme",
"license": "MIT",
"author": {
"name": "Microsoft Open Source",
"email": "microsoftopensource@users.noreply.github.com"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/rnx-kit",
"directory": "packages/metro-transformer"
},
"files": [
"lib/**/*.d.ts",
"lib/**/*.js"
],
"type": "commonjs",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "rnx-kit-scripts build",
"format": "rnx-kit-scripts format",
"lint": "rnx-kit-scripts lint",
"test": "rnx-kit-scripts test"
},
"dependencies": {
"@rnx-kit/types-metro-config": "^0.0.1",
"micromatch": "^4.0.8"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@react-native/metro-babel-transformer": "^0.83.0",
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@types/babel__core": "^7.20.0",
"@types/micromatch": "^4.0.10",
"metro": "^0.83.3",
"metro-babel-transformer": "^0.83.3",
"metro-config": "^0.83.3"
},
"engines": {
"node": ">=18.12"
}
}
Loading
Loading