"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _xhtml = require('../parser/plugins/jsx/xhtml'); var _xhtml2 = _interopRequireDefault(_xhtml); var _tokenizer = require('../parser/tokenizer'); var _types = require('../parser/tokenizer/types'); var _charcodes = require('../parser/util/charcodes'); var _getJSXPragmaInfo = require('../util/getJSXPragmaInfo'); var _getJSXPragmaInfo2 = _interopRequireDefault(_getJSXPragmaInfo); var _Transformer = require('./Transformer'); var _Transformer2 = _interopRequireDefault(_Transformer); class JSXTransformer extends _Transformer2.default { // State for calculating the line number of each JSX tag in development. __init() {this.lastLineNumber = 1} __init2() {this.lastIndex = 0} // In development, variable name holding the name of the current file. __init3() {this.filenameVarName = null} // Mapping of claimed names for imports in the automatic transform, e,g. // {jsx: "_jsx"}. This determines which imports to generate in the prefix. __init4() {this.esmAutomaticImportNameResolutions = {}} // When automatically adding imports in CJS mode, we store the variable name // holding the imported CJS module so we can require it in the prefix. __init5() {this.cjsAutomaticModuleNameResolutions = {}} constructor( rootTransformer, tokens, importProcessor, nameManager, options, ) { super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.importProcessor = importProcessor;this.nameManager = nameManager;this.options = options;JSXTransformer.prototype.__init.call(this);JSXTransformer.prototype.__init2.call(this);JSXTransformer.prototype.__init3.call(this);JSXTransformer.prototype.__init4.call(this);JSXTransformer.prototype.__init5.call(this);; this.jsxPragmaInfo = _getJSXPragmaInfo2.default.call(void 0, options); this.isAutomaticRuntime = options.jsxRuntime === "automatic"; this.jsxImportSource = options.jsxImportSource || "react"; } process() { if (this.tokens.matches1(_types.TokenType.jsxTagStart)) { this.processJSXTag(); return true; } return false; } getPrefixCode() { let prefix = ""; if (this.filenameVarName) { prefix += `const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath || "")};`; } if (this.isAutomaticRuntime) { if (this.importProcessor) { // CJS mode: emit require statements for all modules that were referenced. for (const [path, resolvedName] of Object.entries(this.cjsAutomaticModuleNameResolutions)) { prefix += `var ${resolvedName} = require("${path}");`; } } else { // ESM mode: consolidate and emit import statements for referenced names. const {createElement: createElementResolution, ...otherResolutions} = this.esmAutomaticImportNameResolutions; if (createElementResolution) { prefix += `import {createElement as ${createElementResolution}} from "${this.jsxImportSource}";`; } const importSpecifiers = Object.entries(otherResolutions) .map(([name, resolvedName]) => `${name} as ${resolvedName}`) .join(", "); if (importSpecifiers) { const importPath = this.jsxImportSource + (this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime"); prefix += `import {${importSpecifiers}} from "${importPath}";`; } } } return prefix; } processJSXTag() { const {jsxRole, start} = this.tokens.currentToken(); // Calculate line number information at the very start (if in development // mode) so that the information is guaranteed to be queried in token order. const elementLocationCode = this.options.production ? null : this.getElementLocationCode(start); if (this.isAutomaticRuntime && jsxRole !== _tokenizer.JSXRole.KeyAfterPropSpread) { this.transformTagToJSXFunc(elementLocationCode, jsxRole); } else { this.transformTagToCreateElement(elementLocationCode); } } getElementLocationCode(firstTokenStart) { const lineNumber = this.getLineNumberForIndex(firstTokenStart); return `lineNumber: ${lineNumber}`; } /** * Get the line number for this source position. This is calculated lazily and * must be called in increasing order by index. */ getLineNumberForIndex(index) { const code = this.tokens.code; while (this.lastIndex < index && this.lastIndex < code.length) { if (code[this.lastIndex] === "\n") { this.lastLineNumber++; } this.lastIndex++; } return this.lastLineNumber; } /** * Convert the current JSX element to a call to jsx, jsxs, or jsxDEV. This is * the primary transformation for the automatic transform. * * Example: *
Hello{x}
* becomes * jsxs('div', {a: 1, children: ["Hello", x]}, 2) */ transformTagToJSXFunc(elementLocationCode, jsxRole) { const isStatic = jsxRole === _tokenizer.JSXRole.StaticChildren; // First tag is always jsxTagStart. this.tokens.replaceToken(this.getJSXFuncInvocationCode(isStatic)); let keyCode = null; if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) { // Fragment syntax. this.tokens.replaceToken(`${this.getFragmentCode()}, {`); this.processAutomaticChildrenAndEndProps(jsxRole); } else { // Normal open tag or self-closing tag. this.processTagIntro(); this.tokens.appendCode(", {"); keyCode = this.processProps(true); if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) { // Self-closing tag, no children to add, so close the props. this.tokens.appendCode("}"); } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) { // Tag with children. this.tokens.removeToken(); this.processAutomaticChildrenAndEndProps(jsxRole); } else { throw new Error("Expected either /> or > at the end of the tag."); } // If a key was present, move it to its own arg. Note that moving code // like this will cause line numbers to get out of sync within the JSX // element if the key expression has a newline in it. This is unfortunate, // but hopefully should be rare. if (keyCode) { this.tokens.appendCode(`, ${keyCode}`); } } if (!this.options.production) { // If the key wasn't already added, add it now so we can correctly set // positional args for jsxDEV. if (keyCode === null) { this.tokens.appendCode(", void 0"); } this.tokens.appendCode(`, ${isStatic}, ${this.getDevSource(elementLocationCode)}, this`); } // We're at the close-tag or the end of a self-closing tag, so remove // everything else and close the function call. this.tokens.removeInitialToken(); while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) { this.tokens.removeToken(); } this.tokens.replaceToken(")"); } /** * Convert the current JSX element to a createElement call. In the classic * runtime, this is the only case. In the automatic runtime, this is called * as a fallback in some situations. * * Example: *
Hello{x}
* becomes * React.createElement('div', {a: 1, key: 2}, "Hello", x) */ transformTagToCreateElement(elementLocationCode) { // First tag is always jsxTagStart. this.tokens.replaceToken(this.getCreateElementInvocationCode()); if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) { // Fragment syntax. this.tokens.replaceToken(`${this.getFragmentCode()}, null`); this.processChildren(true); } else { // Normal open tag or self-closing tag. this.processTagIntro(); this.processPropsObjectWithDevInfo(elementLocationCode); if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) { // Self-closing tag; no children to process. } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) { // Tag with children and a close-tag; process the children as args. this.tokens.removeToken(); this.processChildren(true); } else { throw new Error("Expected either /> or > at the end of the tag."); } } // We're at the close-tag or the end of a self-closing tag, so remove // everything else and close the function call. this.tokens.removeInitialToken(); while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) { this.tokens.removeToken(); } this.tokens.replaceToken(")"); } /** * Get the code for the relevant function for this context: jsx, jsxs, * or jsxDEV. The following open-paren is included as well. * * These functions are only used for the automatic runtime, so they are always * auto-imported, but the auto-import will be either CJS or ESM based on the * target module format. */ getJSXFuncInvocationCode(isStatic) { if (this.options.production) { if (isStatic) { return this.claimAutoImportedFuncInvocation("jsxs", "/jsx-runtime"); } else { return this.claimAutoImportedFuncInvocation("jsx", "/jsx-runtime"); } } else { return this.claimAutoImportedFuncInvocation("jsxDEV", "/jsx-dev-runtime"); } } /** * Return the code to use for the createElement function, e.g. * `React.createElement`, including the following open-paren. * * This is the main function to use for the classic runtime. For the * automatic runtime, this function is used as a fallback function to * preserve behavior when there is a prop spread followed by an explicit * key. In that automatic runtime case, the function should be automatically * imported. */ getCreateElementInvocationCode() { if (this.isAutomaticRuntime) { return this.claimAutoImportedFuncInvocation("createElement", ""); } else { const {jsxPragmaInfo} = this; const resolvedPragmaBaseName = this.importProcessor ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base : jsxPragmaInfo.base; return `${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`; } } /** * Return the code to use as the component when compiling a shorthand * fragment, e.g. `React.Fragment`. * * This may be called from either the classic or automatic runtime, and * the value should be auto-imported for the automatic runtime. */ getFragmentCode() { if (this.isAutomaticRuntime) { return this.claimAutoImportedName( "Fragment", this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime", ); } else { const {jsxPragmaInfo} = this; const resolvedFragmentPragmaBaseName = this.importProcessor ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) || jsxPragmaInfo.fragmentBase : jsxPragmaInfo.fragmentBase; return resolvedFragmentPragmaBaseName + jsxPragmaInfo.fragmentSuffix; } } /** * Return code that invokes the given function. * * When the imports transform is enabled, use the CJSImportTransformer * strategy of using `.call(void 0, ...` to avoid passing a `this` value in a * situation that would otherwise look like a method call. */ claimAutoImportedFuncInvocation(funcName, importPathSuffix) { const funcCode = this.claimAutoImportedName(funcName, importPathSuffix); if (this.importProcessor) { return `${funcCode}.call(void 0, `; } else { return `${funcCode}(`; } } claimAutoImportedName(funcName, importPathSuffix) { if (this.importProcessor) { // CJS mode: claim a name for the module and mark it for import. const path = this.jsxImportSource + importPathSuffix; if (!this.cjsAutomaticModuleNameResolutions[path]) { this.cjsAutomaticModuleNameResolutions[path] = this.importProcessor.getFreeIdentifierForPath(path); } return `${this.cjsAutomaticModuleNameResolutions[path]}.${funcName}`; } else { // ESM mode: claim a name for this function and add it to the names that // should be auto-imported when the prefix is generated. if (!this.esmAutomaticImportNameResolutions[funcName]) { this.esmAutomaticImportNameResolutions[funcName] = this.nameManager.claimFreeName( `_${funcName}`, ); } return this.esmAutomaticImportNameResolutions[funcName]; } } /** * Process the first part of a tag, before any props. */ processTagIntro() { // Walk forward until we see one of these patterns: // jsxName to start the first prop, preceded by another jsxName to end the tag name. // jsxName to start the first prop, preceded by greaterThan to end the type argument. // [open brace] to start the first prop. // [jsxTagEnd] to end the open-tag. // [slash, jsxTagEnd] to end the self-closing tag. let introEnd = this.tokens.currentIndex() + 1; while ( this.tokens.tokens[introEnd].isType || (!this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.jsxName, _types.TokenType.jsxName) && !this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.greaterThan, _types.TokenType.jsxName) && !this.tokens.matches1AtIndex(introEnd, _types.TokenType.braceL) && !this.tokens.matches1AtIndex(introEnd, _types.TokenType.jsxTagEnd) && !this.tokens.matches2AtIndex(introEnd, _types.TokenType.slash, _types.TokenType.jsxTagEnd)) ) { introEnd++; } if (introEnd === this.tokens.currentIndex() + 1) { const tagName = this.tokens.identifierName(); if (startsWithLowerCase(tagName)) { this.tokens.replaceToken(`'${tagName}'`); } } while (this.tokens.currentIndex() < introEnd) { this.rootTransformer.processToken(); } } /** * Starting at the beginning of the props, add the props argument to * React.createElement, including the comma before it. */ processPropsObjectWithDevInfo(elementLocationCode) { const devProps = this.options.production ? "" : `__self: this, __source: ${this.getDevSource(elementLocationCode)}`; if (!this.tokens.matches1(_types.TokenType.jsxName) && !this.tokens.matches1(_types.TokenType.braceL)) { if (devProps) { this.tokens.appendCode(`, {${devProps}}`); } else { this.tokens.appendCode(`, null`); } return; } this.tokens.appendCode(`, {`); this.processProps(false); if (devProps) { this.tokens.appendCode(` ${devProps}}`); } else { this.tokens.appendCode("}"); } } /** * Transform the core part of the props, assuming that a { has already been * inserted before us and that a } will be inserted after us. * * If extractKeyCode is true (i.e. when using any jsx... function), any prop * named "key" has its code captured and returned rather than being emitted to * the output code. This shifts line numbers, and emitting the code later will * correct line numbers again. If no key is found or if extractKeyCode is * false, this function returns null. */ processProps(extractKeyCode) { let keyCode = null; while (true) { if (this.tokens.matches2(_types.TokenType.jsxName, _types.TokenType.eq)) { // This is a regular key={value} or key="value" prop. const propName = this.tokens.identifierName(); if (extractKeyCode && propName === "key") { if (keyCode !== null) { // The props list has multiple keys. Different implementations are // inconsistent about what to do here: as of this writing, Babel and // swc keep the *last* key and completely remove the rest, while // TypeScript uses the *first* key and leaves the others as regular // props. The React team collaborated with Babel on the // implementation of this behavior, so presumably the Babel behavior // is the one to use. // Since we won't ever be emitting the previous key code, we need to // at least emit its newlines here so that the line numbers match up // in the long run. this.tokens.appendCode(keyCode.replace(/[^\n]/g, "")); } // key this.tokens.removeToken(); // = this.tokens.removeToken(); const snapshot = this.tokens.snapshot(); this.processPropValue(); keyCode = this.tokens.dangerouslyGetAndRemoveCodeSinceSnapshot(snapshot); // Don't add a comma continue; } else { this.processPropName(propName); this.tokens.replaceToken(": "); this.processPropValue(); } } else if (this.tokens.matches1(_types.TokenType.jsxName)) { // This is a shorthand prop like . const propName = this.tokens.identifierName(); this.processPropName(propName); this.tokens.appendCode(": true"); } else if (this.tokens.matches1(_types.TokenType.braceL)) { // This is prop spread, like
, which we can pass // through fairly directly as an object spread. this.tokens.replaceToken(""); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(""); } else { break; } this.tokens.appendCode(","); } return keyCode; } processPropName(propName) { if (propName.includes("-")) { this.tokens.replaceToken(`'${propName}'`); } else { this.tokens.copyToken(); } } processPropValue() { if (this.tokens.matches1(_types.TokenType.braceL)) { this.tokens.replaceToken(""); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(""); } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) { this.processJSXTag(); } else { this.processStringPropValue(); } } processStringPropValue() { const token = this.tokens.currentToken(); const valueCode = this.tokens.code.slice(token.start + 1, token.end - 1); const replacementCode = formatJSXTextReplacement(valueCode); const literalCode = formatJSXStringValueLiteral(valueCode); this.tokens.replaceToken(literalCode + replacementCode); } /** * Starting in the middle of the props object literal, produce an additional * prop for the children and close the object literal. */ processAutomaticChildrenAndEndProps(jsxRole) { if (jsxRole === _tokenizer.JSXRole.StaticChildren) { this.tokens.appendCode(" children: ["); this.processChildren(false); this.tokens.appendCode("]}"); } else { // The parser information tells us whether we will see a real child or if // all remaining children (if any) will resolve to empty. If there are no // non-empty children, don't emit a children prop at all, but still // process children so that we properly transform the code into nothing. if (jsxRole === _tokenizer.JSXRole.OneChild) { this.tokens.appendCode(" children: "); } this.processChildren(false); this.tokens.appendCode("}"); } } /** * Transform children into a comma-separated list, which will be either * arguments to createElement or array elements of a children prop. */ processChildren(needsInitialComma) { let needsComma = needsInitialComma; while (true) { if (this.tokens.matches2(_types.TokenType.jsxTagStart, _types.TokenType.slash)) { // Closing tag, so no more children. return; } let didEmitElement = false; if (this.tokens.matches1(_types.TokenType.braceL)) { if (this.tokens.matches2(_types.TokenType.braceL, _types.TokenType.braceR)) { // Empty interpolations and comment-only interpolations are allowed // and don't create an extra child arg. this.tokens.replaceToken(""); this.tokens.replaceToken(""); } else { // Interpolated expression. this.tokens.replaceToken(needsComma ? ", " : ""); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(""); didEmitElement = true; } } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) { // Child JSX element this.tokens.appendCode(needsComma ? ", " : ""); this.processJSXTag(); didEmitElement = true; } else if (this.tokens.matches1(_types.TokenType.jsxText) || this.tokens.matches1(_types.TokenType.jsxEmptyText)) { didEmitElement = this.processChildTextElement(needsComma); } else { throw new Error("Unexpected token when processing JSX children."); } if (didEmitElement) { needsComma = true; } } } /** * Turn a JSX text element into a string literal, or nothing at all if the JSX * text resolves to the empty string. * * Returns true if a string literal is emitted, false otherwise. */ processChildTextElement(needsComma) { const token = this.tokens.currentToken(); const valueCode = this.tokens.code.slice(token.start, token.end); const replacementCode = formatJSXTextReplacement(valueCode); const literalCode = formatJSXTextLiteral(valueCode); if (literalCode === '""') { this.tokens.replaceToken(replacementCode); return false; } else { this.tokens.replaceToken(`${needsComma ? ", " : ""}${literalCode}${replacementCode}`); return true; } } getDevSource(elementLocationCode) { return `{fileName: ${this.getFilenameVarName()}, ${elementLocationCode}}`; } getFilenameVarName() { if (!this.filenameVarName) { this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName"); } return this.filenameVarName; } } exports.default = JSXTransformer; /** * Spec for identifiers: https://tc39.github.io/ecma262/#prod-IdentifierStart. * * Really only treat anything starting with a-z as tag names. `_`, `$`, `é` * should be treated as component names */ function startsWithLowerCase(s) { const firstChar = s.charCodeAt(0); return firstChar >= _charcodes.charCodes.lowercaseA && firstChar <= _charcodes.charCodes.lowercaseZ; } exports.startsWithLowerCase = startsWithLowerCase; /** * Turn the given jsxText string into a JS string literal. Leading and trailing * whitespace on lines is removed, except immediately after the open-tag and * before the close-tag. Empty lines are completely removed, and spaces are * added between lines after that. * * 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) { let result = ""; let whitespace = ""; let isInInitialLineWhitespace = false; let seenNonWhitespace = false; for (let i = 0; i < text.length; i++) { const c = text[i]; if (c === " " || c === "\t" || c === "\r") { if (!isInInitialLineWhitespace) { whitespace += c; } } else if (c === "\n") { whitespace = ""; isInInitialLineWhitespace = true; } else { if (seenNonWhitespace && isInInitialLineWhitespace) { result += " "; } result += whitespace; whitespace = ""; if (c === "&") { const {entity, newI} = processEntity(text, i + 1); i = newI - 1; result += entity; } else { result += c; } seenNonWhitespace = true; isInInitialLineWhitespace = false; } } if (!isInInitialLineWhitespace) { result += whitespace; } return JSON.stringify(result); } /** * 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) { let numNewlines = 0; let numSpaces = 0; for (const c of text) { if (c === "\n") { numNewlines++; numSpaces = 0; } else if (c === " ") { numSpaces++; } } return "\n".repeat(numNewlines) + " ".repeat(numSpaces); } /** * 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) { let result = ""; for (let i = 0; i < text.length; i++) { const c = text[i]; if (c === "\n") { if (/\s/.test(text[i + 1])) { result += " "; while (i < text.length && /\s/.test(text[i + 1])) { i++; } } else { result += "\n"; } } else if (c === "&") { const {entity, newI} = processEntity(text, i + 1); result += entity; i = newI - 1; } else { result += c; } } return JSON.stringify(result); } /** * Starting at a &, see if there's an HTML entity (specified by name, decimal * char code, or hex char code) and return it if so. * * Modified from jsxReadString in babel-parser. */ function processEntity(text, indexAfterAmpersand) { let str = ""; let count = 0; let entity; let i = indexAfterAmpersand; if (text[i] === "#") { let radix = 10; i++; let numStart; if (text[i] === "x") { radix = 16; i++; numStart = i; while (i < text.length && isHexDigit(text.charCodeAt(i))) { i++; } } else { numStart = i; while (i < text.length && isDecimalDigit(text.charCodeAt(i))) { i++; } } if (text[i] === ";") { const numStr = text.slice(numStart, i); if (numStr) { i++; entity = String.fromCodePoint(parseInt(numStr, radix)); } } } else { while (i < text.length && count++ < 10) { const ch = text[i]; i++; if (ch === ";") { entity = _xhtml2.default.get(str); break; } str += ch; } } if (!entity) { return {entity: "&", newI: indexAfterAmpersand}; } return {entity, newI: i}; } function isDecimalDigit(code) { return code >= _charcodes.charCodes.digit0 && code <= _charcodes.charCodes.digit9; } function isHexDigit(code) { return ( (code >= _charcodes.charCodes.digit0 && code <= _charcodes.charCodes.digit9) || (code >= _charcodes.charCodes.lowercaseA && code <= _charcodes.charCodes.lowercaseF) || (code >= _charcodes.charCodes.uppercaseA && code <= _charcodes.charCodes.uppercaseF) ); }