432 lines
13 KiB
JavaScript
432 lines
13 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|