Skip to content
Draft
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
21 changes: 21 additions & 0 deletions flow-typed/sourcemap-codec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/

declare module '@jridgewell/sourcemap-codec' {
declare export type SourceMapSegment =
| [number]
| [number, number, number, number]
| [number, number, number, number, number];

declare export function decode(
mappings: string,
): Array<Array<SourceMapSegment>>;
}
18 changes: 17 additions & 1 deletion flow-typed/uglify.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,21 @@ declare module 'terser' {
declare function minify(
code: _Input,
options: {..._Options, sourceMap: _SourceMapOptions, ...},
): _Error | {..._Result, map: string};
):
| _Error
| {
..._Result,
map: string,
decoded_map?: ?{
mappings: Array<
Array<
| [number]
| [number, number, number, number]
| [number, number, number, number, number],
>,
>,
names: Array<string>,
...
},
};
}
22 changes: 22 additions & 0 deletions packages/metro-minify-terser/src/__tests__/minify-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,26 @@ describe('Minification:', () => {
const result = await minify({...baseOptions, filename});
expect(result.map).toEqual({...map, sources: [filename]});
});

test('returns the decoded source map provided by terser, and encodes `map` only when read', async () => {
const decodedMap = {...map, mappings: [[[0, 0, 0, 0]]]};
let encodeCount = 0;
/* $FlowFixMe[incompatible-type] The mocked `minify` result isn't typed as
* Terser's. */
terser.minify.mockResolvedValue({
code: '',
decoded_map: decodedMap,
// flowlint-next-line unsafe-getters-setters:off
get map() {
encodeCount++;
return JSON.stringify(map);
},
});
const result = await minify({...baseOptions, filename});
expect(result.decodedMap).toEqual(decodedMap);
expect(encodeCount).toBe(0);
expect(result.map).toEqual({...map, sources: [filename]});
expect(result.map).toBe(result.map);
expect(encodeCount).toBe(1);
});
});
34 changes: 23 additions & 11 deletions packages/metro-minify-terser/src/minifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @oncall react_native
*/

import type {BasicSourceMap} from 'metro-source-map';
import type {BabelDecodedMap, BasicSourceMap} from 'metro-source-map';
import type {MinifierOptions, MinifierResult} from 'metro-transform-worker';

