import { basename, extname } from "path"; import splitExportDeclaration from "@babel/helper-split-export-declaration"; export type ModuleMetadata = { exportName: string, // The name of the variable that will reference an object containing export names. exportNameListName: null | string, hasExports: boolean, // Lookup from local binding to export information. local: Map, // Lookup of source file to source file metadata. source: Map, }; export type InteropType = "default" | "namespace" | "none"; export type SourceModuleMetadata = { // A unique variable name to use for this namespace object. Centralized for simplicity. name: string, loc: ?BabelNodeSourceLocation, interop: InteropType, // Local binding to reference from this source namespace. Key: Local name, value: Import name imports: Map, // Local names that reference namespace object. importsNamespace: Set, // Reexports to create for namespace. Key: Export name, value: Import name reexports: Map, // List of names to re-export namespace as. reexportNamespace: Set, // Tracks if the source should be re-exported. reexportAll: null | { loc: ?BabelNodeSourceLocation, }, }; export type LocalExportMetadata = { name: Array, // names of exports kind: "import" | "hoisted" | "block" | "var", }; /** * Check if the module has any exports that need handling. */ export function hasExports(metadata: ModuleMetadata) { return metadata.hasExports; } /** * Check if a given source is an anonymous import, e.g. "import 'foo';" */ export function isSideEffectImport(source: SourceModuleMetadata) { return ( source.imports.size === 0 && source.importsNamespace.size === 0 && source.reexports.size === 0 && source.reexportNamespace.size === 0 && !source.reexportAll ); } /** * Remove all imports and exports from the file, and return all metadata * needed to reconstruct the module's behavior. */ export default function normalizeModuleAndLoadMetadata( programPath: NodePath, exportName?: string, { noInterop = false, loose = false, lazy = false, esNamespaceOnly = false, } = {}, ): ModuleMetadata { if (!exportName) { exportName = programPath.scope.generateUidIdentifier("exports").name; } nameAnonymousExports(programPath); const { local, source, hasExports } = getModuleMetadata(programPath, { loose, lazy, }); removeModuleDeclarations(programPath); // Reuse the imported namespace name if there is one. for (const [, metadata] of source) { if (metadata.importsNamespace.size > 0) { // This is kind of gross. If we stop using `loose: true` we should // just make this destructuring assignment. metadata.name = metadata.importsNamespace.values().next().value; } if (noInterop) metadata.interop = "none"; else if (esNamespaceOnly) { // Both the default and namespace interops pass through __esModule // objects, but the namespace interop is used to enable Babel's // destructuring-like interop behavior for normal CommonJS. // Since some tooling has started to remove that behavior, we expose // it as the `esNamespace` option. if (metadata.interop === "namespace") { metadata.interop = "default"; } } } return { exportName, exportNameListName: null, hasExports, local, source, }; } /** * Get metadata about the imports and exports present in this module. */ function getModuleMetadata( programPath: NodePath, { loose, lazy }: { loose: boolean, lazy: boolean }, ) { const localData = getLocalExportMetadata(programPath, loose); const sourceData = new Map(); const getData = sourceNode => { const source = sourceNode.value; let data = sourceData.get(source); if (!data) { data = { name: programPath.scope.generateUidIdentifier( basename(source, extname(source)), ).name, interop: "none", loc: null, // Data about the requested sources and names. imports: new Map(), importsNamespace: new Set(), // Metadata about data that is passed directly from source to export. reexports: new Map(), reexportNamespace: new Set(), reexportAll: null, lazy: false, }; sourceData.set(source, data); } return data; }; let hasExports = false; programPath.get("body").forEach(child => { if (child.isImportDeclaration()) { const data = getData(child.node.source); if (!data.loc) data.loc = child.node.loc; child.get("specifiers").forEach(spec => { if (spec.isImportDefaultSpecifier()) { const localName = spec.get("local").node.name; data.imports.set(localName, "default"); const reexport = localData.get(localName); if (reexport) { localData.delete(localName); reexport.names.forEach(name => { data.reexports.set(name, "default"); }); } } else if (spec.isImportNamespaceSpecifier()) { const localName = spec.get("local").node.name; data.importsNamespace.add(localName); const reexport = localData.get(localName); if (reexport) { localData.delete(localName); reexport.names.forEach(name => { data.reexportNamespace.add(name); }); } } else if (spec.isImportSpecifier()) { const importName = spec.get("imported").node.name; const localName = spec.get("local").node.name; data.imports.set(localName, importName); const reexport = localData.get(localName); if (reexport) { localData.delete(localName); reexport.names.forEach(name => { data.reexports.set(name, importName); }); } } }); } else if (child.isExportAllDeclaration()) { hasExports = true; const data = getData(child.node.source); if (!data.loc) data.loc = child.node.loc; data.reexportAll = { loc: child.node.loc, }; } else if (child.isExportNamedDeclaration() && child.node.source) { hasExports = true; const data = getData(child.node.source); if (!data.loc) data.loc = child.node.loc; child.get("specifiers").forEach(spec => { if (!spec.isExportSpecifier()) { throw spec.buildCodeFrameError("Unexpected export specifier type"); } const importName = spec.get("local").node.name; const exportName = spec.get("exported").node.name; data.reexports.set(exportName, importName); if (exportName === "__esModule") { throw exportName.buildCodeFrameError('Illegal export "__esModule".'); } }); } else if ( child.isExportNamedDeclaration() || child.isExportDefaultDeclaration() ) { hasExports = true; } }); for (const metadata of sourceData.values()) { let needsDefault = false; let needsNamed = false; if (metadata.importsNamespace.size > 0) { needsDefault = true; needsNamed = true; } if (metadata.reexportAll) { needsNamed = true; } for (const importName of metadata.imports.values()) { if (importName === "default") needsDefault = true; else needsNamed = true; } for (const importName of metadata.reexports.values()) { if (importName === "default") needsDefault = true; else needsNamed = true; } if (needsDefault && needsNamed) { // TODO(logan): Using the namespace interop here is unfortunate. Revisit. metadata.interop = "namespace"; } else if (needsDefault) { metadata.interop = "default"; } } for (const [source, metadata] of sourceData) { if ( lazy !== false && !(isSideEffectImport(metadata) || metadata.reexportAll) ) { if (lazy === true) { // 'true' means that local relative files are eagerly loaded and // dependency modules are loaded lazily. metadata.lazy = !/\./.test(source); } else if (Array.isArray(lazy)) { metadata.lazy = lazy.indexOf(source) !== -1; } else if (typeof lazy === "function") { metadata.lazy = lazy(source); } else { throw new Error(`.lazy must be a boolean, string array, or function`); } } } return { hasExports, local: localData, source: sourceData, }; } /** * Get metadata about local variables that are exported. */ function getLocalExportMetadata( programPath: NodePath, loose: boolean, ): Map { const bindingKindLookup = new Map(); programPath.get("body").forEach(child => { let kind; if (child.isImportDeclaration()) { kind = "import"; } else { if (child.isExportDefaultDeclaration()) child = child.get("declaration"); if (child.isExportNamedDeclaration()) { if (child.node.declaration) { child = child.get("declaration"); } else if ( loose && child.node.source && child.get("source").isStringLiteral() ) { child.node.specifiers.forEach(specifier => { bindingKindLookup.set(specifier.local.name, "block"); }); return; } } if (child.isFunctionDeclaration()) { kind = "hoisted"; } else if (child.isClassDeclaration()) { kind = "block"; } else if (child.isVariableDeclaration({ kind: "var" })) { kind = "var"; } else if (child.isVariableDeclaration()) { kind = "block"; } else { return; } } Object.keys(child.getOuterBindingIdentifiers()).forEach(name => { bindingKindLookup.set(name, kind); }); }); const localMetadata = new Map(); const getLocalMetadata = idPath => { const localName = idPath.node.name; let metadata = localMetadata.get(localName); if (!metadata) { const kind = bindingKindLookup.get(localName); if (kind === undefined) { throw idPath.buildCodeFrameError( `Exporting local "${localName}", which is not declared.`, ); } metadata = { names: [], kind, }; localMetadata.set(localName, metadata); } return metadata; }; programPath.get("body").forEach(child => { if (child.isExportNamedDeclaration() && (loose || !child.node.source)) { if (child.node.declaration) { const declaration = child.get("declaration"); const ids = declaration.getOuterBindingIdentifierPaths(); Object.keys(ids).forEach(name => { if (name === "__esModule") { throw declaration.buildCodeFrameError( 'Illegal export "__esModule".', ); } getLocalMetadata(ids[name]).names.push(name); }); } else { child.get("specifiers").forEach(spec => { const local = spec.get("local"); const exported = spec.get("exported"); if (exported.node.name === "__esModule") { throw exported.buildCodeFrameError('Illegal export "__esModule".'); } getLocalMetadata(local).names.push(exported.node.name); }); } } else if (child.isExportDefaultDeclaration()) { const declaration = child.get("declaration"); if ( declaration.isFunctionDeclaration() || declaration.isClassDeclaration() ) { getLocalMetadata(declaration.get("id")).names.push("default"); } else { // These should have been removed by the nameAnonymousExports() call. throw declaration.buildCodeFrameError( "Unexpected default expression export.", ); } } }); return localMetadata; } /** * Ensure that all exported values have local binding names. */ function nameAnonymousExports(programPath: NodePath) { // Name anonymous exported locals. programPath.get("body").forEach(child => { if (!child.isExportDefaultDeclaration()) return; splitExportDeclaration(child); }); } function removeModuleDeclarations(programPath: NodePath) { programPath.get("body").forEach(child => { if (child.isImportDeclaration()) { child.remove(); } else if (child.isExportNamedDeclaration()) { if (child.node.declaration) { child.node.declaration._blockHoist = child.node._blockHoist; child.replaceWith(child.node.declaration); } else { child.remove(); } } else if (child.isExportDefaultDeclaration()) { // export default foo; const declaration = child.get("declaration"); if ( declaration.isFunctionDeclaration() || declaration.isClassDeclaration() ) { declaration._blockHoist = child.node._blockHoist; child.replaceWith(declaration); } else { // These should have been removed by the nameAnonymousExports() call. throw declaration.buildCodeFrameError( "Unexpected default expression export.", ); } } else if (child.isExportAllDeclaration()) { child.remove(); } }); }