flow-remove-types-no-whitespace
Version:
Removes Flow type annotations from JavaScript files with speed and simplicity.
202 lines (176 loc) • 5.71 kB
JavaScript
var babylon = require('babylon');
var MagicString = require('magic-string');
/**
* Given a string JavaScript source which contains Flow types, return a string
* which has removed those types.
*
* Options:
*
* - checkPragma: (default: true) looks for an @flow pragma before parsing.
*/
module.exports = function flowRemoveTypes(source, options) {
options = options || {};
var s = new MagicString(source);
// If there's no @flow or @noflow flag, then expect no annotation.
var pragmaStart = source.indexOf('@flow');
var pragmaEnd = pragmaStart + 5;
if (pragmaStart === -1) {
pragmaStart = source.indexOf('@noflow');
pragmaEnd = pragmaStart + 7;
if (pragmaStart === -1 && options.checkPragma !== false) {
return { code: source, map: s.generateMap({ hires: true }) };
}
}
var removedNodes = pragmaStart === -1 ?
[] :
[ { start: pragmaStart, end: pragmaEnd } ];
// Babylon is one of the sources of truth for Flow syntax. This parse
// configuration is intended to be as permissive as possible.
var ast = babylon.parse(source, {
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
sourceType: 'module',
plugins: [ '*', 'jsx', 'flow' ],
});
visit(ast, removedNodes, removeFlowVisitor);
if (removedNodes.length === 0) {
return { code: source, map: s.generateMap({ hires: true }) };
}
// Step through the removed nodes, building up the resulting string.
for (var i = 0; i < removedNodes.length; i++) {
var node = removedNodes[i];
s.overwrite(node.start, node.end, '');
}
var result = { code: s.toString() };
if (options.sourceMap !== false) {
result.map = s.generateMap({ hires: true });
}
return result;
}
var LINE_RX = /(\r\n?|\n|\u2028|\u2029)/;
// A collection of methods for each AST type names which contain Flow types to
// be removed.
var removeFlowVisitor = {
DeclareClass: removeNode,
DeclareFunction: removeNode,
DeclareInterface:removeNode,
DeclareModule: removeNode,
DeclareTypeAlias: removeNode,
DeclareVariable: removeNode,
InterfaceDeclaration: removeNode,
TypeAlias: removeNode,
TypeAnnotation: removeNode,
TypeParameterDeclaration: removeNode,
TypeParameterInstantiation: removeNode,
ClassDeclaration: removeImplementedInterfaces,
ClassExpression: removeImplementedInterfaces,
Identifier: function (removedNodes, node, ast) {
if (node.optional) {
// Find the optional token.
var idx = findTokenIndex(ast.tokens, node.start);
do {
idx++;
} while (ast.tokens[idx].type.label !== '?');
removeNode(removedNodes, ast.tokens[idx]);
}
},
ClassProperty: function (removedNodes, node) {
if (!node.value) {
return removeNode(removedNodes, node)
}
},
ExportNamedDeclaration: function (removedNodes, node) {
if (node.exportKind === 'type') {
return removeNode(removedNodes, node);
}
},
ImportDeclaration: function (removedNodes, node) {
if (node.importKind === 'type') {
return removeNode(removedNodes, node);
}
}
};
// If this class declaration or expression implements interfaces, remove
// the associated tokens.
function removeImplementedInterfaces(removedNodes, node, ast) {
if (node.implements && node.implements.length > 0) {
var first = node.implements[0];
var last = node.implements[node.implements.length - 1];
var idx = findTokenIndex(ast.tokens, first.start);
do {
idx--;
} while (ast.tokens[idx].value !== 'implements');
var lastIdx = findTokenIndex(ast.tokens, last.start);
do {
if (ast.tokens[idx].type !== 'CommentBlock' &&
ast.tokens[idx].type !== 'CommentLine') {
removeNode(removedNodes, ast.tokens[idx]);
}
} while (idx++ !== lastIdx);
}
}
// Append node to the list of removed nodes, ensuring the order of the nodes
// in the list.
function removeNode(removedNodes, node) {
var length = removedNodes.length;
var index = length;
while (index > 0 && removedNodes[index - 1].end > node.start) {
index--;
}
if (index === length) {
removedNodes.push(node);
} else {
removedNodes.splice(index, 0, node);
}
return false;
}
// Given the AST output of babylon parse, walk through in a depth-first order,
// calling methods on the given visitor, providing context as the first argument.
function visit(ast, context, visitor) {
var stack;
var parent;
var keys = [];
var index = -1;
do {
index++;
if (stack && index === keys.length) {
parent = stack.parent;
keys = stack.keys;
index = stack.index;
stack = stack.prev;
} else {
var node = parent ? parent[keys[index]] : ast.program;
if (node && typeof node === 'object' && (node.type || node.length && node[0].type)) {
if (node.type) {
var visitFn = visitor[node.type];
if (visitFn && visitFn(context, node, ast) === false) {
continue;
}
}
stack = { parent: parent, keys: keys, index: index, prev: stack };
parent = node;
keys = Object.keys(node);
index = -1;
}
}
} while (stack);
}
// Given an array of sorted tokens, find the index of the token which contains
// the given offset. Uses binary search for O(log N) performance.
function findTokenIndex(tokens, offset) {
var min = 0;
var max = tokens.length - 1;
while (min <= max) {
var ptr = (min + max) / 2 | 0;
var token = tokens[ptr];
if (token.end <= offset) {
min = ptr + 1;
} else if (token.start > offset) {
max = ptr - 1;
} else {
return ptr;
}
}
return ptr;
}