@tsrx/core 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -185
- package/package.json +1 -1
- package/src/diagnostics.js +7 -0
- package/src/errors.js +3 -1
- package/src/index.js +1 -0
- package/src/parse/index.js +4 -2
- package/src/plugin.js +157 -112
- package/src/scope.js +2 -2
- package/src/transform/jsx/ast-builders.js +29 -0
- package/src/transform/jsx/index.js +819 -201
- package/src/utils/builders.js +68 -0
- package/types/index.d.ts +10 -3
- package/types/jsx-platform.d.ts +7 -3
- package/types/parse.d.ts +4 -2
package/src/plugin.js
CHANGED
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
} from './parse/index.js';
|
|
16
16
|
import { regex_newline_characters } from './utils/patterns.js';
|
|
17
17
|
import { error } from './errors.js';
|
|
18
|
+
import { DIAGNOSTIC_CODES } from './diagnostics.js';
|
|
19
|
+
|
|
20
|
+
const JSX_EXPRESSION_VALUE_ERROR =
|
|
21
|
+
'JSX elements cannot be used as expressions. Wrap with `<>...</>` or `<tsx>...</tsx>` or use elements as statements within a component.';
|
|
18
22
|
|
|
19
23
|
/** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
|
|
20
24
|
const argument_clash_first_positions = new WeakMap();
|
|
@@ -153,6 +157,35 @@ function looks_like_generic_arrow(input, pos) {
|
|
|
153
157
|
return input.charCodeAt(i) === 61 && input.charCodeAt(i + 1) === 62;
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
/**
|
|
161
|
+
* @param {AST.Node | null | undefined} node
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
function is_pascal_case_function(node) {
|
|
165
|
+
if (node && 'id' in node && node.id && node.id.type === 'Identifier') {
|
|
166
|
+
return /^[A-Z]/.test(node.id.name);
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} input
|
|
173
|
+
* @param {number} pos
|
|
174
|
+
*/
|
|
175
|
+
function previous_word_before(input, pos) {
|
|
176
|
+
let i = pos - 1;
|
|
177
|
+
while (i >= 0) {
|
|
178
|
+
const ch = input.charCodeAt(i);
|
|
179
|
+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) break;
|
|
180
|
+
i--;
|
|
181
|
+
}
|
|
182
|
+
const end = i + 1;
|
|
183
|
+
while (i >= 0 && /[$_\p{ID_Continue}]/u.test(input[i])) {
|
|
184
|
+
i--;
|
|
185
|
+
}
|
|
186
|
+
return input.slice(i + 1, end);
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
/**
|
|
157
190
|
* Acorn parser plugin for Ripple syntax extensions.
|
|
158
191
|
* Adds support for: component declarations, &[]/&{} lazy destructuring,
|
|
@@ -178,7 +211,10 @@ export function TSRXPlugin(config) {
|
|
|
178
211
|
#allowTagStartAfterDoubleQuotedText = false;
|
|
179
212
|
#allowDoubleQuotedTextChildAfterBrace = false;
|
|
180
213
|
#commentContextId = 0;
|
|
214
|
+
#collect = false;
|
|
181
215
|
#loose = false;
|
|
216
|
+
/** @type {AST.Node[]} */
|
|
217
|
+
#functionStack = [];
|
|
182
218
|
/** @type {import('../types/index').CompileError[] | undefined} */
|
|
183
219
|
#errors = undefined;
|
|
184
220
|
/** @type {string | null} */
|
|
@@ -192,6 +228,7 @@ export function TSRXPlugin(config) {
|
|
|
192
228
|
constructor(options, input) {
|
|
193
229
|
super(options, input);
|
|
194
230
|
const tsrx_options = options?.tsrxOptions ?? options?.rippleOptions;
|
|
231
|
+
this.#collect = tsrx_options?.collect === true || tsrx_options?.loose === true;
|
|
195
232
|
this.#loose = tsrx_options?.loose === true;
|
|
196
233
|
this.#errors = tsrx_options?.errors;
|
|
197
234
|
this.#filename = tsrx_options?.filename || null;
|
|
@@ -274,8 +311,9 @@ export function TSRXPlugin(config) {
|
|
|
274
311
|
* @param {number} position
|
|
275
312
|
* @param {number} end
|
|
276
313
|
* @param {string} message
|
|
314
|
+
* @param {string} [code]
|
|
277
315
|
*/
|
|
278
|
-
#report_recoverable_error_range(position, end, message) {
|
|
316
|
+
#report_recoverable_error_range(position, end, message, code) {
|
|
279
317
|
const start = Math.max(0, Math.min(position, this.input.length));
|
|
280
318
|
const range_end = Math.max(start, Math.min(end, this.input.length));
|
|
281
319
|
const start_loc = acorn.getLineInfo(this.input, start);
|
|
@@ -292,20 +330,37 @@ export function TSRXPlugin(config) {
|
|
|
292
330
|
end: end_loc,
|
|
293
331
|
},
|
|
294
332
|
}),
|
|
295
|
-
this.#
|
|
333
|
+
this.#collect ? this.#errors : undefined,
|
|
334
|
+
undefined,
|
|
335
|
+
code,
|
|
296
336
|
);
|
|
297
337
|
}
|
|
298
338
|
|
|
299
339
|
/**
|
|
300
340
|
* @param {number} position
|
|
301
341
|
* @param {string} message
|
|
342
|
+
* @param {string} [code]
|
|
343
|
+
*/
|
|
344
|
+
#report_recoverable_error(position, message, code) {
|
|
345
|
+
this.#report_recoverable_error_range(position, position + 1, message, code);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {number} position
|
|
350
|
+
* @param {string} message
|
|
351
|
+
* @param {string} [code]
|
|
302
352
|
*/
|
|
303
|
-
#
|
|
304
|
-
this.#
|
|
353
|
+
#report_broken_markup_error(position, message, code = DIAGNOSTIC_CODES.UNCLOSED_TAG) {
|
|
354
|
+
if (this.#loose) return;
|
|
355
|
+
if (this.#collect) {
|
|
356
|
+
this.#report_recoverable_error(position, message, code);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.raise(position, message);
|
|
305
360
|
}
|
|
306
361
|
|
|
307
362
|
/**
|
|
308
|
-
*
|
|
363
|
+
* When collecting, keep parsing after duplicate declaration diagnostics so
|
|
309
364
|
* editor tooling can continue producing AST and mappings.
|
|
310
365
|
* @param {number} position
|
|
311
366
|
* @param {string | { message?: string }} message
|
|
@@ -339,7 +394,7 @@ export function TSRXPlugin(config) {
|
|
|
339
394
|
*/
|
|
340
395
|
reportReservedArrowTypeParam(node) {
|
|
341
396
|
// Allow <T>() => {} syntax without requiring trailing comma
|
|
342
|
-
if (this.#
|
|
397
|
+
if (this.#collect && node.params.length === 1 && node.extra?.trailingComma === undefined) {
|
|
343
398
|
error(
|
|
344
399
|
'This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma, as in `<T,>() => ...`.',
|
|
345
400
|
this.#filename,
|
|
@@ -350,7 +405,7 @@ export function TSRXPlugin(config) {
|
|
|
350
405
|
}
|
|
351
406
|
|
|
352
407
|
/**
|
|
353
|
-
* Override to allow `readonly` type modifier on any type
|
|
408
|
+
* Override to allow `readonly` type modifier on any type when collecting.
|
|
354
409
|
* By default, @sveltejs/acorn-typescript throws an error for `readonly { ... }`
|
|
355
410
|
* because TypeScript only permits `readonly` on array and tuple types.
|
|
356
411
|
* Suppress the error in the strict mode as ts is compiled away.
|
|
@@ -363,7 +418,7 @@ export function TSRXPlugin(config) {
|
|
|
363
418
|
return;
|
|
364
419
|
}
|
|
365
420
|
|
|
366
|
-
if (this.#
|
|
421
|
+
if (this.#collect) {
|
|
367
422
|
error(
|
|
368
423
|
"'readonly' type modifier is only permitted on array and tuple literal types.",
|
|
369
424
|
this.#filename,
|
|
@@ -607,21 +662,17 @@ export function TSRXPlugin(config) {
|
|
|
607
662
|
}
|
|
608
663
|
|
|
609
664
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
* otherwise tokenize `<` as `jsxTagStart`
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
665
|
+
* `<T,>(x: T) => x` and `<T>(x: T): T => x` should parse as generic
|
|
666
|
+
* arrow functions, not JSX elements. acorn-typescript's `readToken`
|
|
667
|
+
* can otherwise tokenize `<` as `jsxTagStart` when expression parsing
|
|
668
|
+
* allows JSX, bypassing our `getTokenFromCode` override. We intercept
|
|
669
|
+
* only when the source from `<` actually looks like a generic arrow
|
|
670
|
+
* expression, so JSX like `<div>` keeps parsing normally.
|
|
616
671
|
*
|
|
617
672
|
* @type {Parse.Parser['readToken']}
|
|
618
673
|
*/
|
|
619
674
|
readToken(code) {
|
|
620
|
-
if (
|
|
621
|
-
code === 60 &&
|
|
622
|
-
this.#path.findLast((n) => n.type === 'Component') &&
|
|
623
|
-
looks_like_generic_arrow(this.input, this.pos)
|
|
624
|
-
) {
|
|
675
|
+
if (code === 60 && looks_like_generic_arrow(this.input, this.pos)) {
|
|
625
676
|
++this.pos;
|
|
626
677
|
return this.finishToken(tt.relational, '<');
|
|
627
678
|
}
|
|
@@ -854,15 +905,15 @@ export function TSRXPlugin(config) {
|
|
|
854
905
|
}
|
|
855
906
|
|
|
856
907
|
/**
|
|
857
|
-
* Acorn reports only the second duplicate function parameter.
|
|
858
|
-
*
|
|
908
|
+
* Acorn reports only the second duplicate function parameter. When collecting,
|
|
909
|
+
* report the first one too so editor diagnostics can underline both
|
|
859
910
|
* binding sites. Keep strict mode on Acorn's normal fatal path.
|
|
860
911
|
*
|
|
861
912
|
* @type {Parse.Parser['checkLValSimple']}
|
|
862
913
|
*/
|
|
863
914
|
checkLValSimple(expr, bindingType = BINDING_TYPES.BIND_NONE, checkClashes) {
|
|
864
915
|
if (
|
|
865
|
-
this.#
|
|
916
|
+
this.#collect &&
|
|
866
917
|
expr.type === 'Identifier' &&
|
|
867
918
|
bindingType !== BINDING_TYPES.BIND_NONE &&
|
|
868
919
|
checkClashes
|
|
@@ -1298,10 +1349,12 @@ export function TSRXPlugin(config) {
|
|
|
1298
1349
|
*/
|
|
1299
1350
|
parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
|
|
1300
1351
|
this.#functionBodyDepth++;
|
|
1352
|
+
this.#functionStack.push(node);
|
|
1301
1353
|
|
|
1302
1354
|
try {
|
|
1303
1355
|
return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
|
|
1304
1356
|
} finally {
|
|
1357
|
+
this.#functionStack.pop();
|
|
1305
1358
|
this.#functionBodyDepth--;
|
|
1306
1359
|
}
|
|
1307
1360
|
}
|
|
@@ -1849,10 +1902,16 @@ export function TSRXPlugin(config) {
|
|
|
1849
1902
|
);
|
|
1850
1903
|
}
|
|
1851
1904
|
|
|
1852
|
-
this.
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1905
|
+
const code = this.#functionStack.findLast(is_pascal_case_function)
|
|
1906
|
+
? DIAGNOSTIC_CODES.FUNCTION_COMPONENT_SYNTAX
|
|
1907
|
+
: this.#path.findLast((node) => node.type === 'Component') &&
|
|
1908
|
+
this.#functionStack.length === 0 &&
|
|
1909
|
+
previous_word_before(this.input, this.start) === 'return'
|
|
1910
|
+
? DIAGNOSTIC_CODES.JSX_RETURN_IN_COMPONENT
|
|
1911
|
+
: DIAGNOSTIC_CODES.JSX_EXPRESSION_VALUE;
|
|
1912
|
+
|
|
1913
|
+
this.#report_recoverable_error(this.start, JSX_EXPRESSION_VALUE_ERROR, code);
|
|
1914
|
+
return super.jsx_parseElement();
|
|
1856
1915
|
}
|
|
1857
1916
|
|
|
1858
1917
|
/**
|
|
@@ -2051,12 +2110,10 @@ export function TSRXPlugin(config) {
|
|
|
2051
2110
|
this.#path.pop();
|
|
2052
2111
|
} else {
|
|
2053
2112
|
// No closing tag
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
);
|
|
2059
|
-
}
|
|
2113
|
+
this.#report_broken_markup_error(
|
|
2114
|
+
open.end,
|
|
2115
|
+
"Unclosed tag '<script>'. Expected '</script>' before end of component.",
|
|
2116
|
+
);
|
|
2060
2117
|
/** @type {AST.Element} */ (element).unclosed = true;
|
|
2061
2118
|
this.#path.pop();
|
|
2062
2119
|
}
|
|
@@ -2113,12 +2170,10 @@ export function TSRXPlugin(config) {
|
|
|
2113
2170
|
this.exprAllowed = false;
|
|
2114
2171
|
this.#path.pop();
|
|
2115
2172
|
} else {
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
);
|
|
2121
|
-
}
|
|
2173
|
+
this.#report_broken_markup_error(
|
|
2174
|
+
open.end,
|
|
2175
|
+
"Unclosed tag '<style>'. Expected '</style>' before end of component.",
|
|
2176
|
+
);
|
|
2122
2177
|
/** @type {AST.Element} */ (element).unclosed = true;
|
|
2123
2178
|
this.#path.pop();
|
|
2124
2179
|
}
|
|
@@ -2203,20 +2258,17 @@ export function TSRXPlugin(config) {
|
|
|
2203
2258
|
}
|
|
2204
2259
|
} else if (this.#path[this.#path.length - 1] === element) {
|
|
2205
2260
|
// Check if this element was properly closed
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
this.
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
element.end = element.openingElement.end;
|
|
2218
|
-
this.#path.pop();
|
|
2219
|
-
}
|
|
2261
|
+
const tagName = this.getElementName(element.id);
|
|
2262
|
+
this.#report_broken_markup_error(
|
|
2263
|
+
this.start,
|
|
2264
|
+
`Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
|
|
2265
|
+
);
|
|
2266
|
+
element.unclosed = true;
|
|
2267
|
+
element.loc.end = {
|
|
2268
|
+
.../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
|
|
2269
|
+
};
|
|
2270
|
+
element.end = element.openingElement.end;
|
|
2271
|
+
this.#path.pop();
|
|
2220
2272
|
}
|
|
2221
2273
|
}
|
|
2222
2274
|
|
|
@@ -2267,18 +2319,15 @@ export function TSRXPlugin(config) {
|
|
|
2267
2319
|
|
|
2268
2320
|
while (true) {
|
|
2269
2321
|
if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
|
|
2270
|
-
|
|
2271
|
-
this.
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
};
|
|
2280
|
-
inside_tsx.end = inside_tsx.openingElement.end;
|
|
2281
|
-
}
|
|
2322
|
+
this.#report_broken_markup_error(
|
|
2323
|
+
this.start,
|
|
2324
|
+
`Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
|
|
2325
|
+
);
|
|
2326
|
+
inside_tsx.unclosed = true;
|
|
2327
|
+
/** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
|
|
2328
|
+
.../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
|
|
2329
|
+
};
|
|
2330
|
+
inside_tsx.end = inside_tsx.openingElement.end;
|
|
2282
2331
|
return;
|
|
2283
2332
|
}
|
|
2284
2333
|
|
|
@@ -2344,18 +2393,15 @@ export function TSRXPlugin(config) {
|
|
|
2344
2393
|
|
|
2345
2394
|
while (true) {
|
|
2346
2395
|
if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
|
|
2347
|
-
|
|
2348
|
-
this.
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
};
|
|
2357
|
-
inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
|
|
2358
|
-
}
|
|
2396
|
+
this.#report_broken_markup_error(
|
|
2397
|
+
this.start,
|
|
2398
|
+
`Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
|
|
2399
|
+
);
|
|
2400
|
+
inside_tsx_compat.unclosed = true;
|
|
2401
|
+
/** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
|
|
2402
|
+
.../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
|
|
2403
|
+
};
|
|
2404
|
+
inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
|
|
2359
2405
|
return;
|
|
2360
2406
|
}
|
|
2361
2407
|
|
|
@@ -2480,46 +2526,45 @@ export function TSRXPlugin(config) {
|
|
|
2480
2526
|
}
|
|
2481
2527
|
|
|
2482
2528
|
if (openingTagName !== closingTagName) {
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2529
|
+
// this will throw if not collecting errors
|
|
2530
|
+
this.#report_broken_markup_error(
|
|
2531
|
+
closingElement.start,
|
|
2532
|
+
`Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
|
|
2533
|
+
DIAGNOSTIC_CODES.MISMATCHED_CLOSING_TAG,
|
|
2534
|
+
);
|
|
2535
|
+
// Loop through all unclosed elements on the stack
|
|
2536
|
+
while (this.#path.length > 0) {
|
|
2537
|
+
const elem = this.#path[this.#path.length - 1];
|
|
2492
2538
|
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2539
|
+
// Stop at non-Element boundaries (Component, etc.)
|
|
2540
|
+
if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2497
2543
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2544
|
+
const elemName =
|
|
2545
|
+
elem.type === 'TsxCompat'
|
|
2546
|
+
? 'tsx:' + elem.kind
|
|
2547
|
+
: elem.type === 'Tsx'
|
|
2548
|
+
? elem.openingElement.name
|
|
2549
|
+
? 'tsx'
|
|
2550
|
+
: null
|
|
2551
|
+
: elem.id
|
|
2552
|
+
? this.getElementName(elem.id)
|
|
2553
|
+
: null;
|
|
2554
|
+
|
|
2555
|
+
// Found matching opening tag
|
|
2556
|
+
if (elemName === closingTagName) {
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2513
2559
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2560
|
+
// Mark as unclosed and adjust location
|
|
2561
|
+
elem.unclosed = true;
|
|
2562
|
+
/** @type {AST.NodeWithLocation} */ (elem).loc.end = {
|
|
2563
|
+
.../** @type {AST.SourceLocation} */ (elem.openingElement.loc).end,
|
|
2564
|
+
};
|
|
2565
|
+
elem.end = elem.openingElement.end;
|
|
2520
2566
|
|
|
2521
|
-
|
|
2522
|
-
}
|
|
2567
|
+
this.#path.pop(); // Remove from stack
|
|
2523
2568
|
}
|
|
2524
2569
|
}
|
|
2525
2570
|
|
package/src/scope.js
CHANGED
|
@@ -335,7 +335,7 @@ export class Scope {
|
|
|
335
335
|
`Cannot declare a variable named "${node.name}" as identifiers starting with "${IDENTIFIER_OBFUSCATION_PREFIX}" are reserved`,
|
|
336
336
|
this.#error_options.filename,
|
|
337
337
|
node,
|
|
338
|
-
this.#error_options.
|
|
338
|
+
this.#error_options.collect ? this.#error_options.errors : undefined,
|
|
339
339
|
this.#error_options.comments,
|
|
340
340
|
);
|
|
341
341
|
}
|
|
@@ -345,7 +345,7 @@ export class Scope {
|
|
|
345
345
|
`'${node.name}' has already been declared in the current scope`,
|
|
346
346
|
this.#error_options.filename,
|
|
347
347
|
node,
|
|
348
|
-
this.#error_options.
|
|
348
|
+
this.#error_options.collect ? this.#error_options.errors : undefined,
|
|
349
349
|
this.#error_options.comments,
|
|
350
350
|
);
|
|
351
351
|
}
|
|
@@ -250,17 +250,46 @@ export function flatten_switch_consequent(consequent) {
|
|
|
250
250
|
return result;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* @param {AST.Expression | null | undefined} expression
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
function is_static_string_expression(expression) {
|
|
258
|
+
if (!expression) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (expression.type === 'Literal') {
|
|
262
|
+
return typeof expression.value === 'string';
|
|
263
|
+
}
|
|
264
|
+
if (expression.type === 'TemplateLiteral') {
|
|
265
|
+
return expression.expressions.length === 0;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
253
270
|
/**
|
|
254
271
|
* Build `expr == null ? '' : expr + ''` — the text-coerce form used when a
|
|
255
272
|
* Ripple `{expr}` child must render as a string in JSX (React/Preact drop
|
|
256
273
|
* booleans; Solid's default child semantics don't either). Solid uses this
|
|
257
274
|
* via `to_jsx_child`; React/Preact wrap it in a JSXExpressionContainer.
|
|
258
275
|
*
|
|
276
|
+
* When the expression is statically a non-null string at the AST level —
|
|
277
|
+
* a string `Literal` (`"hello"`, `'hello'`) or a `TemplateLiteral` with no
|
|
278
|
+
* interpolations (`` `hello` ``) — the coercion is provably a no-op and
|
|
279
|
+
* the literal is emitted as-is. This covers both direct double-quoted
|
|
280
|
+
* children (`<b>"hello"</b>`) and inline literal arguments to the explicit
|
|
281
|
+
* `{text ...}` intrinsic (`<b>{text 'hello'}</b>`). Identifiers and any
|
|
282
|
+
* other expression type still get the ternary because the AST alone can't
|
|
283
|
+
* prove they're non-null strings.
|
|
284
|
+
*
|
|
259
285
|
* @param {AST.Expression} expression
|
|
260
286
|
* @param {any} [source_node]
|
|
261
287
|
* @returns {AST.Expression}
|
|
262
288
|
*/
|
|
263
289
|
export function to_text_expression(expression, source_node = expression) {
|
|
290
|
+
if (is_static_string_expression(expression)) {
|
|
291
|
+
return set_loc(clone_expression_node(expression), source_node);
|
|
292
|
+
}
|
|
264
293
|
return set_loc(
|
|
265
294
|
/** @type {AST.Expression} */ ({
|
|
266
295
|
type: 'ConditionalExpression',
|