Skip to content

Commit

Permalink
Generate function map via Babel plugin, remove transformer->source ma…
Browse files Browse the repository at this point in the history
…p dependency

Summary:
## Motivation
As part of work to decouple `metro-react-native-babel-transformer` from Metro, and allow more integration with React Native (in particular, for Static ViewConfigs), we'd like to obviate the current dependency of the transformer on `metro-source-map`.

This will isolate the transformer and `metro-react-native-babel-preset` so that they're only loosely coupled to Metro via stable interfaces, and frees them up to be moved together into RN.

## Function maps
Metro uses "function maps" to augment source maps and provide richer symbolication. They are derived from the source AST (ideally before any mutation) -  consequently, they're currently generated between parsing and transformation by *each* transformer via an imperative API, and returned explicitly alongside the transform result.

## This change
Invert the dependency on `metro-source-map` by extracting function map metadata in a plugin pass, with a new Babel plugin, rather than with a call to `generateFunctionMap`. We take advantage of the existing API to pass a `plugins` array to the transformer implementation.

### Changes to contracts
 - Transformer implementations (`transformer.babelTransformerPath`) *may* return a `metadata` object from Babel's transform result metadata. (As an implementation detail, this may contain a `functionMap` property added by the plugin, but the transformer doesn't need to know about this or handle it explicitly.)
 - Returning a top-level `functionMap` from `transform()`, which was always optional, is now deprecated. It will be temporarily used as a fallback so that this change is non-breaking.

```
* **[Feature]** `metro-babel-transformer` and `metro-react-native-babel-transformer` will return `metadata` from Babel transform results.
```

Reviewed By: motiz88

Differential Revision: D46149279

fbshipit-source-id: e8629be9a90d6b1bec6c2d60d97859d3febaa897
  • Loading branch information
robhogan authored and facebook-github-bot committed Jun 8, 2023
1 parent a064fbf commit 42fdbc2
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 86 deletions.
61 changes: 30 additions & 31 deletions flow-typed/babel.js.flow
Expand Up @@ -298,15 +298,14 @@ declare module '@babel/core' {
};

