UNPKG

sucrase

Version:

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

253 lines (252 loc) 8.75 kB
"use strict"; 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, " ")); }