793 lines
22 KiB
Plaintext
793 lines
22 KiB
Plaintext
/**
|
|
* 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 type {PluginEntry, Plugins} from '@babel/core';
|
|
import type {
|
|
BabelTransformer,
|
|
BabelTransformerArgs,
|
|
CustomTransformOptions,
|
|
TransformProfile,
|
|
} from 'metro-babel-transformer';
|
|
import type {
|
|
BasicSourceMap,
|
|
FBSourceFunctionMap,
|
|
MetroSourceMapSegmentTuple,
|
|
} from 'metro-source-map';
|
|
import type {
|
|
ImportExportPluginOptions,
|
|
InlinePluginOptions,
|
|
InlineRequiresPluginOptions,
|
|
} from 'metro-transform-plugins';
|
|
import type {TransformResultDependency} from 'metro/private/DeltaBundler';
|
|
import type {AllowOptionalDependencies} from 'metro/private/DeltaBundler/types';
|
|
import type {
|
|
DependencyTransformer,
|
|
DynamicRequiresBehavior,
|
|
} from 'metro/private/ModuleGraph/worker/collectDependencies';
|
|
|
|
import * as assetTransformer from './utils/assetTransformer';
|
|
import getMinifier from './utils/getMinifier';
|
|
import {transformFromAstSync} from '@babel/core';
|
|
import generate from '@babel/generator';
|
|
import * as babylon from '@babel/parser';
|
|
import * as types from '@babel/types';
|
|
import {stableHash} from 'metro-cache';
|
|
import {getCacheKey as metroGetCacheKey} from 'metro-cache-key';
|
|
import {
|
|
fromRawMappings,
|
|
functionMapBabelPlugin,
|
|
toBabelSegments,
|
|
toSegmentTuple,
|
|
} from 'metro-source-map';
|
|
import metroTransformPlugins from 'metro-transform-plugins';
|
|
import collectDependencies from 'metro/private/ModuleGraph/worker/collectDependencies';
|
|
import generateImportNames from 'metro/private/ModuleGraph/worker/generateImportNames';
|
|
import {
|
|
importLocationsPlugin,
|
|
locToKey,
|
|
} from 'metro/private/ModuleGraph/worker/importLocationsPlugin';
|
|
import * as JsFileWrapping from 'metro/private/ModuleGraph/worker/JsFileWrapping';
|
|
import nullthrows from 'nullthrows';
|
|
|
|
const InternalInvalidRequireCallError =
|
|
collectDependencies.InvalidRequireCallError;
|
|
|
|
type MinifierConfig = $ReadOnly<{[string]: mixed, ...}>;
|
|
|
|
export type MinifierOptions = {
|
|
code: string,
|
|
map: ?BasicSourceMap,
|
|
filename: string,
|
|
reserved: $ReadOnlyArray<string>,
|
|
config: MinifierConfig,
|
|
...
|
|
};
|
|
|
|
export type MinifierResult = {
|
|
code: string,
|
|
map?: BasicSourceMap,
|
|
...
|
|
};
|
|
|
|
export type Minifier = MinifierOptions =>
|
|
| MinifierResult
|
|
| Promise<MinifierResult>;
|
|
|
|
export type Type = 'script' | 'module' | 'asset';
|
|
|
|
export type JsTransformerConfig = $ReadOnly<{
|
|
assetPlugins: $ReadOnlyArray<string>,
|
|
assetRegistryPath: string,
|
|
asyncRequireModulePath: string,
|
|
babelTransformerPath: string,
|
|
dynamicDepsInPackages: DynamicRequiresBehavior,
|
|
enableBabelRCLookup: boolean,
|
|
enableBabelRuntime: boolean | string,
|
|
globalPrefix: string,
|
|
hermesParser: boolean,
|
|
minifierConfig: MinifierConfig,
|
|
minifierPath: string,
|
|
optimizationSizeLimit: number,
|
|
publicPath: string,
|
|
allowOptionalDependencies: AllowOptionalDependencies,
|
|
unstable_dependencyMapReservedName: ?string,
|
|
unstable_disableModuleWrapping: boolean,
|
|
unstable_disableNormalizePseudoGlobals: boolean,
|
|
unstable_compactOutput: boolean,
|
|
/** Enable `require.context` statements which can be used to import multiple files in a directory. */
|
|
unstable_allowRequireContext: boolean,
|
|
/** With inlineRequires, enable a module-scope memo var and inline as (v || v=require('foo')) */
|
|
unstable_memoizeInlineRequires?: boolean,
|
|
/** With inlineRequires, do not memoize these module specifiers */
|
|
unstable_nonMemoizedInlineRequires?: $ReadOnlyArray<string>,
|
|
/** Whether to rename scoped `require` functions to `_$$_REQUIRE`, usually an extraneous operation when serializing to iife (default). */
|
|
unstable_renameRequire?: boolean,
|
|
}>;
|
|
|
|
export type {CustomTransformOptions} from 'metro-babel-transformer';
|
|
|
|
export type JsTransformOptions = $ReadOnly<{
|
|
customTransformOptions?: CustomTransformOptions,
|
|
dev: boolean,
|
|
experimentalImportSupport?: boolean,
|
|
inlinePlatform: boolean,
|
|
inlineRequires: boolean,
|
|
minify: boolean,
|
|
nonInlinedRequires?: $ReadOnlyArray<string>,
|
|
platform: ?string,
|
|
type: Type,
|
|
unstable_memoizeInlineRequires?: boolean,
|
|
unstable_nonMemoizedInlineRequires?: $ReadOnlyArray<string>,
|
|
unstable_staticHermesOptimizedRequire?: boolean,
|
|
unstable_transformProfile: TransformProfile,
|
|
}>;
|
|
|
|
opaque type Path = string;
|
|
|
|
type BaseFile = $ReadOnly<{
|
|
code: string,
|
|
filename: Path,
|
|
inputFileSize: number,
|
|
}>;
|
|
|
|
type AssetFile = $ReadOnly<{
|
|
...BaseFile,
|
|
type: 'asset',
|
|
}>;
|
|
|
|
type JSFileType = 'js/script' | 'js/module' | 'js/module/asset';
|
|
|
|
type JSFile = $ReadOnly<{
|
|
...BaseFile,
|
|
ast?: ?BabelNodeFile,
|
|
type: JSFileType,
|
|
functionMap: FBSourceFunctionMap | null,
|
|
unstable_importDeclarationLocs?: ?$ReadOnlySet<string>,
|
|
}>;
|
|
|
|
type JSONFile = {
|
|
...BaseFile,
|
|
type: Type,
|
|
};
|
|
|
|
type TransformationContext = $ReadOnly<{
|
|
config: JsTransformerConfig,
|
|
projectRoot: Path,
|
|
options: JsTransformOptions,
|
|
}>;
|
|
|
|
export type JsOutput = $ReadOnly<{
|
|
data: $ReadOnly<{
|
|
code: string,
|
|
lineCount: number,
|
|
map: Array<MetroSourceMapSegmentTuple>,
|
|
functionMap: ?FBSourceFunctionMap,
|
|
}>,
|
|
type: JSFileType,
|
|
}>;
|
|
|
|
type TransformResponse = $ReadOnly<{
|
|
dependencies: $ReadOnlyArray<TransformResultDependency>,
|
|
output: $ReadOnlyArray<JsOutput>,
|
|
}>;
|
|
|
|
function getDynamicDepsBehavior(
|
|
inPackages: DynamicRequiresBehavior,
|
|
filename: string,
|
|
): DynamicRequiresBehavior {
|
|
switch (inPackages) {
|
|
case 'reject':
|
|
return 'reject';
|
|
case 'throwAtRuntime':
|
|
const isPackage = /(?:^|[/\\])node_modules[/\\]/.test(filename);
|
|
return isPackage ? inPackages : 'reject';
|
|
default:
|
|
(inPackages: empty);
|
|
throw new Error(
|
|
`invalid value for dynamic deps behavior: \`${inPackages}\``,
|
|
);
|
|
}
|
|
}
|
|
|
|
const minifyCode = async (
|
|
config: JsTransformerConfig,
|
|
projectRoot: string,
|
|
filename: string,
|
|
code: string,
|
|
source: string,
|
|
map: Array<MetroSourceMapSegmentTuple>,
|
|
reserved?: $ReadOnlyArray<string> = [],
|
|
): Promise<{
|
|
code: string,
|
|
map: Array<MetroSourceMapSegmentTuple>,
|
|
...
|
|
}> => {
|
|
const sourceMap = fromRawMappings([
|
|
{
|
|
code,
|
|
source,
|
|
map,
|
|
// functionMap is overridden by the serializer
|
|
functionMap: null,
|
|
path: filename,
|
|
// isIgnored is overriden by the serializer
|
|
isIgnored: false,
|
|
},
|
|
]).toMap(undefined, {});
|
|
|
|
const minify = getMinifier(config.minifierPath);
|
|
|
|
try {
|
|
const minified = await minify({
|
|
code,
|
|
map: sourceMap,
|
|
filename,
|
|
reserved,
|
|
config: config.minifierConfig,
|
|
});
|
|
|
|
return {
|
|
code: minified.code,
|
|
map: minified.map
|
|
? toBabelSegments(minified.map).map(toSegmentTuple)
|
|
: [],
|
|
};
|
|
} catch (error) {
|
|
if (error.constructor.name === 'JS_Parse_Error') {
|
|
throw new Error(
|
|
`${error.message} in file ${filename} at ${error.line}:${error.col}`,
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const disabledDependencyTransformer: DependencyTransformer = {
|
|
transformSyncRequire: () => void 0,
|
|
transformImportCall: () => void 0,
|
|
transformImportMaybeSyncCall: () => void 0,
|
|
transformPrefetch: () => void 0,
|
|
transformIllegalDynamicRequire: () => void 0,
|
|
};
|
|
|
|
class InvalidRequireCallError extends Error {
|
|
innerError: InternalInvalidRequireCallError;
|
|
filename: string;
|
|
|
|
constructor(innerError: InternalInvalidRequireCallError, filename: string) {
|
|
super(`${filename}:${innerError.message}`);
|
|
this.innerError = innerError;
|
|
this.filename = filename;
|
|
}
|
|
}
|
|
|
|
async function transformJS(
|
|
file: JSFile,
|
|
{config, options, projectRoot}: TransformationContext,
|
|
): Promise<TransformResponse> {
|
|
// Transformers can output null ASTs (if they ignore the file). In that case
|
|
// we need to parse the module source code to get their AST.
|
|
let ast = file.ast ?? babylon.parse(file.code, {sourceType: 'unambiguous'});
|
|
|
|
const {importDefault, importAll} = generateImportNames(ast);
|
|
|
|
// Add "use strict" if the file was parsed as a module, and the directive did
|
|
// not exist yet.
|
|
const {directives} = ast.program;
|
|
|
|
if (
|
|
ast.program.sourceType === 'module' &&
|
|
directives != null &&
|
|
directives.findIndex(d => d.value.value === 'use strict') === -1
|
|
) {
|
|
directives.push(types.directive(types.directiveLiteral('use strict')));
|
|
}
|
|
|
|
// Perform the import-export transform (in case it's still needed), then
|
|
// fold requires and perform constant folding (if in dev).
|
|
const plugins: Array<PluginEntry> = [];
|
|
|
|
if (options.experimentalImportSupport === true) {
|
|
plugins.push([
|
|
metroTransformPlugins.importExportPlugin,
|
|
{
|
|
importAll,
|
|
importDefault,
|
|
resolve: false,
|
|
} as ImportExportPluginOptions,
|
|
]);
|
|
}
|
|
|
|
if (options.inlineRequires) {
|
|
plugins.push([
|
|
metroTransformPlugins.inlineRequiresPlugin,
|
|
{
|
|
ignoredRequires: options.nonInlinedRequires,
|
|
inlineableCalls: [importDefault, importAll],
|
|
memoizeCalls:
|
|
// $FlowFixMe[incompatible-type] is this always (?boolean)?
|
|
options.customTransformOptions?.unstable_memoizeInlineRequires ??
|
|
options.unstable_memoizeInlineRequires,
|
|
nonMemoizedModules: options.unstable_nonMemoizedInlineRequires,
|
|
} as InlineRequiresPluginOptions,
|
|
]);
|
|
}
|
|
|
|
plugins.push([
|
|
metroTransformPlugins.inlinePlugin,
|
|
{
|
|
dev: options.dev,
|
|
inlinePlatform: options.inlinePlatform,
|
|
isWrapped: false,
|
|
// $FlowFixMe[incompatible-type] expects a string if inlinePlatform
|
|
platform: options.platform,
|
|
} as InlinePluginOptions,
|
|
]);
|
|
|
|
ast = nullthrows(
|
|
transformFromAstSync(ast, '', {
|
|
ast: true,
|
|
babelrc: false,
|
|
code: false,
|
|
configFile: false,
|
|
comments: true,
|
|
filename: file.filename,
|
|
plugins,
|
|
sourceMaps: false,
|
|
// Not-Cloning the input AST here should be safe because other code paths above this call
|
|
// are mutating the AST as well and no code is depending on the original AST.
|
|
// However, switching the flag to false caused issues with ES Modules if `experimentalImportSupport` isn't used https://github.com/facebook/metro/issues/641
|
|
// either because one of the plugins is doing something funky or Babel messes up some caches.
|
|
// Make sure to test the above mentioned case before flipping the flag back to false.
|
|
cloneInputAst: true,
|
|
}).ast,
|
|
);
|
|
|
|
if (!options.dev) {
|
|
// Run the constant folding plugin in its own pass, avoiding race conditions
|
|
// with other plugins that have exit() visitors on Program (e.g. the ESM
|
|
// transform).
|
|
ast = nullthrows(
|
|
transformFromAstSync(ast, '', {
|
|
ast: true,
|
|
babelrc: false,
|
|
code: false,
|
|
configFile: false,
|
|
comments: true,
|
|
filename: file.filename,
|
|
plugins: [metroTransformPlugins.constantFoldingPlugin],
|
|
sourceMaps: false,
|
|
cloneInputAst: false,
|
|
}).ast,
|
|
);
|
|
}
|
|
|
|
let dependencyMapName = '';
|
|
let dependencies;
|
|
let wrappedAst;
|
|
|
|
// If the module to transform is a script (meaning that is not part of the
|
|
// dependency graph and it code will just be prepended to the bundle modules),
|
|
// we need to wrap it differently than a commonJS module (also, scripts do
|
|
// not have dependencies).
|
|
if (file.type === 'js/script') {
|
|
dependencies = [];
|
|
wrappedAst = JsFileWrapping.wrapPolyfill(ast);
|
|
} else {
|
|
try {
|
|
const importDeclarationLocs = file.unstable_importDeclarationLocs ?? null;
|
|
const opts = {
|
|
asyncRequireModulePath: config.asyncRequireModulePath,
|
|
dependencyTransformer:
|
|
config.unstable_disableModuleWrapping === true
|
|
? disabledDependencyTransformer
|
|
: undefined,
|
|
dynamicRequires: getDynamicDepsBehavior(
|
|
config.dynamicDepsInPackages,
|
|
file.filename,
|
|
),
|
|
inlineableCalls: [importDefault, importAll],
|
|
keepRequireNames: options.dev,
|
|
allowOptionalDependencies: config.allowOptionalDependencies,
|
|
dependencyMapName: config.unstable_dependencyMapReservedName,
|
|
unstable_allowRequireContext: config.unstable_allowRequireContext,
|
|
unstable_isESMImportAtSource:
|
|
importDeclarationLocs != null
|
|
? (loc: BabelSourceLocation) =>
|
|
importDeclarationLocs.has(locToKey(loc))
|
|
: null,
|
|
};
|
|
({ast, dependencies, dependencyMapName} = collectDependencies(ast, opts));
|
|
} catch (error) {
|
|
if (error instanceof InternalInvalidRequireCallError) {
|
|
throw new InvalidRequireCallError(error, file.filename);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (config.unstable_disableModuleWrapping === true) {
|
|
wrappedAst = ast;
|
|
} else {
|
|
({ast: wrappedAst} = JsFileWrapping.wrapModule(
|
|
ast,
|
|
importDefault,
|
|
importAll,
|
|
dependencyMapName,
|
|
config.globalPrefix,
|
|
// TODO: This config is optional to allow its introduction in a minor
|
|
// release. It should be made non-optional in ConfigT or removed in
|
|
// future.
|
|
config.unstable_renameRequire === false,
|
|
{
|
|
unstable_useStaticHermesModuleFactory: Boolean(
|
|
options.customTransformOptions
|
|
?.unstable_staticHermesOptimizedRequire,
|
|
),
|
|
},
|
|
));
|
|
}
|
|
}
|
|
|
|
const minify =
|
|
options.minify &&
|
|
options.unstable_transformProfile !== 'hermes-canary' &&
|
|
options.unstable_transformProfile !== 'hermes-stable';
|
|
|
|
const reserved = [];
|
|
if (config.unstable_dependencyMapReservedName != null) {
|
|
reserved.push(config.unstable_dependencyMapReservedName);
|
|
}
|
|
if (
|
|
minify &&
|
|
file.inputFileSize <= config.optimizationSizeLimit &&
|
|
!config.unstable_disableNormalizePseudoGlobals
|
|
) {
|
|
reserved.push(
|
|
...metroTransformPlugins.normalizePseudoGlobals(wrappedAst, {
|
|
reservedNames: reserved,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const result = generate(
|
|
wrappedAst,
|
|
{
|
|
comments: true,
|
|
compact: config.unstable_compactOutput,
|
|
filename: file.filename,
|
|
retainLines: false,
|
|
sourceFileName: file.filename,
|
|
sourceMaps: true,
|
|
},
|
|
file.code,
|
|
);
|
|
|
|
let map = result.rawMappings ? result.rawMappings.map(toSegmentTuple) : [];
|
|
let code = result.code;
|
|
|
|
if (minify) {
|
|
({map, code} = await minifyCode(
|
|
config,
|
|
projectRoot,
|
|
file.filename,
|
|
result.code,
|
|
file.code,
|
|
map,
|
|
reserved,
|
|
));
|
|
}
|
|
|
|
let lineCount;
|
|
({lineCount, map} = countLinesAndTerminateMap(code, map));
|
|
|
|
const output: Array<JsOutput> = [
|
|
{
|
|
data: {
|
|
code,
|
|
lineCount,
|
|
map,
|
|
functionMap: file.functionMap,
|
|
},
|
|
type: file.type,
|
|
},
|
|
];
|
|
|
|
return {
|
|
dependencies,
|
|
output,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transforms an asset file
|
|
*/
|
|
async function transformAsset(
|
|
file: AssetFile,
|
|
context: TransformationContext,
|
|
): Promise<TransformResponse> {
|
|
const {assetRegistryPath, assetPlugins} = context.config;
|
|
|
|
const result = await assetTransformer.transform(
|
|
getBabelTransformArgs(file, context),
|
|
assetRegistryPath,
|
|
assetPlugins,
|
|
);
|
|
|
|
const jsFile = {
|
|
...file,
|
|
type: 'js/module/asset' as const,
|
|
ast: result.ast,
|
|
functionMap: null,
|
|
};
|
|
|
|
return transformJS(jsFile, context);
|
|
}
|
|
|
|
/**
|
|
* Transforms a JavaScript file with Babel before processing the file with
|
|
* the generic JavaScript transformation.
|
|
*/
|
|
async function transformJSWithBabel(
|
|
file: JSFile,
|
|
context: TransformationContext,
|
|
): Promise<TransformResponse> {
|
|
const {babelTransformerPath} = context.config;
|
|
// $FlowFixMe[unsupported-syntax] dynamic require
|
|
const transformer: BabelTransformer = require(babelTransformerPath);
|
|
|
|
const transformResult = await transformer.transform(
|
|
getBabelTransformArgs(file, context, [
|
|
// functionMapBabelPlugin populates metadata.metro.functionMap
|
|
functionMapBabelPlugin,
|
|
// importLocationsPlugin populates metadata.metro.unstable_importDeclarationLocs
|
|
importLocationsPlugin,
|
|
]),
|
|
);
|
|
|
|
const jsFile: JSFile = {
|
|
...file,
|
|
ast: transformResult.ast,
|
|
functionMap:
|
|
transformResult.metadata?.metro?.functionMap ??
|
|
// Fallback to deprecated explicitly-generated `functionMap`
|
|
transformResult.functionMap ??
|
|
null,
|
|
unstable_importDeclarationLocs:
|
|
transformResult.metadata?.metro?.unstable_importDeclarationLocs,
|
|
};
|
|
|
|
return await transformJS(jsFile, context);
|
|
}
|
|
|
|
async function transformJSON(
|
|
file: JSONFile,
|
|
{options, config, projectRoot}: TransformationContext,
|
|
): Promise<TransformResponse> {
|
|
let code =
|
|
config.unstable_disableModuleWrapping === true
|
|
? JsFileWrapping.jsonToCommonJS(file.code)
|
|
: JsFileWrapping.wrapJson(
|
|
file.code,
|
|
config.globalPrefix,
|
|
Boolean(
|
|
options.customTransformOptions
|
|
?.unstable_staticHermesOptimizedRequire,
|
|
),
|
|
);
|
|
let map: Array<MetroSourceMapSegmentTuple> = [];
|
|
|
|
// TODO: When we can reuse transformJS for JSON, we should not derive `minify` separately.
|
|
const minify =
|
|
options.minify &&
|
|
options.unstable_transformProfile !== 'hermes-canary' &&
|
|
options.unstable_transformProfile !== 'hermes-stable';
|
|
|
|
if (minify) {
|
|
({map, code} = await minifyCode(
|
|
config,
|
|
projectRoot,
|
|
file.filename,
|
|
code,
|
|
file.code,
|
|
map,
|
|
));
|
|
}
|
|
|
|
let jsType: JSFileType;
|
|
|
|
if (file.type === 'asset') {
|
|
jsType = 'js/module/asset';
|
|
} else if (file.type === 'script') {
|
|
jsType = 'js/script';
|
|
} else {
|
|
jsType = 'js/module';
|
|
}
|
|
|
|
let lineCount;
|
|
({lineCount, map} = countLinesAndTerminateMap(code, map));
|
|
const output: Array<JsOutput> = [
|
|
{
|
|
data: {code, lineCount, map, functionMap: null},
|
|
type: jsType,
|
|
},
|
|
];
|
|
|
|
return {
|
|
dependencies: [],
|
|
output,
|
|
};
|
|
}
|
|
|
|
function getBabelTransformArgs(
|
|
file: $ReadOnly<{filename: Path, code: string, ...}>,
|
|
{options, config, projectRoot}: TransformationContext,
|
|
plugins?: Plugins = [],
|
|
): BabelTransformerArgs {
|
|
const {inlineRequires: _, ...babelTransformerOptions} = options;
|
|
return {
|
|
filename: file.filename,
|
|
options: {
|
|
...babelTransformerOptions,
|
|
enableBabelRCLookup: config.enableBabelRCLookup,
|
|
enableBabelRuntime: config.enableBabelRuntime,
|
|
globalPrefix: config.globalPrefix,
|
|
hermesParser: config.hermesParser,
|
|
projectRoot,
|
|
publicPath: config.publicPath,
|
|
},
|
|
plugins,
|
|
src: file.code,
|
|
};
|
|
}
|
|
|
|
export const transform = async (
|
|
config: JsTransformerConfig,
|
|
projectRoot: string,
|
|
filename: string,
|
|
data: Buffer,
|
|
options: JsTransformOptions,
|
|
): Promise<TransformResponse> => {
|
|
const context: TransformationContext = {
|
|
config,
|
|
projectRoot,
|
|
options,
|
|
};
|
|
const sourceCode = data.toString('utf8');
|
|
|
|
const reservedStrings = [];
|
|
if (
|
|
options.customTransformOptions?.unstable_staticHermesOptimizedRequire ==
|
|
true
|
|
) {
|
|
reservedStrings.push('_$$_METRO_MODULE_ID');
|
|
}
|
|
if (config.unstable_dependencyMapReservedName != null) {
|
|
reservedStrings.push(config.unstable_dependencyMapReservedName);
|
|
}
|
|
for (const reservedString of reservedStrings) {
|
|
const position = sourceCode.indexOf(reservedString);
|
|
if (position > -1) {
|
|
throw new SyntaxError(
|
|
'Source code contains the reserved string `' +
|
|
reservedString +
|
|
'` at character offset ' +
|
|
position,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (filename.endsWith('.json')) {
|
|
const jsonFile: JSONFile = {
|
|
filename,
|
|
inputFileSize: data.length,
|
|
code: sourceCode,
|
|
type: options.type,
|
|
};
|
|
|
|
return await transformJSON(jsonFile, context);
|
|
}
|
|
|
|
if (options.type === 'asset') {
|
|
const file: AssetFile = {
|
|
filename,
|
|
inputFileSize: data.length,
|
|
code: sourceCode,
|
|
type: options.type,
|
|
};
|
|
|
|
return await transformAsset(file, context);
|
|
}
|
|
|
|
const file: JSFile = {
|
|
filename,
|
|
inputFileSize: data.length,
|
|
code: sourceCode,
|
|
type: options.type === 'script' ? 'js/script' : 'js/module',
|
|
functionMap: null,
|
|
};
|
|
|
|
return await transformJSWithBabel(file, context);
|
|
};
|
|
|
|
export const getCacheKey = (config: JsTransformerConfig): string => {
|
|
const {babelTransformerPath, minifierPath, ...remainingConfig} = config;
|
|
|
|
const filesKey = metroGetCacheKey([
|
|
__filename,
|
|
require.resolve(babelTransformerPath),
|
|
require.resolve(minifierPath),
|
|
require.resolve('./utils/getMinifier'),
|
|
require.resolve('./utils/assetTransformer'),
|
|
require.resolve('metro/private/ModuleGraph/worker/generateImportNames'),
|
|
require.resolve('metro/private/ModuleGraph/worker/JsFileWrapping'),
|
|
...metroTransformPlugins.getTransformPluginCacheKeyFiles(),
|
|
]);
|
|
|
|
// $FlowFixMe[unsupported-syntax]
|
|
const babelTransformer = require(babelTransformerPath);
|
|
return [
|
|
filesKey,
|
|
stableHash(remainingConfig).toString('hex'),
|
|
babelTransformer.getCacheKey ? babelTransformer.getCacheKey() : '',
|
|
].join('$');
|
|
};
|
|
|
|
function countLinesAndTerminateMap(
|
|
code: string,
|
|
map: $ReadOnlyArray<MetroSourceMapSegmentTuple>,
|
|
): {
|
|
lineCount: number,
|
|
map: Array<MetroSourceMapSegmentTuple>,
|
|
} {
|
|
const NEWLINE = /\r\n?|\n|\u2028|\u2029/g;
|
|
let lineCount = 1;
|
|
let lastLineStart = 0;
|
|
|
|
// Count lines and keep track of where the last line starts
|
|
for (const match of code.matchAll(NEWLINE)) {
|
|
lineCount++;
|
|
lastLineStart = match.index + match[0].length;
|
|
}
|
|
const lastLineLength = code.length - lastLineStart;
|
|
const lastLineIndex1Based = lineCount;
|
|
const lastLineNextColumn0Based = lastLineLength;
|
|
|
|
// If there isn't a mapping at one-past-the-last column of the last line,
|
|
// add one that maps to nothing. This ensures out-of-bounds lookups hit the
|
|
// null mapping rather than aliasing to whichever mapping happens to be last.
|
|
// ASSUMPTION: Mappings are generated in order of increasing line and column.
|
|
const lastMapping = map[map.length - 1];
|
|
const terminatingMapping = [lastLineIndex1Based, lastLineNextColumn0Based];
|
|
if (
|
|
!lastMapping ||
|
|
lastMapping[0] !== terminatingMapping[0] ||
|
|
lastMapping[1] !== terminatingMapping[1]
|
|
) {
|
|
return {
|
|
lineCount,
|
|
map: map.concat([terminatingMapping]),
|
|
};
|
|
}
|
|
return {lineCount, map: [...map]};
|
|
}
|
|
|
|
/**
|
|
* Backwards-compatibility with CommonJS consumers using interopRequireDefault.
|
|
* Do not add to this list.
|
|
*
|
|
* @deprecated Default import from 'metro-transform-worker' is deprecated, use named exports.
|
|
*/
|
|
export default {
|
|
getCacheKey,
|
|
transform,
|
|
};
|