UNPKG

sucrase

Version:

Super-fast alternative to Babel for when you can target modern JS runtimes

408 lines (407 loc) 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tokenizer_1 = require("../../sucrase-babylon/tokenizer"); const Transformer_1 = require("./Transformer"); class ImportTransformer extends Transformer_1.default { constructor(rootTransformer, tokens, importProcessor, shouldAddModuleExports) { super(); this.rootTransformer = rootTransformer; this.tokens = tokens; this.importProcessor = importProcessor; this.shouldAddModuleExports = shouldAddModuleExports; this.hadExport = false; this.hadNamedExport = false; this.hadDefaultExport = false; } getPrefixCode() { let prefix = '"use strict";'; prefix += this.importProcessor.getPrefixCode(); if (this.hadExport) { prefix += 'Object.defineProperty(exports, "__esModule", {value: true});'; } return prefix; } getSuffixCode() { if (this.shouldAddModuleExports && this.hadDefaultExport && !this.hadNamedExport) { return "\nmodule.exports = exports.default;\n"; } return ""; } process() { if (this.tokens.matches(["import", "name", "="])) { this.tokens.replaceToken("const"); return true; } if (this.tokens.matches(["import"])) { this.processImport(); return true; } if (this.tokens.matches(["export", "="])) { this.tokens.replaceToken("module.exports"); return true; } if (this.tokens.matches(["export"]) && !this.tokens.currentToken().isType) { this.hadExport = true; return this.processExport(); } if (this.tokens.matches(["name"]) || this.tokens.matches(["jsxName"])) { return this.processIdentifier(); } if (this.tokens.matches(["="])) { return this.processAssignment(); } return false; } /** * Transform this: * import foo, {bar} from 'baz'; * into * var _baz = require('baz'); var _baz2 = _interopRequireDefault(_baz); * * The import code was already generated in the import preprocessing step, so * we just need to look it up. */ processImport() { if (this.tokens.matches(["import", "("])) { this.tokens.replaceToken("Promise.resolve().then(() => require"); const contextId = this.tokens.currentToken().contextId; if (contextId == null) { throw new Error("Expected context ID on dynamic import invocation."); } this.tokens.copyToken(); while (!this.tokens.matchesContextIdAndLabel(")", contextId)) { this.rootTransformer.processToken(); } this.tokens.replaceToken("))"); return; } const wasOnlyTypes = this.removeImportAndDetectIfType(); if (wasOnlyTypes) { this.tokens.removeToken(); } else { const path = this.tokens.currentToken().value; this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path)); this.tokens.appendCode(this.importProcessor.claimImportCode(path)); } if (this.tokens.matches([";"])) { this.tokens.removeToken(); } } /** * Erase this import, and if it was either of the form "import type" or * contained only "type". Such imports should not even do a side-effect * import. * * The position should end at the import string. */ removeImportAndDetectIfType() { this.tokens.removeInitialToken(); if (this.tokens.matchesName("type") && !this.tokens.matchesAtIndex(this.tokens.currentIndex() + 1, [","]) && !this.tokens.matchesNameAtIndex(this.tokens.currentIndex() + 1, "from")) { // This is an "import type" statement, so exit early. this.removeRemainingImport(); return true; } if (this.tokens.matches(["name"]) || this.tokens.matches(["*"])) { // We have a default import or namespace import, so there must be some // non-type import. this.removeRemainingImport(); return false; } if (this.tokens.matches(["string"])) { // This is a bare import, so we should proceed with the import. return false; } let foundNonType = false; while (!this.tokens.matches(["string"])) { // Check if any named imports are of the form "foo" or "foo as bar", with // no leading "type". if ((!foundNonType && this.tokens.matches(["{"])) || this.tokens.matches([","])) { this.tokens.removeToken(); if (this.tokens.matches(["name", ","]) || this.tokens.matches(["name", "}"]) || this.tokens.matches(["name", "name", "name", ","]) || this.tokens.matches(["name", "name", "name", "}"])) { foundNonType = true; } } this.tokens.removeToken(); } return !foundNonType; } removeRemainingImport() { while (!this.tokens.matches(["string"])) { this.tokens.removeToken(); } } processIdentifier() { const token = this.tokens.currentToken(); if (token.shadowsGlobal) { return false; } if (token.identifierRole === tokenizer_1.IdentifierRole.ObjectShorthand) { return this.processObjectShorthand(); } if (token.identifierRole !== tokenizer_1.IdentifierRole.Access) { return false; } const replacement = this.importProcessor.getIdentifierReplacement(token.value); if (!replacement) { return false; } // For now, always use the (0, a) syntax so that non-expression replacements // are more likely to become syntax errors. this.tokens.replaceToken(`(0, ${replacement})`); return true; } processObjectShorthand() { const identifier = this.tokens.currentToken().value; const replacement = this.importProcessor.getIdentifierReplacement(identifier); if (!replacement) { return false; } this.tokens.replaceToken(`${identifier}: ${replacement}`); return true; } processExport() { if (this.tokens.matches(["export", "enum"]) || this.tokens.matches(["export", "const", "enum"])) { // Let the TypeScript transform handle it. return false; } if (this.tokens.matches(["export", "default"])) { this.processExportDefault(); this.hadDefaultExport = true; return true; } this.hadNamedExport = true; if (this.tokens.matches(["export", "var"]) || this.tokens.matches(["export", "let"]) || this.tokens.matches(["export", "const"])) { this.processExportVar(); return true; } else if (this.tokens.matches(["export", "function"]) || this.tokens.matches(["export", "name", "function"])) { this.processExportFunction(); return true; } else if (this.tokens.matches(["export", "class"]) || this.tokens.matches(["export", "abstract", "class"])) { this.processExportClass(); return true; } else if (this.tokens.matches(["export", "{"])) { this.processExportBindings(); return true; } else if (this.tokens.matches(["export", "*"])) { this.processExportStar(); return true; } else { throw new Error("Unrecognized export syntax."); } } processAssignment() { const index = this.tokens.currentIndex(); const identifierToken = this.tokens.tokens[index - 1]; if (identifierToken.type.label !== "name") { return false; } if (this.tokens.matchesAtIndex(index - 2, ["."])) { return false; } if (index - 2 >= 0 && ["var", "let", "const"].includes(this.tokens.tokens[index - 2].type.label)) { // Declarations don't need an extra assignment. This doesn't avoid the // assignment for comma-separated declarations, but it's still correct // since the assignment is just redundant. return false; } const exportedName = this.importProcessor.resolveExportBinding(identifierToken.value); if (!exportedName) { return false; } this.tokens.copyToken(); this.tokens.appendCode(` exports.${exportedName} =`); return true; } processExportDefault() { if (this.tokens.matches(["export", "default", "function", "name"]) || // export default aysnc function this.tokens.matches(["export", "default", "name", "function", "name"])) { this.tokens.removeInitialToken(); this.tokens.removeToken(); // Named function export case: change it to a top-level function // declaration followed by exports statement. const name = this.processNamedFunction(); this.tokens.appendCode(` exports.default = ${name};`); } else if (this.tokens.matches(["export", "default", "class", "name"]) || this.tokens.matches(["export", "default", "abstract", "class", "name"])) { this.tokens.removeInitialToken(); this.tokens.removeToken(); if (this.tokens.matches(["abstract"])) { this.tokens.removeToken(); } const name = this.rootTransformer.processNamedClass(); this.tokens.appendCode(` exports.default = ${name};`); } else { this.tokens.replaceToken("exports."); this.tokens.copyToken(); this.tokens.appendCode(" ="); } } /** * Transform this: * export const x = 1; * into this: * const x = exports.x = 1; */ processExportVar() { this.tokens.replaceToken(""); this.tokens.copyToken(); if (!this.tokens.matches(["name"])) { throw new Error("Expected a regular identifier after export var/let/const."); } const name = this.tokens.currentToken().value; this.tokens.copyToken(); this.tokens.appendCode(` = exports.${name}`); } /** * Transform this: * export function foo() {} * into this: * function foo() {} exports.foo = foo; */ processExportFunction() { this.tokens.replaceToken(""); const name = this.processNamedFunction(); this.tokens.appendCode(` exports.${name} = ${name};`); } /** * Skip past a function with a name and return that name. */ processNamedFunction() { if (this.tokens.matches(["function"])) { this.tokens.copyToken(); } else if (this.tokens.matches(["name", "function"])) { if (this.tokens.currentToken().value !== "async") { throw new Error("Expected async keyword in function export."); } this.tokens.copyToken(); this.tokens.copyToken(); } if (!this.tokens.matches(["name"])) { throw new Error("Expected identifier for exported function name."); } const name = this.tokens.currentToken().value; this.tokens.copyToken(); if (this.tokens.currentToken().isType) { this.tokens.removeInitialToken(); while (this.tokens.currentToken().isType) { this.tokens.removeToken(); } } this.tokens.copyExpectedToken("("); this.rootTransformer.processBalancedCode(); this.tokens.copyExpectedToken(")"); this.rootTransformer.processPossibleTypeRange(); this.tokens.copyExpectedToken("{"); this.rootTransformer.processBalancedCode(); this.tokens.copyExpectedToken("}"); return name; } /** * Transform this: * export class A {} * into this: * class A {} exports.A = A; */ processExportClass() { this.tokens.removeInitialToken(); if (this.tokens.matches(["abstract"])) { this.tokens.removeToken(); } const name = this.rootTransformer.processNamedClass(); this.tokens.appendCode(` exports.${name} = ${name};`); } /** * Transform this: * export {a, b as c}; * into this: * exports.a = a; exports.c = b; * * OR * * Transform this: * export {a, b as c} from './foo'; * into the pre-generated Object.defineProperty code from the ImportProcessor. */ processExportBindings() { this.tokens.removeInitialToken(); this.tokens.removeToken(); const exportStatements = []; while (true) { const localName = this.tokens.currentToken().value; let exportedName; this.tokens.removeToken(); if (this.tokens.matchesName("as")) { this.tokens.removeToken(); exportedName = this.tokens.currentToken().value; this.tokens.removeToken(); } else { exportedName = localName; } const newLocalName = this.importProcessor.getIdentifierReplacement(localName); exportStatements.push(`exports.${exportedName} = ${newLocalName || localName};`); if (this.tokens.matches(["}"])) { this.tokens.removeToken(); break; } if (this.tokens.matches([",", "}"])) { this.tokens.removeToken(); this.tokens.removeToken(); break; } else if (this.tokens.matches([","])) { this.tokens.removeToken(); } else { throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.currentToken())}`); } } if (this.tokens.matchesName("from")) { // This is an export...from, so throw away the normal named export code // and use the Object.defineProperty code from ImportProcessor. this.tokens.removeToken(); const path = this.tokens.currentToken().value; this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path)); } else { // This is a normal named export, so use that. this.tokens.appendCode(exportStatements.join(" ")); } if (this.tokens.matches([";"])) { this.tokens.removeToken(); } } processExportStar() { this.tokens.removeInitialToken(); while (!this.tokens.matches(["string"])) { this.tokens.removeToken(); } const path = this.tokens.currentToken().value; this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path)); if (this.tokens.matches([";"])) { this.tokens.removeToken(); } } } exports.default = ImportTransformer;