sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
380 lines (379 loc) • 15.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tokenizer_1 = require("../sucrase-babylon/tokenizer");
/**
* Class responsible for preprocessing and bookkeeping import and export declarations within the
* file.
*
* TypeScript uses a simpler mechanism that does not use functions like interopRequireDefault and
* interopRequireWildcard, so we also allow that mode for compatibility.
*/
class ImportProcessor {
constructor(nameManager, tokens, isTypeScript) {
this.nameManager = nameManager;
this.tokens = tokens;
this.isTypeScript = isTypeScript;
this.importInfoByPath = new Map();
this.importsToReplace = new Map();
this.identifierReplacements = new Map();
this.exportBindingsByLocalName = new Map();
}
getPrefixCode() {
if (this.isTypeScript) {
return "";
}
let prefix = "";
prefix += `
function ${this.interopRequireWildcardName}(obj) {
if (obj && obj.__esModule) {
return obj;
} else {
var newObj = {};
if (obj != null) {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key))
newObj[key] = obj[key];
}
}
newObj.default = obj;
return newObj;
}
}`.replace(/\s+/g, " ");
prefix += `
function ${this.interopRequireDefaultName}(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}`.replace(/\s+/g, " ");
return prefix;
}
preprocessTokens() {
if (!this.isTypeScript) {
this.interopRequireWildcardName = this.nameManager.claimFreeName("_interopRequireWildcard");
this.interopRequireDefaultName = this.nameManager.claimFreeName("_interopRequireDefault");
}
for (let i = 0; i < this.tokens.tokens.length; i++) {
if (this.tokens.matchesAtIndex(i, ["import"]) &&
!this.tokens.matchesAtIndex(i, ["import", "name", "="])) {
this.preprocessImportAtIndex(i);
}
if (this.tokens.matchesAtIndex(i, ["export"]) &&
!this.tokens.matchesAtIndex(i, ["export", "="])) {
this.preprocessExportAtIndex(i);
}
}
this.generateImportReplacements();
}
/**
* In TypeScript, import statements that only import types should be removed. This does not count
* bare imports.
*/
pruneTypeOnlyImports() {
const nonTypeIdentifiers = new Set();
for (const token of this.tokens.tokens) {
if (token.type.label === "name" &&
!token.isType &&
(token.identifierRole === tokenizer_1.IdentifierRole.Access ||
token.identifierRole === tokenizer_1.IdentifierRole.ObjectShorthand ||
token.identifierRole === tokenizer_1.IdentifierRole.ExportAccess)) {
nonTypeIdentifiers.add(token.value);
}
}
for (const [path, importInfo] of this.importInfoByPath.entries()) {
if (importInfo.hasBareImport ||
importInfo.hasStarExport ||
importInfo.exportStarNames.length > 0 ||
importInfo.namedExports.length > 0) {
continue;
}
const names = [
...importInfo.defaultNames,
...importInfo.wildcardNames,
...importInfo.namedImports.map(({ localName }) => localName),
];
if (names.every((name) => !nonTypeIdentifiers.has(name))) {
this.importsToReplace.set(path, "");
}
}
}
generateImportReplacements() {
for (const [path, importInfo] of this.importInfoByPath.entries()) {
const { defaultNames, wildcardNames, namedImports, namedExports, exportStarNames, hasStarExport, } = importInfo;
if (defaultNames.length === 0 &&
wildcardNames.length === 0 &&
namedImports.length === 0 &&
namedExports.length === 0 &&
exportStarNames.length === 0 &&
!hasStarExport) {
// Import is never used, so don't even assign a name.
this.importsToReplace.set(path, `require('${path}');`);
continue;
}
const primaryImportName = this.getFreeIdentifierForPath(path);
let secondaryImportName;
if (this.isTypeScript) {
secondaryImportName = primaryImportName;
}
else {
secondaryImportName =
wildcardNames.length > 0 ? wildcardNames[0] : this.getFreeIdentifierForPath(path);
}
let requireCode = `var ${primaryImportName} = require('${path}');`;
if (wildcardNames.length > 0) {
for (const wildcardName of wildcardNames) {
const moduleExpr = this.isTypeScript
? primaryImportName
: `${this.interopRequireWildcardName}(${primaryImportName})`;
requireCode += ` var ${wildcardName} = ${moduleExpr};`;
}
}
else if (exportStarNames.length > 0 && secondaryImportName !== primaryImportName) {
requireCode += ` var ${secondaryImportName} = ${this.interopRequireWildcardName}(${primaryImportName});`;
}
else if (defaultNames.length > 0 && secondaryImportName !== primaryImportName) {
requireCode += ` var ${secondaryImportName} = ${this.interopRequireDefaultName}(${primaryImportName});`;
}
for (const { importedName, localName } of namedExports) {
requireCode += ` Object.defineProperty(exports, '${localName}', \
{enumerable: true, get: () => ${primaryImportName}.${importedName}});`;
}
for (const exportStarName of exportStarNames) {
requireCode += ` exports.${exportStarName} = ${secondaryImportName};`;
}
if (hasStarExport) {
// Note that TypeScript and Babel do this differently; TypeScript does a simple existence
// check in the exports object and does a plain assignment, whereas Babel uses
// defineProperty and builds an object of explicitly-exported names so that star exports can
// always take lower precedence. For now, we do the easier TypeScript thing.a
requireCode += ` Object.keys(${primaryImportName}).filter(key => \
key !== 'default' && key !== '__esModule').forEach(key => { \
if (exports.hasOwnProperty(key)) { return; } \
Object.defineProperty(exports, key, {enumerable: true, \
get: () => ${primaryImportName}[key]}); });`;
}
this.importsToReplace.set(path, requireCode);
for (const defaultName of defaultNames) {
this.identifierReplacements.set(defaultName, `${secondaryImportName}.default`);
}
for (const { importedName, localName } of namedImports) {
this.identifierReplacements.set(localName, `${primaryImportName}.${importedName}`);
}
}
}
getFreeIdentifierForPath(path) {
const components = path.split("/");
const lastComponent = components[components.length - 1];
const baseName = lastComponent.replace(/\W/g, "");
return this.nameManager.claimFreeName(`_${baseName}`);
}
preprocessImportAtIndex(index) {
const defaultNames = [];
const wildcardNames = [];
let namedImports = [];
index++;
if (
// Ideally "type" would be a token type label, but for now, it's just an identifier.
(this.tokens.matchesNameAtIndex(index, "type") ||
this.tokens.matchesAtIndex(index, ["typeof"])) &&
!this.tokens.matchesAtIndex(index + 1, [","]) &&
!this.tokens.matchesNameAtIndex(index + 1, "from")) {
// import type declaration, so no need to process anything.
return;
}
if (this.tokens.matchesAtIndex(index, ["("])) {
// Dynamic import, so nothing to do
return;
}
if (this.tokens.matchesAtIndex(index, ["name"])) {
defaultNames.push(this.tokens.tokens[index].value);
index++;
if (this.tokens.matchesAtIndex(index, [","])) {
index++;
}
}
if (this.tokens.matchesAtIndex(index, ["*"])) {
// * as
index += 2;
wildcardNames.push(this.tokens.tokens[index].value);
index++;
}
if (this.tokens.matchesAtIndex(index, ["{"])) {
index++;
({ newIndex: index, namedImports } = this.getNamedImports(index));
}
if (this.tokens.matchesNameAtIndex(index, "from")) {
index++;
}
if (!this.tokens.matchesAtIndex(index, ["string"])) {
throw new Error("Expected string token at the end of import statement.");
}
const path = this.tokens.tokens[index].value;
const importInfo = this.getImportInfo(path);
importInfo.defaultNames.push(...defaultNames);
importInfo.wildcardNames.push(...wildcardNames);
importInfo.namedImports.push(...namedImports);
if (defaultNames.length === 0 && wildcardNames.length === 0 && namedImports.length === 0) {
importInfo.hasBareImport = true;
}
}
preprocessExportAtIndex(index) {
if (this.tokens.matchesAtIndex(index, ["export", "var"]) ||
this.tokens.matchesAtIndex(index, ["export", "let"]) ||
this.tokens.matchesAtIndex(index, ["export", "const"]) ||
this.tokens.matchesAtIndex(index, ["export", "function"]) ||
this.tokens.matchesAtIndex(index, ["export", "class"])) {
const exportName = this.tokens.tokens[index + 2].value;
this.exportBindingsByLocalName.set(exportName, exportName);
}
else if (this.tokens.matchesAtIndex(index, ["export", "name", "function"])) {
const exportName = this.tokens.tokens[index + 3].value;
this.exportBindingsByLocalName.set(exportName, exportName);
}
else if (this.tokens.matchesAtIndex(index, ["export", "{"])) {
this.preprocessNamedExportAtIndex(index);
}
else if (this.tokens.matchesAtIndex(index, ["export", "*"])) {
this.preprocessExportStarAtIndex(index);
}
}
/**
* Walk this export statement just in case it's an export...from statement.
* If it is, combine it into the import info for that path. Otherwise, just
* bail out; it'll be handled later.
*/
preprocessNamedExportAtIndex(index) {
// export {
index += 2;
const { newIndex, namedImports } = this.getNamedImports(index);
index = newIndex;
if (this.tokens.matchesNameAtIndex(index, "from")) {
index++;
}
else {
// Reinterpret "a as b" to be local/exported rather than imported/local.
for (const { importedName: localName, localName: exportedName } of namedImports) {
this.exportBindingsByLocalName.set(localName, exportedName);
}
return;
}
if (!this.tokens.matchesAtIndex(index, ["string"])) {
throw new Error("Expected string token at the end of import statement.");
}
const path = this.tokens.tokens[index].value;
const importInfo = this.getImportInfo(path);
importInfo.namedExports.push(...namedImports);
}
preprocessExportStarAtIndex(index) {
let exportedName = null;
if (this.tokens.matchesAtIndex(index, ["export", "*", "as"])) {
// export * as
index += 3;
exportedName = this.tokens.tokens[index].value;
// foo from
index += 2;
}
else {
// export * from
index += 3;
}
if (!this.tokens.matchesAtIndex(index, ["string"])) {
throw new Error("Expected string token at the end of star export statement.");
}
const path = this.tokens.tokens[index].value;
const importInfo = this.getImportInfo(path);
if (exportedName !== null) {
importInfo.exportStarNames.push(exportedName);
}
else {
importInfo.hasStarExport = true;
}
}
getNamedImports(index) {
const namedImports = [];
while (true) {
// Flow type imports should just be ignored.
let isTypeImport = false;
if ((this.tokens.matchesAtIndex(index, ["type"]) ||
this.tokens.matchesAtIndex(index, ["typeof"])) &&
this.tokens.matchesAtIndex(index + 1, ["name"]) &&
!this.tokens.matchesNameAtIndex(index + 1, "as")) {
isTypeImport = true;
index++;
}
const importedName = this.tokens.tokens[index].value;
let localName;
index++;
if (this.tokens.matchesNameAtIndex(index, "as")) {
index++;
localName = this.tokens.tokens[index].value;
index++;
}
else {
localName = importedName;
}
if (!isTypeImport) {
namedImports.push({ importedName, localName });
}
if (this.tokens.matchesAtIndex(index, [",", "}"])) {
index += 2;
break;
}
else if (this.tokens.matchesAtIndex(index, ["}"])) {
index++;
break;
}
else if (this.tokens.matchesAtIndex(index, [","])) {
index++;
}
else {
throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.currentToken())}`);
}
}
return { newIndex: index, namedImports };
}
/**
* Get a mutable import info object for this path, creating one if it doesn't
* exist yet.
*/
getImportInfo(path) {
const existingInfo = this.importInfoByPath.get(path);
if (existingInfo) {
return existingInfo;
}
const newInfo = {
defaultNames: [],
wildcardNames: [],
namedImports: [],
namedExports: [],
hasBareImport: false,
exportStarNames: [],
hasStarExport: false,
};
this.importInfoByPath.set(path, newInfo);
return newInfo;
}
/**
* Return the code to use for the import for this path, or the empty string if
* the code has already been "claimed" by a previous import.
*/
claimImportCode(importPath) {
const result = this.importsToReplace.get(importPath);
this.importsToReplace.set(importPath, "");
return result || "";
}
getIdentifierReplacement(identifierName) {
return this.identifierReplacements.get(identifierName) || null;
}
resolveExportBinding(assignedName) {
return this.exportBindingsByLocalName.get(assignedName) || null;
}
/**
* Return all imported/exported names where we might be interested in whether usages of those
* names are shadowed.
*/
getGlobalNames() {
return new Set([
...this.identifierReplacements.keys(),
...this.exportBindingsByLocalName.keys(),
]);
}
}
exports.default = ImportProcessor;