Implement a new utility module for injecting module imports.

This commit is contained in:
Logan Smyth 2017-09-15 00:20:51 -07:00
parent 84184e2ddd
commit ec9754bc40
11 changed files with 1768 additions and 26 deletions

View File

@ -10,6 +10,7 @@ export default {
* - 1 Default node position
* - 2 Priority over normal nodes
* - 3 We want this to be at the **very** top
* - 4 Reserved for the helpers used to implement module imports.
*/
name: "internal.blockHoist",

View File

@ -0,0 +1,3 @@
src
test
*.log

View File

@ -0,0 +1,27 @@
# babel-helper-module-imports
## Usage
### Adding a named impport
```
import { addNamed } from "babel-helper-module-imports";
export default function({ types: t }) {
return {
visitor: {
ReferencedIdentifier(path) {
let importName = this.importName;
if (importName) {
importName = t.cloneDeep(importName);
} else {
// require('bluebird').coroutine
importName = this.importName = addName(path, 'coroutine', 'bluebird');
}
path.replaceWith(importName);
}
},
};
}
```

View File

@ -0,0 +1,14 @@
{
"name": "babel-helper-module-imports",
"version": "7.0.0-beta.2",
"description": "Babel helper functions for inserting module loads",
"author": "Logan Smyth <loganfsmyth@gmail.com>",
"homepage": "https://babeljs.io/",
"license": "MIT",
"repository": "https://github.com/babel/babel/tree/master/packages/babel-helper-module-imports",
"main": "lib/index.js",
"dependencies": {
"babel-types": "7.0.0-beta.2",
"lodash": "^4.2.0"
}
}

View File

@ -0,0 +1,142 @@
import assert from "assert";
import * as t from "babel-types";
/**
* A class to track and accumulate mutations to the AST that will eventually
* output a new require/import statement list.
*/
export default class ImportBuilder {
_statements = [];
_resultName = null;
_scope = null;
_file = null;
constructor(importedSource, scope, file) {
this._scope = scope;
this._file = file;
this._importedSource = importedSource;
}
done() {
return {
statements: this._statements,
resultName: this._resultName,
};
}
import() {
const importedSource = this._file.resolveModuleSource(this._importedSource);
this._statements.push(
t.importDeclaration([], t.stringLiteral(importedSource)),
);
return this;
}
require() {
const importedSource = this._file.resolveModuleSource(this._importedSource);
this._statements.push(
t.expressionStatement(
t.callExpression(t.identifier("require"), [
t.stringLiteral(importedSource),
]),
),
);
return this;
}
namespace(name) {
name = this._scope.generateUidIdentifier(name);
const statement = this._statements[this._statements.length - 1];
assert(statement.type === "ImportDeclaration");
assert(statement.specifiers.length === 0);
statement.specifiers = [t.importNamespaceSpecifier(name)];
this._resultName = t.clone(name);
return this;
}
default(name) {
name = this._scope.generateUidIdentifier(name);
const statement = this._statements[this._statements.length - 1];
assert(statement.type === "ImportDeclaration");
assert(statement.specifiers.length === 0);
statement.specifiers = [t.importDefaultSpecifier(name)];
this._resultName = t.clone(name);
return this;
}
named(name, importName) {
if (importName === "default") return this.default(name);
name = this._scope.generateUidIdentifier(name);
const statement = this._statements[this._statements.length - 1];
assert(statement.type === "ImportDeclaration");
assert(statement.specifiers.length === 0);
statement.specifiers = [t.importSpecifier(name, t.identifier(importName))];
this._resultName = t.clone(name);
return this;
}
var(name) {
name = this._scope.generateUidIdentifier(name);
let statement = this._statements[this._statements.length - 1];
if (statement.type !== "ExpressionStatement") {
assert(this._resultName);
statement = t.expressionStatement(this._resultName);
this._statements.push(statement);
}
this._statements[
this._statements.length - 1
] = t.variableDeclaration("var", [
t.variableDeclarator(name, statement.expression),
]);
this._resultName = t.clone(name);
return this;
}
defaultInterop() {
return this._interop(this._file.addHelper("interopRequireDefault"));
}
wildcardInterop() {
return this._interop(this._file.addHelper("interopRequireWildcard"));
}
_interop(callee) {
const statement = this._statements[this._statements.length - 1];
if (statement.type === "ExpressionStatement") {
statement.expression = t.callExpression(callee, [statement.expression]);
} else if (statement.type === "VariableDeclaration") {
assert(statement.declarations.length === 1);
statement.declarations[0].init = t.callExpression(callee, [
statement.declarations[0].init,
]);
} else {
assert.fail("Unexpected type.");
}
return this;
}
prop(name) {
const statement = this._statements[this._statements.length - 1];
if (statement.type === "ExpressionStatement") {
statement.expression = t.memberExpression(
statement.expression,
t.identifier(name),
);
} else if (statement.type === "VariableDeclaration") {
assert(statement.declarations.length === 1);
statement.declarations[0].init = t.memberExpression(
statement.declarations[0].init,
t.identifier(name),
);
} else {
assert.fail("Unexpected type:" + statement.type);
}
return this;
}
read(name) {
this._resultName = t.memberExpression(this._resultName, t.identifier(name));
}
}

View File

@ -0,0 +1,425 @@
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.
*/
_file;
/**
* 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._file = programPath.hub.file;
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 = Object.assign({}, 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,
// 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.
nameHint = importName,
// Not meant for public usage. Allows code that absolutely must control
// ordering to set a specific hoist value on the import nodes.
blockHoist,
} = opts;
const isMod = isModule(this._programPath, true);
const isModuleForNode = isMod && importingInterop === "node";
const isModuleForBabel = isMod && importingInterop === "babel";
const builder = new ImportBuilder(
importedSource,
this._programScope,
this._file,
);
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("namespace");
} else if (isDefault || isNamed) {
builder.named(nameHint, 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
builder.import();
if (isNamespace) {
builder
.default("es6Default")
.var(nameHint || "namespace")
.wildcardInterop();
} else if (isDefault) {
if (ensureLiveReference) {
builder
.default("es6Default")
.var("namespace")
.defaultInterop()
.read("default");
} else {
builder
.default("es6Default")
.var(nameHint)
.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("namespace");
} else if (isDefault || isNamed) {
builder.named(nameHint, importName);
}
} else {
// var namespace = interopRequireWildcard(require(''));
// var def = interopRequireDefault(require('')).default; def
// var named = require('').named; named
builder.require();
if (isNamespace) {
builder.var("namespace").wildcardInterop();
} else if ((isDefault || isNamed) && ensureLiveReference) {
builder.var("namespace").read(importName);
if (isDefault) builder.defaultInterop();
} else if (isDefault) {
builder
.var(nameHint)
.defaultInterop()
.prop(importName);
} else if (isNamed) {
builder.var(nameHint).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("namespace");
} else if (isDefault || isNamed) {
builder.default("namespace").read(importName);
}
} 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("namespace");
} else if (isDefault || isNamed) {
builder.named(nameHint, 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("namespace");
} else if (isDefault || isNamed) {
if (ensureLiveReference) {
builder.var("namespace").read(importName);
} else {
builder.prop(importName).var(nameHint);
}
}
}
} 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("namespace");
} else if (isDefault) {
builder.default(nameHint);
} else if (isNamed) {
builder.default("namespace").read(importName);
}
} 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("namespace");
} else if (isDefault) {
builder.default(nameHint);
} else if (isNamed) {
builder.named(nameHint, 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("namespace");
} else if (isDefault) {
builder.var(nameHint);
} else if (isNamed) {
if (ensureLiveReference) {
builder.var("namespace").read(importName);
} else {
builder.var(nameHint).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").filter(p => {
const val = p.node._blockHoist;
return Number.isFinite(val) && val < 4;
})[0];
if (targetPath) {
targetPath.insertBefore(statements);
} else {
this._programPath.unshiftContainer("body", statements);
}
}
}

View File

@ -0,0 +1,21 @@
import ImportInjector from "./import-injector";
export { ImportInjector };
export { default as isModule } from "./is-module";
export function addDefault(path, importedSource, opts) {
return new ImportInjector(path).addDefault(importedSource, opts);
}
export function addNamed(path, name, importedSource, opts) {
return new ImportInjector(path).addNamed(name, importedSource, opts);
}
export function addNamespace(path, importedSource, opts) {
return new ImportInjector(path).addNamespace(importedSource, opts);
}
export function addSideEffect(path, importedSource, opts) {
return new ImportInjector(path).addSideEffect(importedSource, opts);
}

View File

@ -0,0 +1,31 @@
/**
* A small utility to check if a file qualifies as a module, based on a few
* possible conditions.
*/
export default function isModule(
path: NodePath,
requireUnambiguous: boolean = false,
) {
const { sourceType } = path.node;
if (sourceType !== "module" && sourceType !== "script") {
throw path.buildCodeFrameError(
`Unknown sourceType "${sourceType}", cannot transform.`,
);
}
const filename = path.hub.file.opts.filename;
if (/\.mjs$/.test(filename)) {
requireUnambiguous = false;
}
return (
path.node.sourceType === "module" &&
(!requireUnambiguous || isUnambiguousModule(path))
);
}
// This approach is not ideal. It is here to preserve compatibility for now,
// but really this should just return true or be deleted.
function isUnambiguousModule(path) {
return path.get("body").some(p => p.isModuleDeclaration());
}

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
"repository": "https://github.com/babel/babel/tree/master/packages/babel-helper-module-transforms",
"main": "lib/index.js",
"dependencies": {
"babel-helper-module-imports": "7.0.0-beta.2",
"babel-template": "7.0.0-beta.2",
"babel-types": "7.0.0-beta.2",
"lodash": "^4.2.0"

View File

@ -3,6 +3,8 @@ import * as t from "babel-types";
import template from "babel-template";
import chunk from "lodash/chunk";
import { isModule } from "babel-helper-module-imports";
import rewriteThis from "./rewrite-this";
import rewriteLiveReferences from "./rewrite-live-references";
import normalizeAndLoadModuleMetadata, {
@ -10,32 +12,7 @@ import normalizeAndLoadModuleMetadata, {
isSideEffectImport,
} from "./normalize-and-load-metadata";
export { hasExports, isSideEffectImport };
export function isModule(path: NodePath, requireUnambiguous: boolean = false) {
const { sourceType } = path.node;
if (sourceType !== "module" && sourceType !== "script") {
throw path.buildCodeFrameError(
`Unknown sourceType "${sourceType}", cannot transform.`,
);
}
const filename = path.hub.file.opts.filename;
if (/\.mjs$/.test(filename)) {
requireUnambiguous = false;
}
return (
path.node.sourceType === "module" &&
(!requireUnambiguous || isUnambiguousModule(path))
);
}
// This approach is not ideal. It is here to preserve compatibility for now,
// but really this should just return true or be deleted.
function isUnambiguousModule(path) {
return path.get("body").some(p => p.isModuleDeclaration());
}
export { hasExports, isSideEffectImport, isModule };
/**
* Perform all of the generic ES6 module rewriting needed to handle initial