declare export type BabelFileMetadata = {
usedHelpers: Array<string>,

marked: Array<{
usedHelpers?: Array<string>,
marked?: Array<{
type: string,
message: string,
loc: BabelNodeSourceLocation,
}>,

modules: BabelFileModulesMetadata,
modules?: BabelFileModulesMetadata,
...
};

declare class Store {
Expand Down Expand Up @@ -372,9 +371,9 @@ declare module '@babel/core' {

addAst(ast: BabelNode): void;

transform(): TransformResult;
transform(): TransformResult<>;

wrap(code: string, callback: () => mixed): TransformResult;
wrap(code: string, callback: () => mixed): TransformResult<>;

addCode(code: string): void;

Expand All @@ -384,9 +383,9 @@ declare module '@babel/core' {

parseShebang(): void;

makeResult(TransformResult): TransformResult;
makeResult<T>(TransformResult<T>): TransformResult<T>;

generate(): TransformResult;
generate(): TransformResult<>;
}

declare export type MatchPattern =
Expand Down Expand Up @@ -919,96 +918,96 @@ declare module '@babel/core' {
moduleRoot?: string,
|};

declare export type TransformResult = {|
metadata: BabelFileMetadata,
declare export type TransformResult<T = BabelFileMetadata> = {|
metadata: T,
options: BabelCoreOptions,
code: string,
map: _BabelSourceMap | null,
ast: BabelNodeFile | null,
ignored?: boolean,
|};

declare type TransformCallback =
declare type TransformCallback<TMetadata> =
| ((Error, null) => mixed)
| ((null, TransformResult | null) => mixed);
| ((null, TransformResult<TMetadata> | null) => mixed);

/**
* Transforms the passed in code. Calling a callback with an object with the generated code, source map, and AST.
*/
declare export function transform(
declare export function transform<TMetadata = BabelFileMetadata>(
code: string,
options: ?BabelCoreOptions,
callback: TransformCallback,
callback: TransformCallback<TMetadata>,
): void;

/***
* Transforms the passed in code. Returning an object with the generated code, source map, and AST.
*/
declare export function transformSync(
declare export function transformSync<TMetadata = BabelFileMetadata>(
code: string,
options?: BabelCoreOptions,
): TransformResult;
): TransformResult<TMetadata>;

/**
* Transforms the passed in code. Returning an promise for an object with the generated code, source map, and AST.
*/
declare export function transformAsync(
declare export function transformAsync<TMetadata = BabelFileMetadata>(
code: string,
options?: BabelCoreOptions,
): Promise<TransformResult>;
): Promise<TransformResult<TMetadata>>;

/**
* Asynchronously transforms the entire contents of a file.
*/
declare export function transformFile(
declare export function transformFile<TMetadata = BabelFileMetadata>(
filename: string,
options?: BabelCoreOptions,
callback: TransformCallback,
callback: TransformCallback<TMetadata>,
): void;

/**
* Synchronous version of babel.transformFile. Returns the transformed contents of the filename.
*/
declare export function transformFileSync(
declare export function transformFileSync<TMetadata = BabelFileMetadata>(
filename: string,
options?: BabelCoreOptions,
): TransformResult;
): TransformResult<TMetadata>;

/**
* Promise version of babel.transformFile. Returns a promise for the transformed contents of the filename.
*/
declare export function transformFileAsync(
declare export function transformFileAsync<TMetadata = BabelFileMetadata>(
filename: string,
options?: BabelCoreOptions,
): Promise<TransformResult>;
): Promise<TransformResult<TMetadata>>;

/**
* Given an AST, transform it.
*/
declare export function transformFromAst(
declare export function transformFromAst<TMetadata = BabelFileMetadata>(
ast: BabelNodeFile | BabelNodeProgram,
code?: string,
options?: BabelCoreOptions,
callback: TransformCallback,
callback: TransformCallback<TMetadata>,
): void;

/**
* Given an AST, transform it.
*/
declare export function transformFromAstSync(
declare export function transformFromAstSync<TMetadata = BabelFileMetadata>(
ast: BabelNodeFile | BabelNodeProgram,
code?: string,
options?: BabelCoreOptions,
): TransformResult;
): TransformResult<TMetadata>;

/**
* Given an AST, transform it.
*/
declare export function transformFromAstAsync(
declare export function transformFromAstAsync<TMetadata = BabelFileMetadata>(
ast: BabelNodeFile | BabelNodeProgram,
code?: string,
options?: BabelCoreOptions,
): Promise<TransformResult>;
): Promise<TransformResult<TMetadata>>;

/**
* Given some code, parse it using Babel's standard behavior. Referenced presets and plugins will be loaded such that optional syntax plugins are automatically enabled.
Expand Down
1 change: 0 additions & 1 deletion packages/metro-babel-transformer/package.json
Expand Up @@ -19,7 +19,6 @@
"dependencies": {
"@babel/core": "^7.20.0",
"hermes-parser": "0.12.0",
"metro-source-map": "0.76.6",
"nullthrows": "^1.1.1"
},
"engines": {
Expand Down
38 changes: 29 additions & 9 deletions packages/metro-babel-transformer/src/index.js
Expand Up @@ -11,11 +11,9 @@

'use strict';

import type {BabelCoreOptions} from '@babel/core';
import type {FBSourceFunctionMap} from 'metro-source-map';
import type {BabelCoreOptions, BabelFileMetadata} from '@babel/core';

const {parseSync, transformFromAstSync} = require('@babel/core');
const {generateFunctionMap} = require('metro-source-map');
const nullthrows = require('nullthrows');

export type CustomTransformOptions = {
Expand Down Expand Up @@ -54,10 +52,28 @@ export type BabelTransformerArgs = $ReadOnly<{
src: string,
}>;

export type BabelFileFunctionMapMetadata = $ReadOnly<{
names: $ReadOnlyArray<string>,
mappings: string,
}>;

export type MetroBabelFileMetadata = {
...BabelFileMetadata,
metro?: ?{
functionMap?: ?BabelFileFunctionMapMetadata,
...
},
...
};

export type BabelTransformer = {
transform: BabelTransformerArgs => {
ast: BabelNodeFile,
functionMap: ?FBSourceFunctionMap,
// Deprecated, will be removed in a future breaking release. Function maps
// will be generated by an input Babel plugin instead and written into
// `metadata` - transformers don't need to return them explicitly.
functionMap?: BabelFileFunctionMapMetadata,
metadata?: MetroBabelFileMetadata,
...
},
getCacheKey?: () => string,
Expand Down Expand Up @@ -94,12 +110,16 @@ function transform({filename, options, plugins, src}: BabelTransformerArgs) {
})
: parseSync(src, babelConfig);

// Generate the function map before we transform the AST to
// ensure we aren't reading from mutated AST.
const functionMap = generateFunctionMap(sourceAst, {filename});
const {ast} = transformFromAstSync(sourceAst, src, babelConfig);
const transformResult = transformFromAstSync<MetroBabelFileMetadata>(
sourceAst,
src,
babelConfig,
);

return {ast: nullthrows(ast), functionMap};
return {
ast: nullthrows(transformResult.ast),
metadata: transformResult.metadata,
};
} finally {
if (OLD_BABEL_ENV) {
process.env.BABEL_ENV = OLD_BABEL_ENV;
Expand Down
4 changes: 1 addition & 3 deletions packages/metro-babel-transformer/types/index.d.ts
Expand Up @@ -8,8 +8,6 @@
* @oncall react_native
*/

import type {FBSourceFunctionMap} from 'metro-source-map';

export interface CustomTransformOptions {
[key: string]: unknown;
}
Expand Down Expand Up @@ -46,7 +44,7 @@ export interface BabelTransformerArgs {
export interface BabelTransformer {
transform: (args: BabelTransformerArgs) => {
ast: unknown;
functionMap: FBSourceFunctionMap | null;
metadata: unknown;
};
getCacheKey?: () => string;
}
Expand Down
1 change: 0 additions & 1 deletion packages/metro-react-native-babel-transformer/package.json
Expand Up @@ -22,7 +22,6 @@
"babel-preset-fbjs": "^3.4.0",
"hermes-parser": "0.12.0",
"metro-react-native-babel-preset": "0.76.6",
"metro-source-map": "0.76.6",
"nullthrows": "^1.1.1"
},
"peerDependencies": {
Expand Down
28 changes: 15 additions & 13 deletions packages/metro-react-native-babel-transformer/src/index.js
Expand Up @@ -16,16 +16,14 @@
import type {BabelCoreOptions, Plugins} from '@babel/core';
import type {
BabelTransformer,
BabelTransformerArgs,
MetroBabelFileMetadata,
} from 'metro-babel-transformer';
import type {FBSourceFunctionMap} from 'metro-source-map/src/source-map';

const {parseSync, transformFromAstSync} = require('@babel/core');
const inlineRequiresPlugin = require('babel-preset-fbjs/plugins/inline-requires');
const crypto = require('crypto');
const fs = require('fs');
const makeHMRConfig = require('metro-react-native-babel-preset/src/configs/hmr');
const {generateFunctionMap} = require('metro-source-map');
const nullthrows = require('nullthrows');
const path = require('path');

Expand Down Expand Up @@ -186,11 +184,12 @@ function buildBabelConfig(
};
}

function transform({filename, options, src, plugins}: BabelTransformerArgs): {
ast: BabelNodeFile,
functionMap: ?FBSourceFunctionMap,
...
} {
const transform: BabelTransformer['transform'] = ({
filename,
options,
src,
plugins,
}) => {
const OLD_BABEL_ENV = process.env.BABEL_ENV;
process.env.BABEL_ENV = options.dev
? 'development'
Expand Down Expand Up @@ -220,23 +219,26 @@ function transform({filename, options, src, plugins}: BabelTransformerArgs): {
sourceType: babelConfig.sourceType,
});

const functionMap = generateFunctionMap(sourceAst, {filename});
const result = transformFromAstSync(sourceAst, src, babelConfig);
const result = transformFromAstSync<MetroBabelFileMetadata>(
sourceAst,
src,
babelConfig,
);

// The result from `transformFromAstSync` can be null (if the file is ignored)
if (!result) {
/* $FlowFixMe BabelTransformer specifies that the `ast` can never be null but
* the function returns here. Discovered when typing `BabelNode`. */
return {ast: null, functionMap};
return {ast: null};
}

return {ast: nullthrows(result.ast), functionMap};
return {ast: nullthrows(result.ast), metadata: result.metadata};
} finally {
if (OLD_BABEL_ENV) {
process.env.BABEL_ENV = OLD_BABEL_ENV;
}
}
}
};

function getCacheKey() {
var key = crypto.createHash('md5');
Expand Down
Expand Up @@ -11,9 +11,11 @@

'use strict';

import type {MetroBabelFileMetadata} from 'metro-babel-transformer';
import type {Context} from '../generateFunctionMap';

const {
functionMapBabelPlugin,
generateFunctionMap,
generateFunctionMappingsArray,
} = require('../generateFunctionMap');
Expand Down Expand Up @@ -1605,6 +1607,43 @@ function parent2() {
});
});

describe('functionMapBabelPlugin', () => {
it('exports a Babel plugin to be used during transformation', () => {
const code = 'export default function foo(bar){}';
const result = transformFromAstSync<MetroBabelFileMetadata>(
getAst(code),
code,
{
filename: 'file.js',
cwd: '/my/root',
plugins: [functionMapBabelPlugin],
},
);
expect(result.metadata.metro?.functionMap).toEqual({
mappings: 'AAA,eC',
names: ['<global>', 'foo'],
});
});

it('omits parent class name when it matches filename', () => {
const ast = getAst('class FooBar { baz() {} }');
expect(
transformFromAstSync<MetroBabelFileMetadata>(ast, '', {
plugins: [functionMapBabelPlugin],
filename: 'FooBar.ios.js',
}).metadata.metro?.functionMap,
).toMatchInlineSnapshot(`
Object {
"mappings": "AAA,eC,QD",
"names": Array [
"FooBar",
"baz",
],
}
`);
});
});

describe('@babel/traverse path cache workaround (babel#6437)', () => {
/* These tests exist due to the need to work around a Babel issue:
https://github.com/babel/babel/issues/6437
Expand Down

0 comments on commit 42fdbc2

Please sign in to comment.