@expo/metro-config
Version:
A Metro config for running React Native projects with the Metro bundler
437 lines • 24.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.importExportLiveBindingsPlugin = importExportLiveBindingsPlugin;
const helpers_1 = require("./helpers");
var ImportDeclarationKind;
(function (ImportDeclarationKind) {
ImportDeclarationKind["REQUIRE"] = "REQUIRE";
ImportDeclarationKind["IMPORT_DEFAULT"] = "DEFAULT";
ImportDeclarationKind["IMPORT_NAMESPACE"] = "NAMESPACE";
})(ImportDeclarationKind || (ImportDeclarationKind = {}));
function importExportLiveBindingsPlugin({ template, types: t, }) {
const addModuleSpecifiers = (state, source) => {
let moduleSpecifiers = state.importSpecifiers.get(source.value);
if (!moduleSpecifiers) {
moduleSpecifiers = Object.create(null);
state.importSpecifiers.set(source.value, moduleSpecifiers);
}
return moduleSpecifiers;
};
const addImport = (path, state, source, sideEffect = false) => {
const moduleSpecifiers = addModuleSpecifiers(state, source);
if (sideEffect || !state.opts.performConstantFolding) {
moduleSpecifiers.sideEffect = true;
}
let id = moduleSpecifiers[ImportDeclarationKind.REQUIRE];
if (!id) {
id = path.scope.generateUid(source.value);
moduleSpecifiers[ImportDeclarationKind.REQUIRE] = id;
state.importDeclarations.push({
kind: ImportDeclarationKind.REQUIRE,
local: undefined,
source,
loc: path.node.loc,
});
}
return id;
};
const addDefaultImport = (path, state, source, name) => {
const moduleSpecifiers = addModuleSpecifiers(state, source);
let id = moduleSpecifiers[ImportDeclarationKind.IMPORT_DEFAULT];
if (!id) {
// Use the given name, if possible, or generate one. If no initial name is given,
// we'll create one based on the parent import
const parentImportLocal = addImport(path, state, source);
id =
!name || !t.isValidIdentifier(name)
? path.scope.generateUid(name ?? parentImportLocal)
: name;
moduleSpecifiers[ImportDeclarationKind.IMPORT_DEFAULT] = id;
state.importDeclarations.push({
kind: ImportDeclarationKind.IMPORT_DEFAULT,
local: parentImportLocal,
source,
loc: path.node.loc,
});
}
return id;
};
const addNamespaceImport = (path, state, source, name) => {
const moduleSpecifiers = addModuleSpecifiers(state, source);
let id = moduleSpecifiers[ImportDeclarationKind.IMPORT_NAMESPACE];
if (!id) {
// Use the given name, if possible, or generate one. If no initial name is given,
// we'll create one based on the parent import
const parentImportLocal = addImport(path, state, source);
id =
!name || !t.isValidIdentifier(name)
? path.scope.generateUid(name ?? parentImportLocal)
: name;
moduleSpecifiers[ImportDeclarationKind.IMPORT_NAMESPACE] = id;
state.importDeclarations.push({
kind: ImportDeclarationKind.IMPORT_NAMESPACE,
local: parentImportLocal,
source,
loc: path.node.loc,
});
}
return id;
};
return {
visitor: {
// (1): Scan imports and prepare require calls
ImportDeclaration(path, state) {
if (path.node.importKind && path.node.importKind !== 'value') {
path.remove();
return;
}
const source = path.node.source;
if (!path.node.specifiers.length) {
addImport(path, state, source, true);
path.remove();
return;
}
for (const specifier of path.node.specifiers) {
const localId = specifier.local.name;
let importId;
let member;
switch (specifier.type) {
case 'ImportNamespaceSpecifier':
// The `namespaceWrapHelper` ensures a namespace object, but namespaces are accessed directly
member = undefined;
importId = addNamespaceImport(path, state, source, localId);
break;
case 'ImportSpecifier':
if (specifier.importKind && specifier.importKind !== 'value') {
continue;
}
member = t.isIdentifier(specifier.imported)
? specifier.imported.name
: specifier.imported.value;
// An imported default specifier is the same as an ImportDefaultSpecifier
importId =
member === 'default'
? addDefaultImport(path, state, source, localId)
: addImport(path, state, source);
break;
case 'ImportDefaultSpecifier':
// The `defaultWrapHelper` works by wrapping CommonJS modules in a fake module wrapper
member = 'default';
importId = addDefaultImport(path, state, source, localId);
break;
}
state.inlineBodyRefs.set(localId, {
parentId: importId,
member,
});
}
path.remove();
},
// (2.1): Declare live exports for ExportAllDeclarations immediately (References the import)
ExportAllDeclaration(path, state) {
if (path.node.exportKind && path.node.exportKind !== 'value') {
path.remove();
return;
}
const loc = path.node.loc;
const source = path.node.source;
const importId = addImport(path, state, source);
if (!state.exportAll.has(importId)) {
state.referencedLocals.add(importId);
state.exportAll.set(importId, (0, helpers_1.withLocation)((0, helpers_1.liveExportAllHelper)(template, importId), loc));
}
path.remove();
},
// (2.2): Store ExportDefaultDeclaration for later, for processing after all imports are evaluated
ExportDefaultDeclaration(path, state) {
if (path.node.exportKind && path.node.exportKind !== 'value') {
path.remove();
return;
}
let localId;
// We purposefully don't check for `Identifier` or `MemberExpression` here
// `export default` values are assigne at the point they're declared. We don't want them to be mutated
if (t.isDeclaration(path.node.declaration)) {
if (!path.node.declaration.id) {
path.node.declaration.id = path.scope.generateUidIdentifierBasedOnNode(path.node.declaration);
}
localId = path.node.declaration.id.name;
path.replaceWith(path.node.declaration);
}
else {
localId = path.scope.generateUid('_default');
path.replaceWith((0, helpers_1.withLocation)((0, helpers_1.varDeclaratorHelper)(t, localId, path.node.declaration), path.node.loc));
}
state.exportDeclarations.push({
statement: (0, helpers_1.withLocation)((0, helpers_1.liveExportHelper)(t, 'default', t.identifier(localId)), path.node.loc),
local: undefined,
});
},
// (2.3): Store ExportNamedDeclaration for later (if it has a local declaration), for processing after all imports are evaluated
// - If we have a source, create live bindings immediately for specifiers (References the import)
ExportNamedDeclaration(path, state) {
if (path.node.exportKind && path.node.exportKind !== 'value') {
path.remove();
return;
}
else if (path.node.declaration || !path.node.source) {
state.exportStatements.push(path.node);
if (path.node.declaration) {
// If we have a declaration, we'll replace the export with it
// In (3.1), we can then refer to the declarations by their local ids
path.replaceWith(path.node.declaration);
path.skip();
if (!path.node.source) {
return;
}
}
else if (!path.node.source) {
path.remove();
return;
}
}
const source = path.node.source;
if (!path.node.specifiers.length) {
addImport(path, state, source, true);
path.remove();
return;
}
for (const specifier of path.node.specifiers) {
let importId;
let specifierLocal;
let exportExpression;
switch (specifier.type) {
case 'ExportNamespaceSpecifier':
// The `namespaceWrapHelper` ensures a namespace object, but namespaces are accessed directly
specifierLocal = undefined;
importId = addNamespaceImport(path, state, source);
exportExpression = t.identifier(importId);
break;
case 'ExportSpecifier':
if (specifier.exportKind && specifier.exportKind !== 'value') {
continue;
}
specifierLocal = specifier.local.name;
// An imported default specifier is the same as an ImportDefaultSpecifier
importId =
specifierLocal === 'default'
? addDefaultImport(path, state, source)
: addImport(path, state, source);
exportExpression = t.memberExpression(t.identifier(importId), t.identifier(specifierLocal));
break;
case 'ExportDefaultSpecifier':
// The `defaultWrapHelper` works by wrapping CommonJS modules in a fake module wrapper
specifierLocal = 'default';
importId = addDefaultImport(path, state, source);
exportExpression = t.memberExpression(t.identifier(importId), t.identifier(specifierLocal));
break;
}
const exportName = t.isIdentifier(specifier.exported)
? specifier.exported.name
: specifier.exported.value;
state.referencedLocals.add(importId);
state.exportDeclarations.push({
statement: (0, helpers_1.withLocation)((0, helpers_1.liveExportHelper)(t, exportName, exportExpression), path.node.loc),
local: importId,
});
}
path.remove();
},
Program: {
// (0): Initialize all state
enter(path, state) {
state.importSpecifiers = new Map();
state.inlineBodyRefs = new Map();
state.referencedLocals = new Set();
state.exportStatements = [];
state.exportDeclarations = [];
state.exportAll = new Map();
state.importDeclarations = [];
// Ensure the iife "globals" don't have conflicting variables in the module.
['global', 'require', 'module', 'exports'].forEach((name) => {
path.scope.rename(name, path.scope.generateUidIdentifier(name).name);
});
},
exit(path, state) {
function getInlineRefExpression(node, localId) {
const inlineRef = state.inlineBodyRefs.get(localId);
if (!inlineRef)
return undefined;
// Reference count the target ID to ensure its import will be added,
// then replace this ID with the InlineRef
state.referencedLocals.add(inlineRef.parentId);
node.name = inlineRef.parentId;
let refNode;
if (inlineRef.member == null) {
refNode = node;
}
else if (node.type !== 'JSXIdentifier') {
refNode = t.memberExpression(node, t.identifier(inlineRef.member));
}
else {
refNode = t.jsxMemberExpression(t.jsxIdentifier(inlineRef.parentId), t.jsxIdentifier(inlineRef.member));
}
return refNode;
}
// (3): Process all "deferred" export declarations in `state.exportDeclarations`
for (const exportStatement of state.exportStatements) {
// (3.1): Convert all local exports into export declarations, while making sure
// to reference imports if necessary
if (!exportStatement.source && exportStatement.specifiers) {
for (const specifier of exportStatement.specifiers) {
if (specifier.type !== 'ExportSpecifier') {
continue; // NOTE: This is not a legal AST type without `source`
}
else if (specifier.exportKind && specifier.exportKind !== 'value') {
continue;
}
const exportName = t.isIdentifier(specifier.exported)
? specifier.exported.name
: specifier.exported.value;
const exportExpression = getInlineRefExpression(specifier.local, specifier.local.name) ?? specifier.local;
state.exportDeclarations.push({
statement: (0, helpers_1.withLocation)((0, helpers_1.liveExportHelper)(t, exportName, exportExpression), exportStatement.loc),
local: undefined,
});
}
}
// (3.2): Process all locally exported declarations
const declaration = exportStatement.declaration;
if (declaration) {
// Live bindings are used for variables, since they can be reassigned and may not be declared until later on
const exportHelper = declaration.type === 'VariableDeclaration' ||
declaration.type !== 'FunctionDeclaration'
? helpers_1.liveExportHelper
: helpers_1.assignExportHelper;
const exportBindings = t.getBindingIdentifiers(declaration, false, true);
for (const exportName in exportBindings) {
if (exportBindings[exportName] != null) {
state.exportDeclarations.push({
statement: (0, helpers_1.withLocation)(exportHelper(t, exportName, t.identifier(exportBindings[exportName].name)), exportStatement.loc),
local: undefined,
});
}
}
}
}
// (4): Traverse reference identifiers and replace as needed with `state.inlineBodyRefs`'
// synthetic IDs, while marking the IDs that are referenced in `state.syntheticRefs`
path.traverse({
ReferencedIdentifier(path, state) {
if (path.parent.type === 'ExportSpecifier') {
return;
}
const localId = path.node.name;
// We skip this identifier if it's not a program binding, since
// that means it was declared in a child scope
const localBinding = path.scope.getBinding(localId);
const rootBinding = state.programScope.getBinding(localId);
if (rootBinding !== localBinding)
return;
// Replace the local ID with the inlined reference, if there is one
let inlineRefExpression = getInlineRefExpression(path.node, localId);
if (inlineRefExpression) {
// NOTE(@kitten): Ensure that calls after this member access aren't implicitly bound
// to the object they're called on
if (path.parent.type === 'CallExpression' &&
path.parent.callee === path.node &&
inlineRefExpression.type !== 'JSXMemberExpression') {
inlineRefExpression = (0, helpers_1.nullBoundExpression)(t, inlineRefExpression);
}
path.replaceWith(inlineRefExpression);
path.skip();
}
},
}, {
referencedLocals: state.referencedLocals,
inlineBodyRefs: state.inlineBodyRefs,
programScope: path.scope,
});
const preambleStatements = [];
const esmStatements = [];
let _defaultWrapName;
const wrapDefault = (localId, sourceId) => {
if (!_defaultWrapName) {
_defaultWrapName = '_interopDefault';
preambleStatements.push((0, helpers_1.defaultWrapHelper)(template, _defaultWrapName));
}
return (0, helpers_1.varDeclaratorCallHelper)(t, localId, _defaultWrapName, sourceId);
};
let _namespaceWrapName;
const wrapNamespace = (localId, sourceId) => {
if (!_namespaceWrapName) {
_namespaceWrapName = '_interopNamespace';
preambleStatements.push((0, helpers_1.namespaceWrapHelper)(template, _namespaceWrapName));
}
return (0, helpers_1.varDeclaratorCallHelper)(t, localId, _namespaceWrapName, sourceId);
};
// Add `__esModule` marker if we have any exports
if (state.exportDeclarations.length || state.exportAll.size) {
preambleStatements.push((0, helpers_1.esModuleExportTemplate)(template));
}
// (5): Add all exports, and all referenced imports
for (const exportDeclaration of state.exportDeclarations) {
esmStatements.push(exportDeclaration.statement);
if (exportDeclaration.local) {
state.referencedLocals.add(exportDeclaration.local);
}
}
// Reference locals that are referenced by import declarations
for (const importDeclaration of state.importDeclarations) {
// NOTE(@kitten): The first check removes default/namespace import wrappers when they're unused.
// This diverges from the previous implementation a lot, and is basically unused local elimination
// If we don't want this, this can safely be removed
const source = importDeclaration.source;
const local = addModuleSpecifiers(state, source)[importDeclaration.kind];
if (!local || !state.referencedLocals.has(local)) {
continue;
}
else if (importDeclaration.local) {
state.referencedLocals.add(importDeclaration.local);
}
}
// Insert imports, if they're referenced
for (const importDeclaration of state.importDeclarations) {
const source = importDeclaration.source;
const moduleSpecifiers = addModuleSpecifiers(state, source);
const local = moduleSpecifiers[importDeclaration.kind];
if (!local || !state.referencedLocals.has(local)) {
// Don't add imports that aren't referenced, unless they're required for a side-effect
// We check for REQUIRE, to make sure we only ever add a single side-effect require
if (importDeclaration.kind === ImportDeclarationKind.REQUIRE &&
moduleSpecifiers.sideEffect) {
esmStatements.push((0, helpers_1.withLocation)((0, helpers_1.sideEffectRequireCall)(t, source), importDeclaration.loc));
}
continue;
}
let importStatement;
switch (importDeclaration.kind) {
case ImportDeclarationKind.REQUIRE:
importStatement = (0, helpers_1.requireCall)(t, local, source);
break;
case ImportDeclarationKind.IMPORT_DEFAULT:
importStatement = wrapDefault(local, importDeclaration.local);
break;
case ImportDeclarationKind.IMPORT_NAMESPACE:
importStatement = wrapNamespace(local, importDeclaration.local);
break;
}
importStatement = (0, helpers_1.withLocation)(importStatement, importDeclaration.loc);
esmStatements.push(importStatement);
const exportAllStatement = state.exportAll.get(local);
if (exportAllStatement != null) {
esmStatements.push(exportAllStatement);
}
}
// WARN(@kitten): This isn't only dependent on exports! If we set this to `false` but
// added any imports, then those imports will accidentally be shifted back to CJS-mode
if (esmStatements.length && state.opts.out) {
state.opts.out.isESModule = true;
}
path.node.body = [...preambleStatements, ...esmStatements, ...path.node.body];
},
},
},
};
}
//# sourceMappingURL=importExportLiveBindings.js.map