/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const JsFileWrapping = require('../ModuleGraph/worker/JsFileWrapping');
const assetTransformer = require('../assetTransformer');
const babylon = require('@babel/parser');
const collectDependencies = require('../ModuleGraph/worker/collectDependencies');
const constantFoldingPlugin = require('./worker/constant-folding-plugin');
const generateImportNames = require('../ModuleGraph/worker/generateImportNames');
const generate = require('@babel/generator').default;
const getKeyFromFiles = require('../lib/getKeyFromFiles');
const getMinifier = require('../lib/getMinifier');
const importExportPlugin = require('./worker/import-export-plugin');
const inlinePlugin = require('./worker/inline-plugin');
const inlineRequiresPlugin = require('babel-preset-fbjs/plugins/inline-requires');
const normalizePseudoglobals = require('./worker/normalizePseudoglobals');
const {transformFromAstSync} = require('@babel/core');
const {stableHash} = require('metro-cache');
const types = require('@babel/types');
const {
fromRawMappings,
toBabelSegments,
toSegmentTuple,
} = require('metro-source-map');
import type {TransformResultDependency} from 'metro/src/DeltaBundler';
import type {DynamicRequiresBehavior} from '../ModuleGraph/worker/collectDependencies';
import type {BabelSourceMap} from '@babel/core';
import type {MetroSourceMapSegmentTuple} from 'metro-source-map';
type MinifierConfig = $ReadOnly<{[string]: mixed}>;
export type MinifierOptions = {
code: string,
map: ?BabelSourceMap,
filename: string,
reserved: $ReadOnlyArray<string>,
config: MinifierConfig,
};
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,
minifierConfig: MinifierConfig,
minifierPath: string,
optimizationSizeLimit: number,
publicPath: string,
|}>;
import type {CustomTransformOptions} from 'metro-babel-transformer';
export type {CustomTransformOptions} from 'metro-babel-transformer';
export type JsTransformOptions = $ReadOnly<{|
customTransformOptions?: CustomTransformOptions,
dev: boolean,
experimentalImportSupport?: boolean,
hot: boolean,
inlineRequires: boolean,
minify: boolean,
platform: ?string,
type: Type,
|}>;
export type JsOutput = $ReadOnly<{|
data: $ReadOnly<{|
code: string,
map: Array<MetroSourceMapSegmentTuple>,
|}>,
type: string,
|}>;
type Result = {|
output: $ReadOnlyArray<JsOutput>,
dependencies: $ReadOnlyArray<TransformResultDependency>,
|};
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}\``,
);
}
}
class JsTransformer {
_config: JsTransformerConfig;
_projectRoot: string;
constructor(projectRoot: string, config: JsTransformerConfig) {
this._projectRoot = projectRoot;
this._config = config;
}
async transform(
filename: string,
data: Buffer,
options: JsTransformOptions,
): Promise<Result> {
const sourceCode = data.toString('utf8');
let type = 'js/module';
if (options.type === 'asset') {
type = 'js/module/asset';
}
if (options.type === 'script') {
type = 'js/script';
}
if (filename.endsWith('.json')) {
let code = JsFileWrapping.wrapJson(sourceCode);
let map = [];
if (options.minify) {
({map, code} = await this._minifyCode(filename, code, sourceCode, map));
}
return {dependencies: [], output: [{data: {code, map}, type}]};
}
// $FlowFixMe TODO t26372934 Plugin system
const transformer: Transformer<*> = require(this._config
.babelTransformerPath);
const transformerArgs = {
filename,
options: {
...options,
enableBabelRCLookup: this._config.enableBabelRCLookup,
enableBabelRuntime: this._config.enableBabelRuntime,
// Inline requires are now performed at a secondary step. We cannot
// unfortunately remove it from the internal transformer, since this one
// is used by other tooling, and this would affect it.
inlineRequires: false,
projectRoot: this._projectRoot,
publicPath: this._config.publicPath,
},
plugins: [],
src: sourceCode,
};
const transformResult =
type === 'js/module/asset'
? await assetTransformer.transform(
transformerArgs,
this._config.assetRegistryPath,
this._config.assetPlugins,
)
: await transformer.transform(transformerArgs);
// Transformers can ouptut null ASTs (if they ignore the file). In that case
// we need to parse the module source code to get their AST.
let ast =
transformResult.ast ||
babylon.parse(sourceCode, {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.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 = [];
const opts = {
...options,
inlineableCalls: [importDefault, importAll],
importDefault,
importAll,
};
if (options.experimentalImportSupport) {
plugins.push([importExportPlugin, opts]);
}
if (options.inlineRequires) {
plugins.push([inlineRequiresPlugin, opts]);
}
if (!options.dev) {
plugins.push([constantFoldingPlugin, opts]);
plugins.push([inlinePlugin, opts]);
}
({ast} = transformFromAstSync(ast, '', {
ast: true,
babelrc: false,
code: false,
configFile: false,
comments: false,
compact: false,
filename,
plugins,
sourceMaps: false,
}));
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 (type === 'js/script') {
dependencies = [];
wrappedAst = JsFileWrapping.wrapPolyfill(ast);
} else {
try {
const opts = {
asyncRequireModulePath: this._config.asyncRequireModulePath,
dynamicRequires: getDynamicDepsBehavior(
this._config.dynamicDepsInPackages,
filename,
),
inlineableCalls: [importDefault, importAll],
keepRequireNames: options.dev,
};
({dependencies, dependencyMapName} = collectDependencies(ast, opts));
} catch (error) {
if (error instanceof collectDependencies.InvalidRequireCallError) {
throw new InvalidRequireCallError(error, filename);
}
throw error;
}
({ast: wrappedAst} = JsFileWrapping.wrapModule(
ast,
importDefault,
importAll,
dependencyMapName,
));
}
const reserved =
options.minify && data.length <= this._config.optimizationSizeLimit
? normalizePseudoglobals(wrappedAst)
: [];
const result = generate(
wrappedAst,
{
comments: false,
compact: false,
filename,
retainLines: false,
sourceFileName: filename,
sourceMaps: true,
},
sourceCode,
);
let map = result.rawMappings ? result.rawMappings.map(toSegmentTuple) : [];
let code = result.code;
if (options.minify) {
({map, code} = await this._minifyCode(
filename,
result.code,
sourceCode,
map,
reserved,
));
}
return {dependencies, output: [{data: {code, map}, type}]};
}
async _minifyCode(
filename: string,
code: string,
source: string,
map: Array<MetroSourceMapSegmentTuple>,
reserved?: $ReadOnlyArray<string> = [],
): Promise<{
code: string,
map: Array<MetroSourceMapSegmentTuple>,
}> {
const sourceMap = fromRawMappings([
{code, source, map, path: filename},
]).toMap(undefined, {});
const minify = getMinifier(this._config.minifierPath);
try {
const minified = minify({
code,
map: sourceMap,
filename,
reserved,
config: this._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;
}
}
getCacheKey(): string {
const {babelTransformerPath, minifierPath, ...config} = this._config;
const filesKey = getKeyFromFiles([
require.resolve(babelTransformerPath),
require.resolve(minifierPath),
require.resolve('../ModuleGraph/worker/JsFileWrapping'),
require.resolve('../assetTransformer'),
require.resolve('../ModuleGraph/worker/collectDependencies'),
require.resolve('./worker/constant-folding-plugin'),
require.resolve('../lib/getMinifier'),
require.resolve('./worker/inline-plugin'),
require.resolve('./worker/import-export-plugin'),
require.resolve('./worker/normalizePseudoglobals'),
require.resolve('../ModuleGraph/worker/optimizeDependencies'),
require.resolve('../ModuleGraph/worker/generateImportNames'),
]);
const babelTransformer = require(babelTransformerPath);
const babelTransformerKey = babelTransformer.getCacheKey
? babelTransformer.getCacheKey()
: '';
return [
filesKey,
stableHash(config).toString('hex'),
babelTransformerKey,
].join('$');
}
}
class InvalidRequireCallError extends Error {
innerError: collectDependencies.InvalidRequireCallError;
filename: string;
constructor(
innerError: collectDependencies.InvalidRequireCallError,
filename: string,
) {
super(`${filename}:${innerError.message}`);
this.innerError = innerError;
this.filename = filename;
}
}
module.exports = JsTransformer;