sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
253 lines (252 loc) • 8.75 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
var babylon_1 = require("babylon");
var DEFAULT_PLUGINS = ['jsx', 'objectRestSpread'];
function transform(code, options) {
if (options === void 0) { options = {}; }
var resultCode = '';
var babylonPlugins = options.babylonPlugins || DEFAULT_PLUGINS;
var transforms = options.transforms || ['jsx'];
if (!transforms.includes('jsx')) {
return code;
}
if (transforms.includes('imports')) {
throw new Error('Import transform is not supported yet.');
}
var ast = babylon_1.parse(code, { tokens: true, sourceType: 'module', plugins: babylonPlugins });
var tokens = ast.tokens;
tokens = tokens.filter(function (token) {
return token.type !== 'CommentLine' && token.type !== 'CommentBlock';
});
var tokenIndex = 0;
function matchesAtIndex(index, tagLabels) {
for (var i = 0; i < tagLabels.length; i++) {
if (index + i >= tokens.length) {
return false;
}
if (tokens[index + i].type.label !== tagLabels[i]) {
return false;
}
}
return true;
}
function matches(tagLabels) {
return matchesAtIndex(tokenIndex, tagLabels);
}
/**
* Produce the props arg to createElement, starting at the first token of the
* props, if any.
*/
function processProps() {
if (!matches(['jsxName']) && !matches(['{'])) {
resultCode += ', null';
return;
}
resultCode += ', {';
while (true) {
if (matches(['jsxName', '='])) {
if (tokens[tokenIndex].value.includes('-')) {
replaceToken("'" + tokens[tokenIndex].value + "'");
}
else {
copyToken();
}
replaceToken(': ');
if (matches(['{'])) {
replaceToken('');
processBalancedCode();
replaceToken('');
}
else {
processStringPropValue();
}
}
else if (matches(['jsxName'])) {
copyToken();
resultCode += ': true';
}
else if (matches(['{'])) {
replaceToken('');
processBalancedCode();
replaceToken('');
}
else {
break;
}
resultCode += ',';
}
resultCode += '}';
}
function processStringPropValue() {
var value = tokens[tokenIndex].value;
var replacementCode = formatJSXTextReplacement(value);
var literalCode = formatJSXStringValueLiteral(value);
replaceToken(literalCode + replacementCode);
}
/**
* Process the first part of a tag, before any props.
*/
function processTagIntro() {
// Walk forward until we see one of these patterns:
// [jsxIdentifer, equals] to start the first prop.
// [open brace] to start the first prop.
// [jsxTagEnd] to end the open-tag.
// [slash, jsxTagEnd] to end the self-closing tag.
var introEnd = tokenIndex + 1;
while (!matchesAtIndex(introEnd, ['jsxName', '=']) &&
!matchesAtIndex(introEnd, ['{']) &&
!matchesAtIndex(introEnd, ['jsxTagEnd']) &&
!matchesAtIndex(introEnd, ['/', 'jsxTagEnd'])) {
introEnd++;
}
if (introEnd === tokenIndex + 1 && startsWithLowerCase(tokens[tokenIndex].value)) {
replaceToken("'" + tokens[tokenIndex].value + "'");
}
while (tokenIndex < introEnd) {
copyToken();
}
}
function processChildren() {
while (true) {
if (matches(['jsxTagStart', '/'])) {
// Closing tag, so no more children.
return;
}
if (matches(['{'])) {
if (matches(['{', '}'])) {
// Empty interpolations and comment-only interpolations are allowed
// and don't create an extra child arg.
replaceToken('');
replaceToken('');
}
else {
// Interpolated expression.
replaceToken(', ');
processBalancedCode();
replaceToken('');
}
}
else if (matches(['jsxTagStart'])) {
// Child JSX element
resultCode += ', ';
processJSXTag();
}
else if (matches(['jsxText'])) {
processChildTextElement();
}
else {
throw new Error('Unexpected token when processing JSX children.');
}
}
}
function processChildTextElement() {
var value = tokens[tokenIndex].value;
var replacementCode = formatJSXTextReplacement(value);
var literalCode = formatJSXTextLiteral(value);
if (literalCode === '""') {
replaceToken(replacementCode);
}
else {
replaceToken(', ' + literalCode + replacementCode);
}
}
function processJSXTag() {
// First tag is always jsxTagStart.
replaceToken('React.createElement(');
processTagIntro();
processProps();
if (matches(['/', 'jsxTagEnd'])) {
// Self-closing tag.
replaceToken('');
replaceToken(')');
}
else if (matches(['jsxTagEnd'])) {
replaceToken('');
// Tag with children.
processChildren();
while (!matches(['jsxTagEnd'])) {
replaceToken('');
}
replaceToken(')');
}
else {
throw new Error('Expected either /> or > at the end of the tag.');
}
}
function processBalancedCode() {
var braceDepth = 0;
while (tokenIndex < tokens.length) {
if (matches(['jsxTagStart'])) {
processJSXTag();
}
else {
if (matches(['{']) || matches(['${'])) {
braceDepth++;
}
else if (matches(['}'])) {
if (braceDepth === 0) {
return;
}
braceDepth--;
}
copyToken();
}
}
}
function replaceToken(newCode) {
resultCode += code.slice(tokenIndex > 0 ? tokens[tokenIndex - 1].end : 0, tokens[tokenIndex].start);
resultCode += newCode;
tokenIndex++;
}
function copyToken() {
resultCode += code.slice(tokenIndex > 0 ? tokens[tokenIndex - 1].end : 0, tokens[tokenIndex].end);
tokenIndex++;
}
processBalancedCode();
resultCode += code.slice(tokens[tokens.length - 1].end);
return resultCode;
}
exports.transform = transform;
function startsWithLowerCase(s) {
return s[0] == s[0].toLowerCase();
}
/**
* Turn the given jsxText string into a JS string literal.
*
* We use JSON.stringify to introduce escape characters as necessary, and trim
* the start and end of each line and remove blank lines.
*/
function formatJSXTextLiteral(text) {
// Introduce fake characters at the start and end to avoid trimming the start
// of the first line or the end of the last line.
var lines = ("!" + text + "!").split('\n');
// Trim spaces and tabs, but NOT non-breaking spaces.
lines = lines.map(function (line) { return line.replace(/^[ \t]*/, '').replace(/[ \t]*$/, ''); });
lines[0] = lines[0].slice(1);
lines[lines.length - 1] = lines[lines.length - 1].slice(0, -1);
lines = lines.filter(function (line) { return line; });
return JSON.stringify(lines.join(' '));
}
/**
* Produce the code that should be printed after the JSX text string literal,
* with most content removed, but all newlines preserved and all spacing at the
* end preserved.
*/
function formatJSXTextReplacement(text) {
var lines = text.split('\n');
lines = lines.map(function (line, i) {
return i < lines.length - 1
? ''
: Array.from(line).filter(function (char) { return char === ' '; }).join('');
});
return lines.join('\n');
}
/**
* Format a string in the value position of a JSX prop.
*
* Use the same implementation as convertAttribute from
* babel-helper-builder-react-jsx.
*/
function formatJSXStringValueLiteral(text) {
return JSON.stringify(text.replace(/\n\s+/g, " "));
}