import assert from "assert"; import * as t from "@babel/types"; import ImportBuilder from "./import-builder"; import isModule from "./is-module"; export type ImportOptions = { /** * The module being referenced. */ importedSource: string | null, /** * The type of module being imported: * * * 'es6' - An ES6 module. * * 'commonjs' - A CommonJS module. (Default) */ importedType: "es6" | "commonjs", /** * The type of interop behavior for namespace/default/named when loading * CommonJS modules. * * ## 'babel' (Default) * * Load using Babel's interop. * * If '.__esModule' is true, treat as 'compiled', else: * * * Namespace: A copy of the module.exports with .default * populated by the module.exports object. * * Default: The module.exports value. * * Named: The .named property of module.exports. * * The 'ensureLiveReference' has no effect on the liveness of these. * * ## 'compiled' * * Assume the module is ES6 compiled to CommonJS. Useful to avoid injecting * interop logic if you are confident that the module is a certain format. * * * Namespace: The root module.exports object. * * Default: The .default property of the namespace. * * Named: The .named property of the namespace. * * Will return erroneous results if the imported module is _not_ compiled * from ES6 with Babel. * * ## 'uncompiled' * * Assume the module is _not_ ES6 compiled to CommonJS. Used a simplified * access pattern that doesn't require additional function calls. * * Will return erroneous results if the imported module _is_ compiled * from ES6 with Babel. * * * Namespace: The module.exports object. * * Default: The module.exports object. * * Named: The .named property of module.exports. */ importedInterop: "babel" | "node" | "compiled" | "uncompiled", /** * The type of CommonJS interop included in the environment that will be * loading the output code. * * * 'babel' - CommonJS modules load with Babel's interop. (Default) * * 'node' - CommonJS modules load with Node's interop. * * See descriptions in 'importedInterop' for more details. */ importingInterop: "babel" | "node", /** * Define whether we explicitly care that the import be a live reference. * Only applies when importing default and named imports, not the namespace. * * * true - Force imported values to be live references. * * false - No particular requirements. Keeps the code simplest. (Default) */ ensureLiveReference: boolean, /** * Define if we explicitly care that the result not be a property reference. * * * true - Force calls to exclude context. Useful if the value is going to * be used as function callee. * * false - No particular requirements for context of the access. (Default) */ ensureNoContext: boolean, }; /** * A general helper classes add imports via transforms. See README for usage. */ export default class ImportInjector { /** * The path used for manipulation. */ _programPath: NodePath; /** * The scope used to generate unique variable names. */ _programScope; /** * The file used to inject helpers and resolve paths. */ _hub; /** * The default options to use with this instance when imports are added. */ _defaultOpts: ImportOptions = { importedSource: null, importedType: "commonjs", importedInterop: "babel", importingInterop: "babel", ensureLiveReference: false, ensureNoContext: false, }; constructor(path, importedSource, opts) { const programPath = path.find(p => p.isProgram()); this._programPath = programPath; this._programScope = programPath.scope; this._hub = programPath.hub; this._defaultOpts = this._applyDefaults(importedSource, opts, true); } addDefault(importedSourceIn, opts) { return this.addNamed("default", importedSourceIn, opts); } addNamed(importName, importedSourceIn, opts) { assert(typeof importName === "string"); return this._generateImport( this._applyDefaults(importedSourceIn, opts), importName, ); } addNamespace(importedSourceIn, opts) { return this._generateImport( this._applyDefaults(importedSourceIn, opts), null, ); } addSideEffect(importedSourceIn, opts) { return this._generateImport( this._applyDefaults(importedSourceIn, opts), false, ); } _applyDefaults(importedSource, opts, isInit = false) { const optsList = []; if (typeof importedSource === "string") { optsList.push({ importedSource }); optsList.push(opts); } else { assert(!opts, "Unexpected secondary arguments."); optsList.push(importedSource); } const newOpts = { ...this._defaultOpts, }; for (const opts of optsList) { if (!opts) continue; Object.keys(newOpts).forEach(key => { if (opts[key] !== undefined) newOpts[key] = opts[key]; }); if (!isInit) { if (opts.nameHint !== undefined) newOpts.nameHint = opts.nameHint; if (opts.blockHoist !== undefined) newOpts.blockHoist = opts.blockHoist; } } return newOpts; } _generateImport(opts, importName) { const isDefault = importName === "default"; const isNamed = !!importName && !isDefault; const isNamespace = importName === null; const { importedSource, importedType, importedInterop, importingInterop, ensureLiveReference, ensureNoContext, nameHint, // Not meant for public usage. Allows code that absolutely must control // ordering to set a specific hoist value on the import nodes. blockHoist, } = opts; // Provide a hint for generateUidIdentifier for the local variable name // to use for the import, if the code will generate a simple assignment // to a variable. let name = nameHint || importName; const isMod = isModule(this._programPath); const isModuleForNode = isMod && importingInterop === "node"; const isModuleForBabel = isMod && importingInterop === "babel"; const builder = new ImportBuilder( importedSource, this._programScope, this._hub, ); if (importedType === "es6") { if (!isModuleForNode && !isModuleForBabel) { throw new Error("Cannot import an ES6 module from CommonJS"); } // import * as namespace from ''; namespace // import def from ''; def // import { named } from ''; named builder.import(); if (isNamespace) { builder.namespace(nameHint || importedSource); } else if (isDefault || isNamed) { builder.named(name, importName); } } else if (importedType !== "commonjs") { throw new Error(`Unexpected interopType "${importedType}"`); } else if (importedInterop === "babel") { if (isModuleForNode) { // import _tmp from ''; var namespace = interopRequireWildcard(_tmp); namespace // import _tmp from ''; var def = interopRequireDefault(_tmp).default; def // import _tmp from ''; _tmp.named name = name !== "default" ? name : importedSource; const es6Default = `${importedSource}$es6Default`; builder.import(); if (isNamespace) { builder .default(es6Default) .var(name || importedSource) .wildcardInterop(); } else if (isDefault) { if (ensureLiveReference) { builder .default(es6Default) .var(name || importedSource) .defaultInterop() .read("default"); } else { builder .default(es6Default) .var(name) .defaultInterop() .prop(importName); } } else if (isNamed) { builder.default(es6Default).read(importName); } } else if (isModuleForBabel) { // import * as namespace from ''; namespace // import def from ''; def // import { named } from ''; named builder.import(); if (isNamespace) { builder.namespace(name || importedSource); } else if (isDefault || isNamed) { builder.named(name, importName); } } else { // var namespace = interopRequireWildcard(require('')); // var def = interopRequireDefault(require('')).default; def // var named = require('').named; named builder.require(); if (isNamespace) { builder.var(name || importedSource).wildcardInterop(); } else if ((isDefault || isNamed) && ensureLiveReference) { if (isDefault) { name = name !== "default" ? name : importedSource; builder.var(name).read(importName); builder.defaultInterop(); } else { builder.var(importedSource).read(importName); } } else if (isDefault) { builder .var(name) .defaultInterop() .prop(importName); } else if (isNamed) { builder.var(name).prop(importName); } } } else if (importedInterop === "compiled") { if (isModuleForNode) { // import namespace from ''; namespace // import namespace from ''; namespace.default // import namespace from ''; namespace.named builder.import(); if (isNamespace) { builder.default(name || importedSource); } else if (isDefault || isNamed) { builder.default(importedSource).read(name); } } else if (isModuleForBabel) { // import * as namespace from ''; namespace // import def from ''; def // import { named } from ''; named // Note: These lookups will break if the module has no __esModule set, // hence the warning that 'compiled' will not work on standard CommonJS. builder.import(); if (isNamespace) { builder.namespace(name || importedSource); } else if (isDefault || isNamed) { builder.named(name, importName); } } else { // var namespace = require(''); namespace // var namespace = require(''); namespace.default // var namespace = require(''); namespace.named // var named = require('').named; builder.require(); if (isNamespace) { builder.var(name || importedSource); } else if (isDefault || isNamed) { if (ensureLiveReference) { builder.var(importedSource).read(name); } else { builder.prop(importName).var(name); } } } } else if (importedInterop === "uncompiled") { if (isDefault && ensureLiveReference) { throw new Error("No live reference for commonjs default"); } if (isModuleForNode) { // import namespace from ''; namespace // import def from ''; def; // import namespace from ''; namespace.named builder.import(); if (isNamespace) { builder.default(name || importedSource); } else if (isDefault) { builder.default(name); } else if (isNamed) { builder.default(importedSource).read(name); } } else if (isModuleForBabel) { // import namespace from ''; // import def from ''; // import { named } from ''; named; // Note: These lookups will break if the module has __esModule set, // hence the warning that 'uncompiled' will not work on ES6 transpiled // to CommonJS. builder.import(); if (isNamespace) { builder.default(name || importedSource); } else if (isDefault) { builder.default(name); } else if (isNamed) { builder.named(name, importName); } } else { // var namespace = require(''); namespace // var def = require(''); def // var namespace = require(''); namespace.named // var named = require('').named; builder.require(); if (isNamespace) { builder.var(name || importedSource); } else if (isDefault) { builder.var(name); } else if (isNamed) { if (ensureLiveReference) { builder.var(importedSource).read(name); } else { builder.var(name).prop(importName); } } } } else { throw new Error(`Unknown importedInterop "${importedInterop}".`); } const { statements, resultName } = builder.done(); this._insertStatements(statements, blockHoist); if ( (isDefault || isNamed) && ensureNoContext && resultName.type !== "Identifier" ) { return t.sequenceExpression([t.numericLiteral(0), resultName]); } return resultName; } _insertStatements(statements, blockHoist = 3) { statements.forEach(node => { node._blockHoist = blockHoist; }); const targetPath = this._programPath.get("body").find(p => { const val = p.node._blockHoist; return Number.isFinite(val) && val < 4; }); if (targetPath) { targetPath.insertBefore(statements); } else { this._programPath.unshiftContainer("body", statements); } } }