| 1 | "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 | var _xhtml = require('../parser/plugins/jsx/xhtml'); var _xhtml2 = _interopRequireDefault(_xhtml);
|
|---|
| 5 | var _tokenizer = require('../parser/tokenizer');
|
|---|
| 6 | var _types = require('../parser/tokenizer/types');
|
|---|
| 7 | var _charcodes = require('../parser/util/charcodes');
|
|---|
| 8 |
|
|---|
| 9 | var _getJSXPragmaInfo = require('../util/getJSXPragmaInfo'); var _getJSXPragmaInfo2 = _interopRequireDefault(_getJSXPragmaInfo);
|
|---|
| 10 |
|
|---|
| 11 | var _Transformer = require('./Transformer'); var _Transformer2 = _interopRequireDefault(_Transformer);
|
|---|
| 12 |
|
|---|
| 13 | class JSXTransformer extends _Transformer2.default {
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 | // State for calculating the line number of each JSX tag in development.
|
|---|
| 19 | __init() {this.lastLineNumber = 1}
|
|---|
| 20 | __init2() {this.lastIndex = 0}
|
|---|
| 21 |
|
|---|
| 22 | // In development, variable name holding the name of the current file.
|
|---|
| 23 | __init3() {this.filenameVarName = null}
|
|---|
| 24 | // Mapping of claimed names for imports in the automatic transform, e,g.
|
|---|
| 25 | // {jsx: "_jsx"}. This determines which imports to generate in the prefix.
|
|---|
| 26 | __init4() {this.esmAutomaticImportNameResolutions = {}}
|
|---|
| 27 | // When automatically adding imports in CJS mode, we store the variable name
|
|---|
| 28 | // holding the imported CJS module so we can require it in the prefix.
|
|---|
| 29 | __init5() {this.cjsAutomaticModuleNameResolutions = {}}
|
|---|
| 30 |
|
|---|
| 31 | constructor(
|
|---|
| 32 | rootTransformer,
|
|---|
| 33 | tokens,
|
|---|
| 34 | importProcessor,
|
|---|
| 35 | nameManager,
|
|---|
| 36 | options,
|
|---|
| 37 | ) {
|
|---|
| 38 | 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);;
|
|---|
| 39 | this.jsxPragmaInfo = _getJSXPragmaInfo2.default.call(void 0, options);
|
|---|
| 40 | this.isAutomaticRuntime = options.jsxRuntime === "automatic";
|
|---|
| 41 | this.jsxImportSource = options.jsxImportSource || "react";
|
|---|
| 42 | }
|
|---|
| 43 |
|
|---|
| 44 | process() {
|
|---|
| 45 | if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
|
|---|
| 46 | this.processJSXTag();
|
|---|
| 47 | return true;
|
|---|
| 48 | }
|
|---|
| 49 | return false;
|
|---|
| 50 | }
|
|---|
| 51 |
|
|---|
| 52 | getPrefixCode() {
|
|---|
| 53 | let prefix = "";
|
|---|
| 54 | if (this.filenameVarName) {
|
|---|
| 55 | prefix += `const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath || "")};`;
|
|---|
| 56 | }
|
|---|
| 57 | if (this.isAutomaticRuntime) {
|
|---|
| 58 | if (this.importProcessor) {
|
|---|
| 59 | // CJS mode: emit require statements for all modules that were referenced.
|
|---|
| 60 | for (const [path, resolvedName] of Object.entries(this.cjsAutomaticModuleNameResolutions)) {
|
|---|
| 61 | prefix += `var ${resolvedName} = require("${path}");`;
|
|---|
| 62 | }
|
|---|
| 63 | } else {
|
|---|
| 64 | // ESM mode: consolidate and emit import statements for referenced names.
|
|---|
| 65 | const {createElement: createElementResolution, ...otherResolutions} =
|
|---|
| 66 | this.esmAutomaticImportNameResolutions;
|
|---|
| 67 | if (createElementResolution) {
|
|---|
| 68 | prefix += `import {createElement as ${createElementResolution}} from "${this.jsxImportSource}";`;
|
|---|
| 69 | }
|
|---|
| 70 | const importSpecifiers = Object.entries(otherResolutions)
|
|---|
| 71 | .map(([name, resolvedName]) => `${name} as ${resolvedName}`)
|
|---|
| 72 | .join(", ");
|
|---|
| 73 | if (importSpecifiers) {
|
|---|
| 74 | const importPath =
|
|---|
| 75 | this.jsxImportSource + (this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime");
|
|---|
| 76 | prefix += `import {${importSpecifiers}} from "${importPath}";`;
|
|---|
| 77 | }
|
|---|
| 78 | }
|
|---|
| 79 | }
|
|---|
| 80 | return prefix;
|
|---|
| 81 | }
|
|---|
| 82 |
|
|---|
| 83 | processJSXTag() {
|
|---|
| 84 | const {jsxRole, start} = this.tokens.currentToken();
|
|---|
| 85 | // Calculate line number information at the very start (if in development
|
|---|
| 86 | // mode) so that the information is guaranteed to be queried in token order.
|
|---|
| 87 | const elementLocationCode = this.options.production ? null : this.getElementLocationCode(start);
|
|---|
| 88 | if (this.isAutomaticRuntime && jsxRole !== _tokenizer.JSXRole.KeyAfterPropSpread) {
|
|---|
| 89 | this.transformTagToJSXFunc(elementLocationCode, jsxRole);
|
|---|
| 90 | } else {
|
|---|
| 91 | this.transformTagToCreateElement(elementLocationCode);
|
|---|
| 92 | }
|
|---|
| 93 | }
|
|---|
| 94 |
|
|---|
| 95 | getElementLocationCode(firstTokenStart) {
|
|---|
| 96 | const lineNumber = this.getLineNumberForIndex(firstTokenStart);
|
|---|
| 97 | return `lineNumber: ${lineNumber}`;
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 | /**
|
|---|
| 101 | * Get the line number for this source position. This is calculated lazily and
|
|---|
| 102 | * must be called in increasing order by index.
|
|---|
| 103 | */
|
|---|
| 104 | getLineNumberForIndex(index) {
|
|---|
| 105 | const code = this.tokens.code;
|
|---|
| 106 | while (this.lastIndex < index && this.lastIndex < code.length) {
|
|---|
| 107 | if (code[this.lastIndex] === "\n") {
|
|---|
| 108 | this.lastLineNumber++;
|
|---|
| 109 | }
|
|---|
| 110 | this.lastIndex++;
|
|---|
| 111 | }
|
|---|
| 112 | return this.lastLineNumber;
|
|---|
| 113 | }
|
|---|
| 114 |
|
|---|
| 115 | /**
|
|---|
| 116 | * Convert the current JSX element to a call to jsx, jsxs, or jsxDEV. This is
|
|---|
| 117 | * the primary transformation for the automatic transform.
|
|---|
| 118 | *
|
|---|
| 119 | * Example:
|
|---|
| 120 | * <div a={1} key={2}>Hello{x}</div>
|
|---|
| 121 | * becomes
|
|---|
| 122 | * jsxs('div', {a: 1, children: ["Hello", x]}, 2)
|
|---|
| 123 | */
|
|---|
| 124 | transformTagToJSXFunc(elementLocationCode, jsxRole) {
|
|---|
| 125 | const isStatic = jsxRole === _tokenizer.JSXRole.StaticChildren;
|
|---|
| 126 | // First tag is always jsxTagStart.
|
|---|
| 127 | this.tokens.replaceToken(this.getJSXFuncInvocationCode(isStatic));
|
|---|
| 128 |
|
|---|
| 129 | let keyCode = null;
|
|---|
| 130 | if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
|
|---|
| 131 | // Fragment syntax.
|
|---|
| 132 | this.tokens.replaceToken(`${this.getFragmentCode()}, {`);
|
|---|
| 133 | this.processAutomaticChildrenAndEndProps(jsxRole);
|
|---|
| 134 | } else {
|
|---|
| 135 | // Normal open tag or self-closing tag.
|
|---|
| 136 | this.processTagIntro();
|
|---|
| 137 | this.tokens.appendCode(", {");
|
|---|
| 138 | keyCode = this.processProps(true);
|
|---|
| 139 |
|
|---|
| 140 | if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) {
|
|---|
| 141 | // Self-closing tag, no children to add, so close the props.
|
|---|
| 142 | this.tokens.appendCode("}");
|
|---|
| 143 | } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
|
|---|
| 144 | // Tag with children.
|
|---|
| 145 | this.tokens.removeToken();
|
|---|
| 146 | this.processAutomaticChildrenAndEndProps(jsxRole);
|
|---|
| 147 | } else {
|
|---|
| 148 | throw new Error("Expected either /> or > at the end of the tag.");
|
|---|
| 149 | }
|
|---|
| 150 | // If a key was present, move it to its own arg. Note that moving code
|
|---|
| 151 | // like this will cause line numbers to get out of sync within the JSX
|
|---|
| 152 | // element if the key expression has a newline in it. This is unfortunate,
|
|---|
| 153 | // but hopefully should be rare.
|
|---|
| 154 | if (keyCode) {
|
|---|
| 155 | this.tokens.appendCode(`, ${keyCode}`);
|
|---|
| 156 | }
|
|---|
| 157 | }
|
|---|
| 158 | if (!this.options.production) {
|
|---|
| 159 | // If the key wasn't already added, add it now so we can correctly set
|
|---|
| 160 | // positional args for jsxDEV.
|
|---|
| 161 | if (keyCode === null) {
|
|---|
| 162 | this.tokens.appendCode(", void 0");
|
|---|
| 163 | }
|
|---|
| 164 | this.tokens.appendCode(`, ${isStatic}, ${this.getDevSource(elementLocationCode)}, this`);
|
|---|
| 165 | }
|
|---|
| 166 | // We're at the close-tag or the end of a self-closing tag, so remove
|
|---|
| 167 | // everything else and close the function call.
|
|---|
| 168 | this.tokens.removeInitialToken();
|
|---|
| 169 | while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
|
|---|
| 170 | this.tokens.removeToken();
|
|---|
| 171 | }
|
|---|
| 172 | this.tokens.replaceToken(")");
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | /**
|
|---|
| 176 | * Convert the current JSX element to a createElement call. In the classic
|
|---|
| 177 | * runtime, this is the only case. In the automatic runtime, this is called
|
|---|
| 178 | * as a fallback in some situations.
|
|---|
| 179 | *
|
|---|
| 180 | * Example:
|
|---|
| 181 | * <div a={1} key={2}>Hello{x}</div>
|
|---|
| 182 | * becomes
|
|---|
| 183 | * React.createElement('div', {a: 1, key: 2}, "Hello", x)
|
|---|
| 184 | */
|
|---|
| 185 | transformTagToCreateElement(elementLocationCode) {
|
|---|
| 186 | // First tag is always jsxTagStart.
|
|---|
| 187 | this.tokens.replaceToken(this.getCreateElementInvocationCode());
|
|---|
| 188 |
|
|---|
| 189 | if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
|
|---|
| 190 | // Fragment syntax.
|
|---|
| 191 | this.tokens.replaceToken(`${this.getFragmentCode()}, null`);
|
|---|
| 192 | this.processChildren(true);
|
|---|
| 193 | } else {
|
|---|
| 194 | // Normal open tag or self-closing tag.
|
|---|
| 195 | this.processTagIntro();
|
|---|
| 196 | this.processPropsObjectWithDevInfo(elementLocationCode);
|
|---|
| 197 |
|
|---|
| 198 | if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) {
|
|---|
| 199 | // Self-closing tag; no children to process.
|
|---|
| 200 | } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
|
|---|
| 201 | // Tag with children and a close-tag; process the children as args.
|
|---|
| 202 | this.tokens.removeToken();
|
|---|
| 203 | this.processChildren(true);
|
|---|
| 204 | } else {
|
|---|
| 205 | throw new Error("Expected either /> or > at the end of the tag.");
|
|---|
| 206 | }
|
|---|
| 207 | }
|
|---|
| 208 | // We're at the close-tag or the end of a self-closing tag, so remove
|
|---|
| 209 | // everything else and close the function call.
|
|---|
| 210 | this.tokens.removeInitialToken();
|
|---|
| 211 | while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
|
|---|
| 212 | this.tokens.removeToken();
|
|---|
| 213 | }
|
|---|
| 214 | this.tokens.replaceToken(")");
|
|---|
| 215 | }
|
|---|
| 216 |
|
|---|
| 217 | /**
|
|---|
| 218 | * Get the code for the relevant function for this context: jsx, jsxs,
|
|---|
| 219 | * or jsxDEV. The following open-paren is included as well.
|
|---|
| 220 | *
|
|---|
| 221 | * These functions are only used for the automatic runtime, so they are always
|
|---|
| 222 | * auto-imported, but the auto-import will be either CJS or ESM based on the
|
|---|
| 223 | * target module format.
|
|---|
| 224 | */
|
|---|
| 225 | getJSXFuncInvocationCode(isStatic) {
|
|---|
| 226 | if (this.options.production) {
|
|---|
| 227 | if (isStatic) {
|
|---|
| 228 | return this.claimAutoImportedFuncInvocation("jsxs", "/jsx-runtime");
|
|---|
| 229 | } else {
|
|---|
| 230 | return this.claimAutoImportedFuncInvocation("jsx", "/jsx-runtime");
|
|---|
| 231 | }
|
|---|
| 232 | } else {
|
|---|
| 233 | return this.claimAutoImportedFuncInvocation("jsxDEV", "/jsx-dev-runtime");
|
|---|
| 234 | }
|
|---|
| 235 | }
|
|---|
| 236 |
|
|---|
| 237 | /**
|
|---|
| 238 | * Return the code to use for the createElement function, e.g.
|
|---|
| 239 | * `React.createElement`, including the following open-paren.
|
|---|
| 240 | *
|
|---|
| 241 | * This is the main function to use for the classic runtime. For the
|
|---|
| 242 | * automatic runtime, this function is used as a fallback function to
|
|---|
| 243 | * preserve behavior when there is a prop spread followed by an explicit
|
|---|
| 244 | * key. In that automatic runtime case, the function should be automatically
|
|---|
| 245 | * imported.
|
|---|
| 246 | */
|
|---|
| 247 | getCreateElementInvocationCode() {
|
|---|
| 248 | if (this.isAutomaticRuntime) {
|
|---|
| 249 | return this.claimAutoImportedFuncInvocation("createElement", "");
|
|---|
| 250 | } else {
|
|---|
| 251 | const {jsxPragmaInfo} = this;
|
|---|
| 252 | const resolvedPragmaBaseName = this.importProcessor
|
|---|
| 253 | ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base
|
|---|
| 254 | : jsxPragmaInfo.base;
|
|---|
| 255 | return `${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`;
|
|---|
| 256 | }
|
|---|
| 257 | }
|
|---|
| 258 |
|
|---|
| 259 | /**
|
|---|
| 260 | * Return the code to use as the component when compiling a shorthand
|
|---|
| 261 | * fragment, e.g. `React.Fragment`.
|
|---|
| 262 | *
|
|---|
| 263 | * This may be called from either the classic or automatic runtime, and
|
|---|
| 264 | * the value should be auto-imported for the automatic runtime.
|
|---|
| 265 | */
|
|---|
| 266 | getFragmentCode() {
|
|---|
| 267 | if (this.isAutomaticRuntime) {
|
|---|
| 268 | return this.claimAutoImportedName(
|
|---|
| 269 | "Fragment",
|
|---|
| 270 | this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime",
|
|---|
| 271 | );
|
|---|
| 272 | } else {
|
|---|
| 273 | const {jsxPragmaInfo} = this;
|
|---|
| 274 | const resolvedFragmentPragmaBaseName = this.importProcessor
|
|---|
| 275 | ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) ||
|
|---|
| 276 | jsxPragmaInfo.fragmentBase
|
|---|
| 277 | : jsxPragmaInfo.fragmentBase;
|
|---|
| 278 | return resolvedFragmentPragmaBaseName + jsxPragmaInfo.fragmentSuffix;
|
|---|
| 279 | }
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | /**
|
|---|
| 283 | * Return code that invokes the given function.
|
|---|
| 284 | *
|
|---|
| 285 | * When the imports transform is enabled, use the CJSImportTransformer
|
|---|
| 286 | * strategy of using `.call(void 0, ...` to avoid passing a `this` value in a
|
|---|
| 287 | * situation that would otherwise look like a method call.
|
|---|
| 288 | */
|
|---|
| 289 | claimAutoImportedFuncInvocation(funcName, importPathSuffix) {
|
|---|
| 290 | const funcCode = this.claimAutoImportedName(funcName, importPathSuffix);
|
|---|
| 291 | if (this.importProcessor) {
|
|---|
| 292 | return `${funcCode}.call(void 0, `;
|
|---|
| 293 | } else {
|
|---|
| 294 | return `${funcCode}(`;
|
|---|
| 295 | }
|
|---|
| 296 | }
|
|---|
| 297 |
|
|---|
| 298 | claimAutoImportedName(funcName, importPathSuffix) {
|
|---|
| 299 | if (this.importProcessor) {
|
|---|
| 300 | // CJS mode: claim a name for the module and mark it for import.
|
|---|
| 301 | const path = this.jsxImportSource + importPathSuffix;
|
|---|
| 302 | if (!this.cjsAutomaticModuleNameResolutions[path]) {
|
|---|
| 303 | this.cjsAutomaticModuleNameResolutions[path] =
|
|---|
| 304 | this.importProcessor.getFreeIdentifierForPath(path);
|
|---|
| 305 | }
|
|---|
| 306 | return `${this.cjsAutomaticModuleNameResolutions[path]}.${funcName}`;
|
|---|
| 307 | } else {
|
|---|
| 308 | // ESM mode: claim a name for this function and add it to the names that
|
|---|
| 309 | // should be auto-imported when the prefix is generated.
|
|---|
| 310 | if (!this.esmAutomaticImportNameResolutions[funcName]) {
|
|---|
| 311 | this.esmAutomaticImportNameResolutions[funcName] = this.nameManager.claimFreeName(
|
|---|
| 312 | `_${funcName}`,
|
|---|
| 313 | );
|
|---|
| 314 | }
|
|---|
| 315 | return this.esmAutomaticImportNameResolutions[funcName];
|
|---|
| 316 | }
|
|---|
| 317 | }
|
|---|
| 318 |
|
|---|
| 319 | /**
|
|---|
| 320 | * Process the first part of a tag, before any props.
|
|---|
| 321 | */
|
|---|
| 322 | processTagIntro() {
|
|---|
| 323 | // Walk forward until we see one of these patterns:
|
|---|
| 324 | // jsxName to start the first prop, preceded by another jsxName to end the tag name.
|
|---|
| 325 | // jsxName to start the first prop, preceded by greaterThan to end the type argument.
|
|---|
| 326 | // [open brace] to start the first prop.
|
|---|
| 327 | // [jsxTagEnd] to end the open-tag.
|
|---|
| 328 | // [slash, jsxTagEnd] to end the self-closing tag.
|
|---|
| 329 | let introEnd = this.tokens.currentIndex() + 1;
|
|---|
| 330 | while (
|
|---|
| 331 | this.tokens.tokens[introEnd].isType ||
|
|---|
| 332 | (!this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.jsxName, _types.TokenType.jsxName) &&
|
|---|
| 333 | !this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.greaterThan, _types.TokenType.jsxName) &&
|
|---|
| 334 | !this.tokens.matches1AtIndex(introEnd, _types.TokenType.braceL) &&
|
|---|
| 335 | !this.tokens.matches1AtIndex(introEnd, _types.TokenType.jsxTagEnd) &&
|
|---|
| 336 | !this.tokens.matches2AtIndex(introEnd, _types.TokenType.slash, _types.TokenType.jsxTagEnd))
|
|---|
| 337 | ) {
|
|---|
| 338 | introEnd++;
|
|---|
| 339 | }
|
|---|
| 340 | if (introEnd === this.tokens.currentIndex() + 1) {
|
|---|
| 341 | const tagName = this.tokens.identifierName();
|
|---|
| 342 | if (startsWithLowerCase(tagName)) {
|
|---|
| 343 | this.tokens.replaceToken(`'${tagName}'`);
|
|---|
| 344 | }
|
|---|
| 345 | }
|
|---|
| 346 | while (this.tokens.currentIndex() < introEnd) {
|
|---|
| 347 | this.rootTransformer.processToken();
|
|---|
| 348 | }
|
|---|
| 349 | }
|
|---|
| 350 |
|
|---|
| 351 | /**
|
|---|
| 352 | * Starting at the beginning of the props, add the props argument to
|
|---|
| 353 | * React.createElement, including the comma before it.
|
|---|
| 354 | */
|
|---|
| 355 | processPropsObjectWithDevInfo(elementLocationCode) {
|
|---|
| 356 | const devProps = this.options.production
|
|---|
| 357 | ? ""
|
|---|
| 358 | : `__self: this, __source: ${this.getDevSource(elementLocationCode)}`;
|
|---|
| 359 | if (!this.tokens.matches1(_types.TokenType.jsxName) && !this.tokens.matches1(_types.TokenType.braceL)) {
|
|---|
| 360 | if (devProps) {
|
|---|
| 361 | this.tokens.appendCode(`, {${devProps}}`);
|
|---|
| 362 | } else {
|
|---|
| 363 | this.tokens.appendCode(`, null`);
|
|---|
| 364 | }
|
|---|
| 365 | return;
|
|---|
| 366 | }
|
|---|
| 367 | this.tokens.appendCode(`, {`);
|
|---|
| 368 | this.processProps(false);
|
|---|
| 369 | if (devProps) {
|
|---|
| 370 | this.tokens.appendCode(` ${devProps}}`);
|
|---|
| 371 | } else {
|
|---|
| 372 | this.tokens.appendCode("}");
|
|---|
| 373 | }
|
|---|
| 374 | }
|
|---|
| 375 |
|
|---|
| 376 | /**
|
|---|
| 377 | * Transform the core part of the props, assuming that a { has already been
|
|---|
| 378 | * inserted before us and that a } will be inserted after us.
|
|---|
| 379 | *
|
|---|
| 380 | * If extractKeyCode is true (i.e. when using any jsx... function), any prop
|
|---|
| 381 | * named "key" has its code captured and returned rather than being emitted to
|
|---|
| 382 | * the output code. This shifts line numbers, and emitting the code later will
|
|---|
| 383 | * correct line numbers again. If no key is found or if extractKeyCode is
|
|---|
| 384 | * false, this function returns null.
|
|---|
| 385 | */
|
|---|
| 386 | processProps(extractKeyCode) {
|
|---|
| 387 | let keyCode = null;
|
|---|
| 388 | while (true) {
|
|---|
| 389 | if (this.tokens.matches2(_types.TokenType.jsxName, _types.TokenType.eq)) {
|
|---|
| 390 | // This is a regular key={value} or key="value" prop.
|
|---|
| 391 | const propName = this.tokens.identifierName();
|
|---|
| 392 | if (extractKeyCode && propName === "key") {
|
|---|
| 393 | if (keyCode !== null) {
|
|---|
| 394 | // The props list has multiple keys. Different implementations are
|
|---|
| 395 | // inconsistent about what to do here: as of this writing, Babel and
|
|---|
| 396 | // swc keep the *last* key and completely remove the rest, while
|
|---|
| 397 | // TypeScript uses the *first* key and leaves the others as regular
|
|---|
| 398 | // props. The React team collaborated with Babel on the
|
|---|
| 399 | // implementation of this behavior, so presumably the Babel behavior
|
|---|
| 400 | // is the one to use.
|
|---|
| 401 | // Since we won't ever be emitting the previous key code, we need to
|
|---|
| 402 | // at least emit its newlines here so that the line numbers match up
|
|---|
| 403 | // in the long run.
|
|---|
| 404 | this.tokens.appendCode(keyCode.replace(/[^\n]/g, ""));
|
|---|
| 405 | }
|
|---|
| 406 | // key
|
|---|
| 407 | this.tokens.removeToken();
|
|---|
| 408 | // =
|
|---|
| 409 | this.tokens.removeToken();
|
|---|
| 410 | const snapshot = this.tokens.snapshot();
|
|---|
| 411 | this.processPropValue();
|
|---|
| 412 | keyCode = this.tokens.dangerouslyGetAndRemoveCodeSinceSnapshot(snapshot);
|
|---|
| 413 | // Don't add a comma
|
|---|
| 414 | continue;
|
|---|
| 415 | } else {
|
|---|
| 416 | this.processPropName(propName);
|
|---|
| 417 | this.tokens.replaceToken(": ");
|
|---|
| 418 | this.processPropValue();
|
|---|
| 419 | }
|
|---|
| 420 | } else if (this.tokens.matches1(_types.TokenType.jsxName)) {
|
|---|
| 421 | // This is a shorthand prop like <input disabled />.
|
|---|
| 422 | const propName = this.tokens.identifierName();
|
|---|
| 423 | this.processPropName(propName);
|
|---|
| 424 | this.tokens.appendCode(": true");
|
|---|
| 425 | } else if (this.tokens.matches1(_types.TokenType.braceL)) {
|
|---|
| 426 | // This is prop spread, like <div {...getProps()}>, which we can pass
|
|---|
| 427 | // through fairly directly as an object spread.
|
|---|
| 428 | this.tokens.replaceToken("");
|
|---|
| 429 | this.rootTransformer.processBalancedCode();
|
|---|
| 430 | this.tokens.replaceToken("");
|
|---|
| 431 | } else {
|
|---|
| 432 | break;
|
|---|
| 433 | }
|
|---|
| 434 | this.tokens.appendCode(",");
|
|---|
| 435 | }
|
|---|
| 436 | return keyCode;
|
|---|
| 437 | }
|
|---|
| 438 |
|
|---|
| 439 | processPropName(propName) {
|
|---|
| 440 | if (propName.includes("-")) {
|
|---|
| 441 | this.tokens.replaceToken(`'${propName}'`);
|
|---|
| 442 | } else {
|
|---|
| 443 | this.tokens.copyToken();
|
|---|
| 444 | }
|
|---|
| 445 | }
|
|---|
| 446 |
|
|---|
| 447 | processPropValue() {
|
|---|
| 448 | if (this.tokens.matches1(_types.TokenType.braceL)) {
|
|---|
| 449 | this.tokens.replaceToken("");
|
|---|
| 450 | this.rootTransformer.processBalancedCode();
|
|---|
| 451 | this.tokens.replaceToken("");
|
|---|
| 452 | } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
|
|---|
| 453 | this.processJSXTag();
|
|---|
| 454 | } else {
|
|---|
| 455 | this.processStringPropValue();
|
|---|
| 456 | }
|
|---|
| 457 | }
|
|---|
| 458 |
|
|---|
| 459 | processStringPropValue() {
|
|---|
| 460 | const token = this.tokens.currentToken();
|
|---|
| 461 | const valueCode = this.tokens.code.slice(token.start + 1, token.end - 1);
|
|---|
| 462 | const replacementCode = formatJSXTextReplacement(valueCode);
|
|---|
| 463 | const literalCode = formatJSXStringValueLiteral(valueCode);
|
|---|
| 464 | this.tokens.replaceToken(literalCode + replacementCode);
|
|---|
| 465 | }
|
|---|
| 466 |
|
|---|
| 467 | /**
|
|---|
| 468 | * Starting in the middle of the props object literal, produce an additional
|
|---|
| 469 | * prop for the children and close the object literal.
|
|---|
| 470 | */
|
|---|
| 471 | processAutomaticChildrenAndEndProps(jsxRole) {
|
|---|
| 472 | if (jsxRole === _tokenizer.JSXRole.StaticChildren) {
|
|---|
| 473 | this.tokens.appendCode(" children: [");
|
|---|
| 474 | this.processChildren(false);
|
|---|
| 475 | this.tokens.appendCode("]}");
|
|---|
| 476 | } else {
|
|---|
| 477 | // The parser information tells us whether we will see a real child or if
|
|---|
| 478 | // all remaining children (if any) will resolve to empty. If there are no
|
|---|
| 479 | // non-empty children, don't emit a children prop at all, but still
|
|---|
| 480 | // process children so that we properly transform the code into nothing.
|
|---|
| 481 | if (jsxRole === _tokenizer.JSXRole.OneChild) {
|
|---|
| 482 | this.tokens.appendCode(" children: ");
|
|---|
| 483 | }
|
|---|
| 484 | this.processChildren(false);
|
|---|
| 485 | this.tokens.appendCode("}");
|
|---|
| 486 | }
|
|---|
| 487 | }
|
|---|
| 488 |
|
|---|
| 489 | /**
|
|---|
| 490 | * Transform children into a comma-separated list, which will be either
|
|---|
| 491 | * arguments to createElement or array elements of a children prop.
|
|---|
| 492 | */
|
|---|
| 493 | processChildren(needsInitialComma) {
|
|---|
| 494 | let needsComma = needsInitialComma;
|
|---|
| 495 | while (true) {
|
|---|
| 496 | if (this.tokens.matches2(_types.TokenType.jsxTagStart, _types.TokenType.slash)) {
|
|---|
| 497 | // Closing tag, so no more children.
|
|---|
| 498 | return;
|
|---|
| 499 | }
|
|---|
| 500 | let didEmitElement = false;
|
|---|
| 501 | if (this.tokens.matches1(_types.TokenType.braceL)) {
|
|---|
| 502 | if (this.tokens.matches2(_types.TokenType.braceL, _types.TokenType.braceR)) {
|
|---|
| 503 | // Empty interpolations and comment-only interpolations are allowed
|
|---|
| 504 | // and don't create an extra child arg.
|
|---|
| 505 | this.tokens.replaceToken("");
|
|---|
| 506 | this.tokens.replaceToken("");
|
|---|
| 507 | } else {
|
|---|
| 508 | // Interpolated expression.
|
|---|
| 509 | this.tokens.replaceToken(needsComma ? ", " : "");
|
|---|
| 510 | this.rootTransformer.processBalancedCode();
|
|---|
| 511 | this.tokens.replaceToken("");
|
|---|
| 512 | didEmitElement = true;
|
|---|
| 513 | }
|
|---|
| 514 | } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
|
|---|
| 515 | // Child JSX element
|
|---|
| 516 | this.tokens.appendCode(needsComma ? ", " : "");
|
|---|
| 517 | this.processJSXTag();
|
|---|
| 518 | didEmitElement = true;
|
|---|
| 519 | } else if (this.tokens.matches1(_types.TokenType.jsxText) || this.tokens.matches1(_types.TokenType.jsxEmptyText)) {
|
|---|
| 520 | didEmitElement = this.processChildTextElement(needsComma);
|
|---|
| 521 | } else {
|
|---|
| 522 | throw new Error("Unexpected token when processing JSX children.");
|
|---|
| 523 | }
|
|---|
| 524 | if (didEmitElement) {
|
|---|
| 525 | needsComma = true;
|
|---|
| 526 | }
|
|---|
| 527 | }
|
|---|
| 528 | }
|
|---|
| 529 |
|
|---|
| 530 | /**
|
|---|
| 531 | * Turn a JSX text element into a string literal, or nothing at all if the JSX
|
|---|
| 532 | * text resolves to the empty string.
|
|---|
| 533 | *
|
|---|
| 534 | * Returns true if a string literal is emitted, false otherwise.
|
|---|
| 535 | */
|
|---|
| 536 | processChildTextElement(needsComma) {
|
|---|
| 537 | const token = this.tokens.currentToken();
|
|---|
| 538 | const valueCode = this.tokens.code.slice(token.start, token.end);
|
|---|
| 539 | const replacementCode = formatJSXTextReplacement(valueCode);
|
|---|
| 540 | const literalCode = formatJSXTextLiteral(valueCode);
|
|---|
| 541 | if (literalCode === '""') {
|
|---|
| 542 | this.tokens.replaceToken(replacementCode);
|
|---|
| 543 | return false;
|
|---|
| 544 | } else {
|
|---|
| 545 | this.tokens.replaceToken(`${needsComma ? ", " : ""}${literalCode}${replacementCode}`);
|
|---|
| 546 | return true;
|
|---|
| 547 | }
|
|---|
| 548 | }
|
|---|
| 549 |
|
|---|
| 550 | getDevSource(elementLocationCode) {
|
|---|
| 551 | return `{fileName: ${this.getFilenameVarName()}, ${elementLocationCode}}`;
|
|---|
| 552 | }
|
|---|
| 553 |
|
|---|
| 554 | getFilenameVarName() {
|
|---|
| 555 | if (!this.filenameVarName) {
|
|---|
| 556 | this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName");
|
|---|
| 557 | }
|
|---|
| 558 | return this.filenameVarName;
|
|---|
| 559 | }
|
|---|
| 560 | } exports.default = JSXTransformer;
|
|---|
| 561 |
|
|---|
| 562 | /**
|
|---|
| 563 | * Spec for identifiers: https://tc39.github.io/ecma262/#prod-IdentifierStart.
|
|---|
| 564 | *
|
|---|
| 565 | * Really only treat anything starting with a-z as tag names. `_`, `$`, `é`
|
|---|
| 566 | * should be treated as component names
|
|---|
| 567 | */
|
|---|
| 568 | function startsWithLowerCase(s) {
|
|---|
| 569 | const firstChar = s.charCodeAt(0);
|
|---|
| 570 | return firstChar >= _charcodes.charCodes.lowercaseA && firstChar <= _charcodes.charCodes.lowercaseZ;
|
|---|
| 571 | } exports.startsWithLowerCase = startsWithLowerCase;
|
|---|
| 572 |
|
|---|
| 573 | /**
|
|---|
| 574 | * Turn the given jsxText string into a JS string literal. Leading and trailing
|
|---|
| 575 | * whitespace on lines is removed, except immediately after the open-tag and
|
|---|
| 576 | * before the close-tag. Empty lines are completely removed, and spaces are
|
|---|
| 577 | * added between lines after that.
|
|---|
| 578 | *
|
|---|
| 579 | * We use JSON.stringify to introduce escape characters as necessary, and trim
|
|---|
| 580 | * the start and end of each line and remove blank lines.
|
|---|
| 581 | */
|
|---|
| 582 | function formatJSXTextLiteral(text) {
|
|---|
| 583 | let result = "";
|
|---|
| 584 | let whitespace = "";
|
|---|
| 585 |
|
|---|
| 586 | let isInInitialLineWhitespace = false;
|
|---|
| 587 | let seenNonWhitespace = false;
|
|---|
| 588 | for (let i = 0; i < text.length; i++) {
|
|---|
| 589 | const c = text[i];
|
|---|
| 590 | if (c === " " || c === "\t" || c === "\r") {
|
|---|
| 591 | if (!isInInitialLineWhitespace) {
|
|---|
| 592 | whitespace += c;
|
|---|
| 593 | }
|
|---|
| 594 | } else if (c === "\n") {
|
|---|
| 595 | whitespace = "";
|
|---|
| 596 | isInInitialLineWhitespace = true;
|
|---|
| 597 | } else {
|
|---|
| 598 | if (seenNonWhitespace && isInInitialLineWhitespace) {
|
|---|
| 599 | result += " ";
|
|---|
| 600 | }
|
|---|
| 601 | result += whitespace;
|
|---|
| 602 | whitespace = "";
|
|---|
| 603 | if (c === "&") {
|
|---|
| 604 | const {entity, newI} = processEntity(text, i + 1);
|
|---|
| 605 | i = newI - 1;
|
|---|
| 606 | result += entity;
|
|---|
| 607 | } else {
|
|---|
| 608 | result += c;
|
|---|
| 609 | }
|
|---|
| 610 | seenNonWhitespace = true;
|
|---|
| 611 | isInInitialLineWhitespace = false;
|
|---|
| 612 | }
|
|---|
| 613 | }
|
|---|
| 614 | if (!isInInitialLineWhitespace) {
|
|---|
| 615 | result += whitespace;
|
|---|
| 616 | }
|
|---|
| 617 | return JSON.stringify(result);
|
|---|
| 618 | }
|
|---|
| 619 |
|
|---|
| 620 | /**
|
|---|
| 621 | * Produce the code that should be printed after the JSX text string literal,
|
|---|
| 622 | * with most content removed, but all newlines preserved and all spacing at the
|
|---|
| 623 | * end preserved.
|
|---|
| 624 | */
|
|---|
| 625 | function formatJSXTextReplacement(text) {
|
|---|
| 626 | let numNewlines = 0;
|
|---|
| 627 | let numSpaces = 0;
|
|---|
| 628 | for (const c of text) {
|
|---|
| 629 | if (c === "\n") {
|
|---|
| 630 | numNewlines++;
|
|---|
| 631 | numSpaces = 0;
|
|---|
| 632 | } else if (c === " ") {
|
|---|
| 633 | numSpaces++;
|
|---|
| 634 | }
|
|---|
| 635 | }
|
|---|
| 636 | return "\n".repeat(numNewlines) + " ".repeat(numSpaces);
|
|---|
| 637 | }
|
|---|
| 638 |
|
|---|
| 639 | /**
|
|---|
| 640 | * Format a string in the value position of a JSX prop.
|
|---|
| 641 | *
|
|---|
| 642 | * Use the same implementation as convertAttribute from
|
|---|
| 643 | * babel-helper-builder-react-jsx.
|
|---|
| 644 | */
|
|---|
| 645 | function formatJSXStringValueLiteral(text) {
|
|---|
| 646 | let result = "";
|
|---|
| 647 | for (let i = 0; i < text.length; i++) {
|
|---|
| 648 | const c = text[i];
|
|---|
| 649 | if (c === "\n") {
|
|---|
| 650 | if (/\s/.test(text[i + 1])) {
|
|---|
| 651 | result += " ";
|
|---|
| 652 | while (i < text.length && /\s/.test(text[i + 1])) {
|
|---|
| 653 | i++;
|
|---|
| 654 | }
|
|---|
| 655 | } else {
|
|---|
| 656 | result += "\n";
|
|---|
| 657 | }
|
|---|
| 658 | } else if (c === "&") {
|
|---|
| 659 | const {entity, newI} = processEntity(text, i + 1);
|
|---|
| 660 | result += entity;
|
|---|
| 661 | i = newI - 1;
|
|---|
| 662 | } else {
|
|---|
| 663 | result += c;
|
|---|
| 664 | }
|
|---|
| 665 | }
|
|---|
| 666 | return JSON.stringify(result);
|
|---|
| 667 | }
|
|---|
| 668 |
|
|---|
| 669 | /**
|
|---|
| 670 | * Starting at a &, see if there's an HTML entity (specified by name, decimal
|
|---|
| 671 | * char code, or hex char code) and return it if so.
|
|---|
| 672 | *
|
|---|
| 673 | * Modified from jsxReadString in babel-parser.
|
|---|
| 674 | */
|
|---|
| 675 | function processEntity(text, indexAfterAmpersand) {
|
|---|
| 676 | let str = "";
|
|---|
| 677 | let count = 0;
|
|---|
| 678 | let entity;
|
|---|
| 679 | let i = indexAfterAmpersand;
|
|---|
| 680 |
|
|---|
| 681 | if (text[i] === "#") {
|
|---|
| 682 | let radix = 10;
|
|---|
| 683 | i++;
|
|---|
| 684 | let numStart;
|
|---|
| 685 | if (text[i] === "x") {
|
|---|
| 686 | radix = 16;
|
|---|
| 687 | i++;
|
|---|
| 688 | numStart = i;
|
|---|
| 689 | while (i < text.length && isHexDigit(text.charCodeAt(i))) {
|
|---|
| 690 | i++;
|
|---|
| 691 | }
|
|---|
| 692 | } else {
|
|---|
| 693 | numStart = i;
|
|---|
| 694 | while (i < text.length && isDecimalDigit(text.charCodeAt(i))) {
|
|---|
| 695 | i++;
|
|---|
| 696 | }
|
|---|
| 697 | }
|
|---|
| 698 | if (text[i] === ";") {
|
|---|
| 699 | const numStr = text.slice(numStart, i);
|
|---|
| 700 | if (numStr) {
|
|---|
| 701 | i++;
|
|---|
| 702 | entity = String.fromCodePoint(parseInt(numStr, radix));
|
|---|
| 703 | }
|
|---|
| 704 | }
|
|---|
| 705 | } else {
|
|---|
| 706 | while (i < text.length && count++ < 10) {
|
|---|
| 707 | const ch = text[i];
|
|---|
| 708 | i++;
|
|---|
| 709 | if (ch === ";") {
|
|---|
| 710 | entity = _xhtml2.default.get(str);
|
|---|
| 711 | break;
|
|---|
| 712 | }
|
|---|
| 713 | str += ch;
|
|---|
| 714 | }
|
|---|
| 715 | }
|
|---|
| 716 |
|
|---|
| 717 | if (!entity) {
|
|---|
| 718 | return {entity: "&", newI: indexAfterAmpersand};
|
|---|
| 719 | }
|
|---|
| 720 | return {entity, newI: i};
|
|---|
| 721 | }
|
|---|
| 722 |
|
|---|
| 723 | function isDecimalDigit(code) {
|
|---|
| 724 | return code >= _charcodes.charCodes.digit0 && code <= _charcodes.charCodes.digit9;
|
|---|
| 725 | }
|
|---|
| 726 |
|
|---|
| 727 | function isHexDigit(code) {
|
|---|
| 728 | return (
|
|---|
| 729 | (code >= _charcodes.charCodes.digit0 && code <= _charcodes.charCodes.digit9) ||
|
|---|
| 730 | (code >= _charcodes.charCodes.lowercaseA && code <= _charcodes.charCodes.lowercaseF) ||
|
|---|
| 731 | (code >= _charcodes.charCodes.uppercaseA && code <= _charcodes.charCodes.uppercaseF)
|
|---|
| 732 | );
|
|---|
| 733 | }
|
|---|