diff --git a/docs/CLI.md b/docs/CLI.md index 8efbde82e3..6a7014245b 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -24,6 +24,8 @@ Generates a JavaScript bundle containing the specified entrypoint and its descen | `source-map` | | Whether Metro should generate source maps | Boolean | | `source-map-url` | | URL where the source map can be found | String | | `legacy-bundler` | | Whether Metro should use the legacy bundler | Boolean | +| `resolver-option` | | [Custom resolver options](./Resolution.md#customresolveroptions-string-mixed) of the form `key=value` | Array | +| `transform-option` | | Custom transform options of the form `key=value` | Array | ## `serve` diff --git a/packages/metro/src/cli/__tests__/parseKeyValueParamArray-test.js b/packages/metro/src/cli/__tests__/parseKeyValueParamArray-test.js new file mode 100644 index 0000000000..e26e769e42 --- /dev/null +++ b/packages/metro/src/cli/__tests__/parseKeyValueParamArray-test.js @@ -0,0 +1,66 @@ +/** + * 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-local + * @format + * @oncall react_native + */ + +import parseKeyValueParamArray from '../parseKeyValueParamArray'; + +test('empty', () => { + expect(parseKeyValueParamArray([])).toEqual({}); +}); + +test('result has nullish prototype', () => { + // eslint-disable-next-line no-proto + expect(parseKeyValueParamArray([]).__proto__).toBe(undefined); +}); + +test('single prop', () => { + expect(parseKeyValueParamArray(['foo=bar'])).toEqual({ + foo: 'bar', + }); +}); + +test('repeated prop, last one wins', () => { + expect(parseKeyValueParamArray(['foo=bar', 'foo=baz'])).toEqual({ + foo: 'baz', + }); +}); + +test('multiple props', () => { + expect(parseKeyValueParamArray(['foo=bar', 'baz=quux'])).toEqual({ + foo: 'bar', + baz: 'quux', + }); +}); + +test('"&" is not allowed', () => { + expect(() => + parseKeyValueParamArray(['foo=bar&baz=quux']), + ).toThrowErrorMatchingInlineSnapshot( + `"Parameter cannot include \\"&\\" but found: foo=bar&baz=quux"`, + ); +}); + +test('"=" is required', () => { + expect(() => + parseKeyValueParamArray(['foo', 'bar']), + ).toThrowErrorMatchingInlineSnapshot( + `"Expected parameter to include \\"=\\" but found: foo"`, + ); +}); + +test('multiple "=" characters', () => { + expect(parseKeyValueParamArray(['a=b=c'])).toEqual({a: 'b=c'}); +}); + +test('performs URL decoding', () => { + expect(parseKeyValueParamArray(['a=b%20c'])).toEqual({a: 'b c'}); + expect(parseKeyValueParamArray(['a=b%26c'])).toEqual({a: 'b&c'}); + expect(parseKeyValueParamArray(['a%3Db=c'])).toEqual({'a=b': 'c'}); +}); diff --git a/packages/metro/src/cli/parseKeyValueParamArray.js b/packages/metro/src/cli/parseKeyValueParamArray.js new file mode 100644 index 0000000000..d89af4a7cd --- /dev/null +++ b/packages/metro/src/cli/parseKeyValueParamArray.js @@ -0,0 +1,31 @@ +/** + * 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-local + * @format + * @oncall react_native + */ + +import querystring from 'querystring'; + +export default function coerceKeyValueArray( + keyValueArray: $ReadOnlyArray, +): { + [key: string]: string, + __proto__: null, +} { + const result: {[key: string]: string, __proto__: null} = Object.create(null); + for (const item of keyValueArray) { + if (item.indexOf('=') === -1) { + throw new Error('Expected parameter to include "=" but found: ' + item); + } + if (item.indexOf('&') !== -1) { + throw new Error('Parameter cannot include "&" but found: ' + item); + } + Object.assign(result, querystring.parse(item)); + } + return result; +} diff --git a/packages/metro/src/commands/build.js b/packages/metro/src/commands/build.js index 7208c426e9..391d12be30 100644 --- a/packages/metro/src/commands/build.js +++ b/packages/metro/src/commands/build.js @@ -9,8 +9,10 @@ * @oncall react_native */ -'use strict'; +import parseKeyValueParamArray from '../cli/parseKeyValueParamArray'; +import type {CustomTransformOptions} from 'metro-babel-transformer'; +import type {CustomResolverOptions} from 'metro-resolver'; import type {RunBuildOptions} from '../index'; import typeof Yargs from 'yargs'; import type {ModuleObject} from 'yargs'; @@ -37,6 +39,8 @@ type Args = $ReadOnly<{ resetCache?: boolean, sourceMap?: boolean, sourceMapUrl?: string, + transformOption: CustomTransformOptions, + resolverOption: CustomResolverOptions, }>; module.exports = (): { @@ -69,6 +73,23 @@ module.exports = (): { yargs.option('config', {alias: 'c', type: 'string'}); + yargs.option('transform-option', { + type: 'string', + array: true, + alias: 'transformer-option', + coerce: (parseKeyValueParamArray: $FlowFixMe), + describe: + 'Custom transform options of the form key=value. URL-encoded. May be specified multiple times.', + }); + + yargs.option('resolver-option', { + type: 'string', + array: true, + coerce: (parseKeyValueParamArray: $FlowFixMe), + describe: + 'Custom resolver options of the form key=value. URL-encoded. May be specified multiple times.', + }); + // Deprecated yargs.option('reset-cache', {type: 'boolean'}); }, @@ -83,6 +104,8 @@ module.exports = (): { platform: argv.platform, sourceMap: argv.sourceMap, sourceMapUrl: argv.sourceMapUrl, + customResolverOptions: argv.resolverOption, + customTransformOptions: argv.transformOption, }; // Inline require() to avoid circular dependency with ../index diff --git a/packages/metro/src/index.flow.js b/packages/metro/src/index.flow.js index 9a31e20db0..6c31045363 100644 --- a/packages/metro/src/index.flow.js +++ b/packages/metro/src/index.flow.js @@ -11,6 +11,7 @@ 'use strict'; +import type {CustomResolverOptions} from 'metro-resolver'; import type {ReadOnlyGraph} from './DeltaBundler'; import type {ServerOptions} from './Server'; import type {OutputOptions, RequestOptions} from './shared/types.flow.js'; @@ -112,6 +113,8 @@ export type RunBuildOptions = { platform?: string, sourceMap?: boolean, sourceMapUrl?: string, + customResolverOptions?: CustomResolverOptions, + customTransformOptions?: CustomTransformOptions, }; type BuildCommandOptions = {} | null; @@ -347,6 +350,8 @@ exports.runServer = async ( exports.runBuild = async ( config: ConfigT, { + customResolverOptions, + customTransformOptions, dev = false, entry, onBegin, @@ -378,6 +383,8 @@ exports.runBuild = async ( sourceMapUrl: sourceMap === false ? undefined : sourceMapUrl, createModuleIdFactory: config.serializer.createModuleIdFactory, onProgress, + customResolverOptions, + customTransformOptions, }; if (onBegin) { diff --git a/packages/metro/src/shared/types.flow.js b/packages/metro/src/shared/types.flow.js index 928f2db455..0bf2b40ca7 100644 --- a/packages/metro/src/shared/types.flow.js +++ b/packages/metro/src/shared/types.flow.js @@ -139,6 +139,8 @@ export type RequestOptions = { platform: string, createModuleIdFactory?: () => (path: string) => number, onProgress?: (transformedFileCount: number, totalFileCount: number) => void, + +customResolverOptions?: CustomResolverOptions, + +customTransformOptions?: CustomTransformOptions, }; export type {MinifierOptions};