sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
371 lines (370 loc) • 14.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const isMaybePropertyName_1 = require("../util/isMaybePropertyName");
class ImportTransformer {
constructor(rootTransformer, tokens, nameManager, importProcessor, shouldAddModuleExports) {
this.rootTransformer = rootTransformer;
this.tokens = tokens;
this.nameManager = nameManager;
this.importProcessor = importProcessor;
this.shouldAddModuleExports = shouldAddModuleExports;
this.hadExport = false;
this.hadNamedExport = false;
this.hadDefaultExport = false;
}
preprocess() {
this.nameManager.preprocessNames(this.tokens.tokens);
this.importProcessor.preprocessTokens();
}
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']) &&
!isMaybePropertyName_1.default(this.tokens, this.tokens.currentIndex())) {
this.processImport();
return true;
}
if (this.tokens.matches(['export']) &&
!isMaybePropertyName_1.default(this.tokens, this.tokens.currentIndex())) {
this.hadExport = true;
this.processExport();
return true;
}
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() {
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();
}
}
processIdentifier() {
const token = this.tokens.currentToken();
const lastToken = this.tokens.tokens[this.tokens.currentIndex() - 1];
const nextToken = this.tokens.tokens[this.tokens.currentIndex() + 1];
// Skip identifiers that are part of property accesses.
if (lastToken && lastToken.type.label === '.') {
return false;
}
// For shorthand object keys, we need to expand them and replace only the value.
if (token.contextName === 'object' &&
lastToken && (lastToken.type.label === ',' || lastToken.type.label === '{') &&
nextToken && (nextToken.type.label === ',' || nextToken.type.label === '}')) {
return this.processObjectShorthand();
}
// For non-shorthand object keys, just ignore them.
if (token.contextName === 'object' &&
nextToken && nextToken.type.label === ':' && lastToken && (lastToken.type.label === ',' || lastToken.type.label === '{')) {
return false;
}
// Object methods identifiers can be identified similarly, and they also
// could have the async keyword before them.
if (token.contextName === 'object' &&
nextToken && nextToken.type.label === '(' && lastToken && (lastToken.type.label === ',' || lastToken.type.label === '{' ||
(lastToken.type.label === 'name' && lastToken.value === 'async'))) {
return false;
}
// Identifiers within class bodies must be method names.
if (token.contextName === 'class') {
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', 'default'])) {
this.processExportDefault();
this.hadDefaultExport = true;
return;
}
this.hadNamedExport = true;
if (this.tokens.matches(['export', 'var']) ||
this.tokens.matches(['export', 'let']) ||
this.tokens.matches(['export', 'const'])) {
this.processExportVar();
}
else if (this.tokens.matches(['export', 'function']) ||
this.tokens.matches(['export', 'name', 'function'])) {
this.processExportFunction();
}
else if (this.tokens.matches(['export', 'class'])) {
this.processExportClass();
}
else if (this.tokens.matches(['export', '{'])) {
this.processExportBindings();
}
else if (this.tokens.matches(['export', '*'])) {
this.processExportStar();
}
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']) ||
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.removeInitialToken();
this.tokens.removeToken();
const name = this.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();
this.tokens.copyExpectedToken('(');
this.rootTransformer.processBalancedCode();
this.tokens.copyExpectedToken(')');
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.replaceToken('');
const name = this.processNamedClass();
this.tokens.appendCode(` exports.${name} = ${name};`);
}
/**
* Skip past a class with a name and return that name.
*/
processNamedClass() {
this.tokens.copyExpectedToken('class');
if (!this.tokens.matches(['name'])) {
throw new Error('Expected identifier for exported class name.');
}
const name = this.tokens.currentToken().value;
this.tokens.copyToken();
if (this.tokens.matches(['extends'])) {
// There are only some limited expressions that are allowed within the
// `extends` expression, e.g. no top-level binary operators, so we can
// skip past even fairly complex expressions by being a bit careful.
this.tokens.copyToken();
if (this.tokens.matches(['{'])) {
// Extending an object literal.
this.tokens.copyExpectedToken('{');
this.rootTransformer.processBalancedCode();
this.tokens.copyExpectedToken('}');
}
else {
while (!this.tokens.matches(['{']) && !this.tokens.matches(['('])) {
this.rootTransformer.processToken();
}
if (this.tokens.matches(['('])) {
this.tokens.copyExpectedToken('(');
this.rootTransformer.processBalancedCode();
this.tokens.copyExpectedToken(')');
}
}
}
this.tokens.copyExpectedToken('{');
this.rootTransformer.processBalancedCode();
this.tokens.copyExpectedToken('}');
return 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;
}
exportStatements.push(`exports.${exportedName} = ${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');
}
}
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;