import terser from 'terser';
Expand All @@ -18,22 +18,33 @@ export default async function minifier(
options: MinifierOptions,
): Promise<MinifierResult> {
const result = await minify(options);
const {getMap} = result;

if (!options.map || result.map == null) {
if (!options.map || getMap == null) {
return {code: result.code};
}

const map: BasicSourceMap = JSON.parse(result.map);
let map: ?BasicSourceMap;

return {code: result.code, map: {...map, sources: [options.filename]}};
return {
code: result.code,
// Terser encodes its map only when `result.map` is read.
// flowlint-next-line unsafe-getters-setters:off
get map(): BasicSourceMap {
if (map == null) {
map = {...JSON.parse(getMap()), sources: [options.filename]};
}
return map;
},
decodedMap: result.decodedMap,
};
}

async function minify({
code,
map,
reserved,
config,
}: MinifierOptions): Promise<{code: string, map: ?string}> {
async function minify({code, map, reserved, config}: MinifierOptions): Promise<{
code: string,
getMap: ?() => string,
decodedMap: ?BabelDecodedMap,
}> {
const options = {
...config,
output: {
Expand Down Expand Up @@ -65,6 +76,7 @@ async function minify({

return {
code: result.code,
map: result.map,
getMap: options.sourceMap ? () => result.map : null,
decodedMap: result.decoded_map,
};
}
3 changes: 3 additions & 0 deletions packages/metro-source-map/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,13 @@ export interface SourceMapGenerator {
toString(file?: string, options?: {excludeSource?: boolean | undefined}): string;
}

// @deprecated
export function toBabelSegments(sourceMap: BasicSourceMap): Array<BabelSourceMapSegment>;

// @deprecated
export function toSegmentTuple(mapping: BabelSourceMapSegment): MetroSourceMapSegmentTuple;

// @deprecated
export function tuplesFromBabelDecodedMap(decodedMap: BabelDecodedMap): Array<MetroSourceMapSegmentTuple>;

export type VlqMap = {
Expand Down
8 changes: 8 additions & 0 deletions packages/metro-source-map/src/source-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ function toIndexMapSection(module: RawMappingsModule): BasicSourceMap {
/**
* Transforms a standard source map object into a Raw Mappings object, to be
* used across the bundler.
*
* @deprecated Unused by Metro, and will be removed. `Consumer`'s
* `generatedMappings()` decodes a source map.
*/
function toBabelSegments(
sourceMap: BasicSourceMap,
Expand Down Expand Up @@ -383,6 +386,9 @@ function toBabelSegments(
return rawMappings;
}

/**
* @deprecated Unused by Metro, and will be removed.
*/
function toSegmentTuple(
mapping: BabelSourceMapSegment,
): MetroSourceMapSegmentTuple {
Expand All @@ -409,6 +415,8 @@ function toSegmentTuple(
* during generation, whereas accessing `rawMappings` triggers a second decode
* (`allMappings`) that allocates ~4-5 objects per segment. No terminating
* mapping is appended (callers that need one use `countLinesAndTerminateMap`).
*
* @deprecated Unused by Metro, and will be removed.
*/
function tuplesFromBabelDecodedMap(
decodedMap: BabelDecodedMap,
Expand Down
6 changes: 5 additions & 1 deletion packages/metro-transform-worker/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export type MinifierOptions = {
config: MinifierConfig;
};

export type MinifierResult = {code: string; map?: BasicSourceMap | undefined};
export type MinifierResult = {
code: string;
map?: BasicSourceMap | undefined;
decodedMap?: null | undefined | BabelDecodedMap;
};

export const transform: (config: JsTransformerConfig, projectRoot: string, projectRelativePath: string, data: Buffer, options: JsTransformOptions) => Promise<TransformResponse>;

Expand Down
1 change: 1 addition & 0 deletions packages/metro-transform-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@babel/generator": "^7.29.1",
"@babel/parser": "^7.29.9",
"@babel/types": "^7.29.8",
"@jridgewell/sourcemap-codec": "^1.5.0",
"flow-enums-runtime": "^0.0.6",
"metro": "0.87.1",
"metro-babel-transformer": "0.87.1",
Expand Down
48 changes: 39 additions & 9 deletions packages/metro-transform-worker/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
'use strict';

jest
.mock('../utils/getMinifier', () => () => ({code, map, config}) => {
const trimmed = config.output.comments
? code
: code.replace('/*#__PURE__*/', '');
return {
code: trimmed.replace('arbitrary(code)', 'minified(code)'),
map,
};
.mock('../utils/getMinifier', () => {
const minifier = jest.fn(({code, map, config}) => {
const trimmed = config.output.comments
? code
: code.replace('/*#__PURE__*/', '');
return {
code: trimmed.replace('arbitrary(code)', 'minified(code)'),
map,
};
});
return () => minifier;
})
.mock('metro-transform-plugins', () => ({
...jest.requireActual('metro-transform-plugins'),
Expand All @@ -32,6 +35,7 @@ import type {JsTransformerConfig, JsTransformOptions} from '../index';
import typeof * as TransformerType from '../index';
import typeof FSType from 'node:fs';

const {vlqMapFromBabelDecodedMap} = require('metro-source-map');
const {Buffer} = require('node:buffer');
const path = require('node:path');

Expand Down Expand Up @@ -433,7 +437,7 @@ test('emits a compact VlqMap for both the non-minified and minified paths', asyn
source,
{...baseTransformOptions, experimentalImportSupport: true},
);
// Minified path re-encodes the minifier's tuple output to VLQ.
// Minified path encodes VLQ from the minifier's decoded map.
const minifiedResult = await Transformer.transform(
baseConfig,
'/root',
Expand All @@ -455,6 +459,32 @@ test('emits a compact VlqMap for both the non-minified and minified paths', asyn
}
});

test("uses the minifier's decoded map, if it has one, without reading `map`", async () => {
const decodedMap = {mappings: [[[0, 0, 0, 0, 0]]], names: ['arbitrary']};
jest
.requireMock('../utils/getMinifier')()
.mockImplementationOnce(() => ({
code: 'minified(code);',
decodedMap,
// flowlint-next-line unsafe-getters-setters:off
get map() {
throw new Error('Expected the decoded map to be used');
},
}));

const result = await Transformer.transform(
baseConfig,
'/root',
'local/file.js',
Buffer.from('arbitrary(code);', 'utf8'),
{...baseTransformOptions, dev: false, minify: true},
);

expect(result.output[0].data.map).toEqual(
vlqMapFromBabelDecodedMap(decodedMap, [1, 'minified(code);'.length]),
);
});

test('throws if the reserved dependency map name appears in the input', async () => {
await expect(
Transformer.transform(
Expand Down
Loading
Loading