Skip to content

Commit

Permalink
feat: helper-module-imports adds specifiers to existing imports
Browse files Browse the repository at this point in the history
  • Loading branch information
conartist6 committed Apr 24, 2022
1 parent bdc4eb5 commit a6a6afe
Show file tree
Hide file tree
Showing 3 changed files with 482 additions and 51 deletions.
106 changes: 75 additions & 31 deletions packages/babel-helper-module-imports/src/import-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
cloneNode,
expressionStatement,
identifier,
isImportDeclaration,
isImportSpecifier,
isImportDefaultSpecifier,
isImportNamespaceSpecifier,
importDeclaration,
importDefaultSpecifier,
importNamespaceSpecifier,
Expand All @@ -19,17 +23,20 @@ import {
* output a new require/import statement list.
*/
export default class ImportBuilder {
_body = null;
_statements = [];
_statement = null;
_resultName = null;

_scope = null;
_hub = null;
private _importedSource: any;

constructor(importedSource, scope, hub) {
constructor(importedSource, { scope, hub, body }) {
this._importedSource = importedSource;
this._scope = scope;
this._hub = hub;
this._importedSource = importedSource;
this._body = body;
}

done() {
Expand All @@ -40,78 +47,115 @@ export default class ImportBuilder {
}

import() {
this._statements.push(
importDeclaration([], stringLiteral(this._importedSource)),
const source = this._importedSource;
const existingStatement = this._body.find(
decl => isImportDeclaration(decl) && decl.source?.value === source,
);

if (existingStatement) {
this._statement = existingStatement;
} else {
this._statement = importDeclaration([], stringLiteral(source));
this._statements.push(this._statement);
}
return this;
}

require() {
this._statements.push(
expressionStatement(
callExpression(identifier("require"), [
stringLiteral(this._importedSource),
]),
),
this._statement = expressionStatement(
callExpression(identifier("require"), [
stringLiteral(this._importedSource),
]),
);
this._statements.push(this._statement);
return this;
}

namespace(name = "namespace") {
const local = this._scope.generateUidIdentifier(name);
const statement = this._statement;
const { type, specifiers } = statement;

assert(type === "ImportDeclaration");

let local;
const existingSpecifier = specifiers.find(isImportNamespaceSpecifier);
if (existingSpecifier) {
({ local } = existingSpecifier);
} else {
local = this._scope.generateUidIdentifier(name);
specifiers.push(importNamespaceSpecifier(local));
}

const statement = this._statements[this._statements.length - 1];
assert(statement.type === "ImportDeclaration");
assert(statement.specifiers.length === 0);
statement.specifiers = [importNamespaceSpecifier(local)];
this._resultName = cloneNode(local);
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 = [importDefaultSpecifier(name)];
this._resultName = cloneNode(name);
const statement = this._statement;
const { type, specifiers } = statement;

assert(type === "ImportDeclaration");

let local;
const existingSpecifier = specifiers.find(isImportDefaultSpecifier);
if (existingSpecifier) {
({ local } = existingSpecifier);
} else {
local = this._scope.generateUidIdentifier(name);
specifiers.push(importDefaultSpecifier(local));
}

this._resultName = cloneNode(local);
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 = [importSpecifier(name, identifier(importName))];
this._resultName = cloneNode(name);
const statement = this._statement;
const { type, specifiers } = statement;

assert(type === "ImportDeclaration");

let local;
const existingSpecifier = specifiers.find(isImportSpecifier);
if (existingSpecifier) {
({ local } = existingSpecifier);
} else {
local = this._scope.generateUidIdentifier(name);
specifiers.push(importSpecifier(local, identifier(importName)));
}

this._resultName = cloneNode(local);
return this;
}

var(name) {
name = this._scope.generateUidIdentifier(name);
let statement = this._statements[this._statements.length - 1];
let statement = this._statement;
if (statement.type !== "ExpressionStatement") {
assert(this._resultName);
statement = expressionStatement(this._resultName);
this._statements.push(statement);
}
this._statements[this._statements.length - 1] = variableDeclaration("var", [
this._statement = variableDeclaration("var", [
variableDeclarator(name, statement.expression),
]);
this._statements[this._statements.length - 1] = this._statement;
this._resultName = cloneNode(name);
return this;
}

defaultInterop() {
return this._interop(this._hub.addHelper("interopRequireDefault"));
}

wildcardInterop() {
return this._interop(this._hub.addHelper("interopRequireWildcard"));
}

_interop(callee) {
const statement = this._statements[this._statements.length - 1];
const statement = this._statement;
if (statement.type === "ExpressionStatement") {
statement.expression = callExpression(callee, [statement.expression]);
} else if (statement.type === "VariableDeclaration") {
Expand All @@ -126,7 +170,7 @@ export default class ImportBuilder {
}

prop(name) {
const statement = this._statements[this._statements.length - 1];
const statement = this._statement;
if (statement.type === "ExpressionStatement") {
statement.expression = memberExpression(
statement.expression,
Expand Down
39 changes: 29 additions & 10 deletions packages/babel-helper-module-imports/src/import-injector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import assert from "assert";
import { numericLiteral, sequenceExpression } from "@babel/types";
import {
numericLiteral,
sequenceExpression,
isImportDeclaration,
} from "@babel/types";
import type * as t from "@babel/types";
import type { NodePath, Scope, HubInterface } from "@babel/traverse";

Expand Down Expand Up @@ -228,11 +232,11 @@ export default class ImportInjector {
throw new Error(`"importPosition": "after" is only supported in modules`);
}

const builder = new ImportBuilder(
importedSource,
this._programScope,
this._hub,
);
const builder = new ImportBuilder(importedSource, {
scope: this._programScope,
hub: this._hub,
body: this._programPath.node.body,
});

if (importedType === "es6") {
if (!isModuleForNode && !isModuleForBabel) {
Expand Down Expand Up @@ -425,6 +429,9 @@ export default class ImportInjector {
_insertStatements(statements, importPosition = "before", blockHoist = 3) {
const body = this._programPath.get("body");

const importDeclarations = statements.filter(n => isImportDeclaration(n));
const otherDeclarations = statements.filter(n => !isImportDeclaration(n));

if (importPosition === "after") {
for (let i = body.length - 1; i >= 0; i--) {
if (body[i].isImportDeclaration()) {
Expand All @@ -437,18 +444,30 @@ export default class ImportInjector {
node._blockHoist = blockHoist;
});

const targetPath = body.find(p => {
const topPathIdx = body.findIndex(p => {
// @ts-expect-error todo(flow->ts): avoid mutations
const val = p.node._blockHoist;
return Number.isFinite(val) && val < 4;
});

if (targetPath) {
targetPath.insertBefore(statements);
if (topPathIdx >= 0) {
body[topPathIdx].insertBefore(importDeclarations);

const topBelowImportsIdx = body.findIndex((p, i) => {
return i >= topPathIdx && !p.isImportDeclaration();
});

if (topBelowImportsIdx >= 0) {
body[topBelowImportsIdx].insertBefore(otherDeclarations);
} else {
this._programPath.pushContainer("body", otherDeclarations);
}

return;
}
}

this._programPath.unshiftContainer("body", statements);
this._programPath.unshiftContainer("body", otherDeclarations);
this._programPath.unshiftContainer("body", importDeclarations);
}
}
Loading

0 comments on commit a6a6afe

Please sign in to comment.