@barefootjs/jsx 0.5.2 → 0.6.0
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/dist/adapters/parsed-expr-emitter.d.ts +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/combine-client-js.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +330 -70
- package/dist/ir-to-client-js/collect-elements.d.ts +26 -14
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +8 -3
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +30 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +3 -3
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +36 -4
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +19 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +203 -203
- package/src/__tests__/child-components-in-map.test.ts +333 -0
- package/src/__tests__/combine-client-js.test.ts +47 -0
- package/src/__tests__/dangerously-set-inner-html.test.ts +82 -0
- package/src/__tests__/expression-parser.test.ts +167 -13
- package/src/__tests__/ir-to-client-js/reactivity.test.ts +1 -0
- package/src/__tests__/staged-ir/06-multi-stage-soak.test.ts +18 -3
- package/src/__tests__/static-loop-csr-materialize.test.ts +6 -4
- package/src/__tests__/text-slot-escaping.test.ts +56 -0
- package/src/adapters/parsed-expr-emitter.ts +7 -0
- package/src/combine-client-js.ts +66 -22
- package/src/expression-parser.ts +200 -17
- package/src/ir-to-client-js/collect-elements.ts +170 -32
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +8 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +3 -3
- package/src/ir-to-client-js/emit-reactive.ts +9 -0
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/generate-init.ts +16 -1
- package/src/ir-to-client-js/html-template.ts +238 -12
- package/src/ir-to-client-js/imports.ts +1 -1
- package/src/ir-to-client-js/index.ts +1 -0
- package/src/ir-to-client-js/phases/provider-and-child-inits.ts +12 -1
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +4 -8
- package/src/ir-to-client-js/plan/static-array-child-init.ts +3 -3
- package/src/ir-to-client-js/types.ts +37 -4
- package/src/ir-to-client-js/utils.ts +41 -1
package/src/expression-parser.ts
CHANGED
|
@@ -46,6 +46,13 @@ export type ParsedExpr =
|
|
|
46
46
|
| 'toLowerCase'
|
|
47
47
|
| 'toUpperCase'
|
|
48
48
|
| 'trim'
|
|
49
|
+
| 'split'
|
|
50
|
+
| 'startsWith'
|
|
51
|
+
| 'endsWith'
|
|
52
|
+
| 'replace'
|
|
53
|
+
| 'repeat'
|
|
54
|
+
| 'padStart'
|
|
55
|
+
| 'padEnd'
|
|
49
56
|
object: ParsedExpr
|
|
50
57
|
args: ParsedExpr[]
|
|
51
58
|
}
|
|
@@ -225,12 +232,46 @@ const UNSUPPORTED_METHODS = new Set([
|
|
|
225
232
|
// unsupported array methods above get), pointing users at the
|
|
226
233
|
// `/* @client */` escape hatch. Each name drops off as its lowering
|
|
227
234
|
// lands. See #1448 "Unsupported string methods" Tier B / Tier C.
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
// `split` is no longer here — `String.prototype.split(sep)` lowers
|
|
236
|
+
// via the `array-method` IR + `bf_split` (Go) / `bf->split` (Mojo),
|
|
237
|
+
// returning an array that composes with `.join()` / `.map()` / etc.
|
|
238
|
+
// See #1448 Tier B.
|
|
239
|
+
// `startsWith` / `endsWith` are no longer here — both lower via the
|
|
240
|
+
// `array-method` IR + `bf_starts_with` / `bf_ends_with` (Go) and
|
|
241
|
+
// `bf->starts_with` / `bf->ends_with` (Mojo). See #1448 Tier B.
|
|
242
|
+
// `replace` is no longer here — the string-pattern form lowers via
|
|
243
|
+
// the `array-method` IR + `bf_replace` (Go) / `bf->replace` (Mojo);
|
|
244
|
+
// the regex-pattern form is refused at the parse arm below (it would
|
|
245
|
+
// need the per-adapter regex-flavour decision). `replaceAll` stays
|
|
246
|
+
// refused. See #1448 Tier B.
|
|
247
|
+
// `repeat` is no longer here — `String.prototype.repeat(n)` lowers via
|
|
248
|
+
// the `array-method` IR + `bf_repeat` (Go) / `bf->repeat` (Mojo).
|
|
249
|
+
// See #1448 Tier B.
|
|
250
|
+
// `padStart` / `padEnd` are no longer here — both lower via the
|
|
251
|
+
// `array-method` IR + `bf_pad_start` / `bf_pad_end` (Go) and
|
|
252
|
+
// `bf->pad_start` / `bf->pad_end` (Mojo). See #1448 Tier B.
|
|
253
|
+
'replaceAll',
|
|
230
254
|
'charAt', 'charCodeAt', 'codePointAt', 'normalize',
|
|
231
255
|
'substring', 'substr', 'match', 'matchAll', 'search',
|
|
232
256
|
])
|
|
233
257
|
|
|
258
|
+
// Methods that lower at their single-argument form but whose EXTRA
|
|
259
|
+
// argument is meaningful and NOT yet lowered: the `fromIndex` of
|
|
260
|
+
// `.includes` / `.indexOf` / `.lastIndexOf` (the 2-arg form) and the
|
|
261
|
+
// additional arrays of a variadic `.concat(a, b, …)`. The relaxed
|
|
262
|
+
// per-method arms in `convertNode` accept every method's zero-arg
|
|
263
|
+
// defaults (`.join()` / `.slice()` / `.concat()` / `.at()`) and
|
|
264
|
+
// JS-ignored trailing arguments; this guard catches only the remaining
|
|
265
|
+
// meaningful-extra forms, refusing them with BF101 because silently
|
|
266
|
+
// dropping the argument would make the SSR output differ from the
|
|
267
|
+
// client. See #1448.
|
|
268
|
+
const LOWERED_ARRAY_METHODS = new Set([
|
|
269
|
+
'includes',
|
|
270
|
+
'indexOf',
|
|
271
|
+
'lastIndexOf',
|
|
272
|
+
'concat',
|
|
273
|
+
])
|
|
274
|
+
|
|
234
275
|
// =============================================================================
|
|
235
276
|
// Expression Parser
|
|
236
277
|
// =============================================================================
|
|
@@ -344,7 +385,10 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
344
385
|
// etc. later means widening the IR discriminator, not adding more
|
|
345
386
|
// branches to every adapter's call dispatch.
|
|
346
387
|
if (callee.kind === 'member' && !callee.computed) {
|
|
347
|
-
|
|
388
|
+
// `.join()` / `.join(sep)` — JS defaults the separator to `,` when
|
|
389
|
+
// omitted and ignores any extra arguments. Accept every arity; the
|
|
390
|
+
// adapters supply the default separator and read only `args[0]`.
|
|
391
|
+
if (callee.property === 'join') {
|
|
348
392
|
return { kind: 'array-method', method: 'join', object: callee.object, args }
|
|
349
393
|
}
|
|
350
394
|
// `.includes(x)` — shared between `Array.prototype.includes` and
|
|
@@ -369,20 +413,28 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
369
413
|
// element). Go has `bf_at` registered already (see runtime
|
|
370
414
|
// FuncMap); Mojo's `bf->at` wraps the same arithmetic.
|
|
371
415
|
// See #1448 Tier A.
|
|
372
|
-
|
|
416
|
+
// `.at(i)` — JS ignores any argument past the first, and `.at()`
|
|
417
|
+
// with no argument is `.at(0)` (the first element). Accept every
|
|
418
|
+
// arity; the adapters read `args[0]` (defaulting the index to 0).
|
|
419
|
+
if (callee.property === 'at') {
|
|
373
420
|
return { kind: 'array-method', method: 'at', object: callee.object, args }
|
|
374
421
|
}
|
|
375
|
-
// `.concat(other)` —
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
422
|
+
// `.concat()` / `.concat(other)` — `.concat()` returns a shallow
|
|
423
|
+
// copy (indistinguishable from the receiver in an SSR snapshot),
|
|
424
|
+
// and `.concat(other)` merges the two arrays. Go uses `bf_concat`
|
|
425
|
+
// (reflect-based append into `[]any`); Mojo uses `bf->concat`
|
|
426
|
+
// (Perl list builder). The VARIADIC form (`.concat(a, b, …)`) is
|
|
427
|
+
// not lowered yet — it's refused by the guard below rather than
|
|
428
|
+
// silently dropping the extra arrays.
|
|
429
|
+
if (callee.property === 'concat' && args.length <= 1) {
|
|
380
430
|
return { kind: 'array-method', method: 'concat', object: callee.object, args }
|
|
381
431
|
}
|
|
382
|
-
// `.slice(start)` / `.slice(start, end)` —
|
|
383
|
-
// through `bf_slice` (Go) / `bf->slice` (Mojo)
|
|
384
|
-
//
|
|
385
|
-
|
|
432
|
+
// `.slice()` / `.slice(start)` / `.slice(start, end)` — route
|
|
433
|
+
// through `bf_slice` (Go) / `bf->slice` (Mojo). A missing `start`
|
|
434
|
+
// defaults to 0 (full copy), a missing / undef `end` means "to
|
|
435
|
+
// length", and JS ignores any third+ argument. Accept every arity;
|
|
436
|
+
// the adapters read only `args[0]` / `args[1]`.
|
|
437
|
+
if (callee.property === 'slice') {
|
|
386
438
|
return { kind: 'array-method', method: 'slice', object: callee.object, args }
|
|
387
439
|
}
|
|
388
440
|
// `.reverse()` and `.toReversed()` — both zero-arg shapes
|
|
@@ -390,7 +442,8 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
390
442
|
// of state, so JS's mutate-and-return-receiver (`reverse`)
|
|
391
443
|
// vs return-new-array (`toReversed`) distinction has no
|
|
392
444
|
// template-level meaning; both produce a new reversed array.
|
|
393
|
-
|
|
445
|
+
// JS takes no argument and ignores any that are passed.
|
|
446
|
+
if (callee.property === 'reverse' || callee.property === 'toReversed') {
|
|
394
447
|
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
395
448
|
}
|
|
396
449
|
// `.toLowerCase()` — string-only (the IR carries a value-builtin
|
|
@@ -398,20 +451,150 @@ function convertNode(node: ts.Node, raw: string): ParsedExpr {
|
|
|
398
451
|
// label is a misnomer for string methods but the mechanical
|
|
399
452
|
// pipeline matches). Go uses the existing `bf_lower` helper;
|
|
400
453
|
// Mojo uses Perl's native `lc`. See #1448 Tier A.
|
|
401
|
-
if (callee.property === 'toLowerCase'
|
|
454
|
+
if (callee.property === 'toLowerCase') {
|
|
402
455
|
return { kind: 'array-method', method: 'toLowerCase', object: callee.object, args }
|
|
403
456
|
}
|
|
404
457
|
// `.toUpperCase()` — Go uses the existing `bf_upper` helper;
|
|
405
458
|
// Mojo uses Perl's native `uc`.
|
|
406
|
-
if (callee.property === 'toUpperCase'
|
|
459
|
+
if (callee.property === 'toUpperCase') {
|
|
407
460
|
return { kind: 'array-method', method: 'toUpperCase', object: callee.object, args }
|
|
408
461
|
}
|
|
409
462
|
// `.trim()` — Go uses the existing `bf_trim` helper; Mojo uses
|
|
410
463
|
// a new `bf->trim` method that mirrors JS's "strip leading +
|
|
411
464
|
// trailing whitespace" semantic via a Perl regex.
|
|
412
|
-
if (callee.property === 'trim'
|
|
465
|
+
if (callee.property === 'trim') {
|
|
413
466
|
return { kind: 'array-method', method: 'trim', object: callee.object, args }
|
|
414
467
|
}
|
|
468
|
+
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
|
|
469
|
+
// array, full JS arity. `.split()` (no separator) returns the
|
|
470
|
+
// whole string as a single element; `.split(sep)` splits on the
|
|
471
|
+
// (literal) separator; the optional `limit` caps the number of
|
|
472
|
+
// pieces. JS ignores a third+ argument. Go uses `bf_split`
|
|
473
|
+
// (`strings.Split`, optional limit, normalised to `[]any`) and
|
|
474
|
+
// `bf_arr` for the no-separator whole-string case; Mojo uses
|
|
475
|
+
// `bf->split`. The regex-separator form stays refused (the parser
|
|
476
|
+
// never reaches here for it — a regex literal arg is `unsupported`
|
|
477
|
+
// and propagates). See #1448 Tier B.
|
|
478
|
+
if (callee.property === 'split') {
|
|
479
|
+
return { kind: 'array-method', method: 'split', object: callee.object, args }
|
|
480
|
+
}
|
|
481
|
+
// Arity guard for the forms whose EXTRA argument changes the
|
|
482
|
+
// result and is not yet lowered: the `fromIndex` of `.includes` /
|
|
483
|
+
// `.indexOf` / `.lastIndexOf` (the 2-arg form), and the additional
|
|
484
|
+
// arrays of a variadic `.concat(a, b, …)`. Silently dropping those
|
|
485
|
+
// would make the SSR output *differ* from the client (worse than a
|
|
486
|
+
// build error), so they refuse with BF101 until lowered. (The
|
|
487
|
+
// single-argument forms, zero-arg defaults, and JS-ignored
|
|
488
|
+
// trailing arguments of every method are accepted by the relaxed
|
|
489
|
+
// arms above.) See #1448.
|
|
490
|
+
if (LOWERED_ARRAY_METHODS.has(callee.property)) {
|
|
491
|
+
const argName = callee.property === 'concat' ? 'other' : 'x'
|
|
492
|
+
const detail =
|
|
493
|
+
callee.property === 'concat'
|
|
494
|
+
? 'the variadic `.concat(a, b, …)` form'
|
|
495
|
+
: `\`.${callee.property}(…)\` with ${args.length} argument(s)`
|
|
496
|
+
return {
|
|
497
|
+
kind: 'unsupported',
|
|
498
|
+
raw,
|
|
499
|
+
reason: `${detail} is not yet lowered to the Go/Mojo template adapters. Use the single-argument \`.${callee.property}(${argName})\` form, or pre-compute the value before the template.`,
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// `.startsWith(search, position?)` / `.endsWith(search, endPosition?)`
|
|
503
|
+
// — string → boolean, full JS arity. Go uses `bf_starts_with` /
|
|
504
|
+
// `bf_ends_with` (wrapping `strings.HasPrefix` / `strings.HasSuffix`,
|
|
505
|
+
// with an optional position that re-anchors the comparison); Mojo
|
|
506
|
+
// uses `bf->starts_with` / `bf->ends_with` (substr comparison). JS
|
|
507
|
+
// ignores a third+ argument. The zero-arg form (`.startsWith()`) is
|
|
508
|
+
// refused: JS coerces the missing search to the literal string
|
|
509
|
+
// "undefined", a degenerate result not worth lowering (mirrors the
|
|
510
|
+
// `.includes()` zero-arg refusal). See #1448 Tier B.
|
|
511
|
+
if (callee.property === 'startsWith' || callee.property === 'endsWith') {
|
|
512
|
+
if (args.length === 0) {
|
|
513
|
+
return {
|
|
514
|
+
kind: 'unsupported',
|
|
515
|
+
raw,
|
|
516
|
+
reason: `\`.${callee.property}()\` with no search string is not lowered — JS coerces the missing argument to the string "undefined", a degenerate result. Pass an explicit search string, or pre-compute the value before the template.`,
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
520
|
+
}
|
|
521
|
+
// `.replace(pattern, replacement)` — string-pattern form,
|
|
522
|
+
// replacing the FIRST occurrence (JS semantics for a string
|
|
523
|
+
// pattern). Go uses `bf_replace` (`strings.Replace` with n=1);
|
|
524
|
+
// Mojo uses `bf->replace` (index/substr splice, no regex). A
|
|
525
|
+
// regex-literal pattern parses as `unsupported` (convertNode has
|
|
526
|
+
// no regex arm), so it's refused explicitly here rather than
|
|
527
|
+
// emitting a broken `.Replace` — the Perl `s///` vs Go
|
|
528
|
+
// `regexp.ReplaceAllString` flavour gap is the open design
|
|
529
|
+
// question in #1448. `replaceAll` stays refused entirely.
|
|
530
|
+
//
|
|
531
|
+
// Full JS arity: a third+ argument is ignored (the adapter reads
|
|
532
|
+
// only the pattern + replacement). The one- and zero-argument
|
|
533
|
+
// forms are refused: JS coerces the missing replacement (and
|
|
534
|
+
// pattern) to the literal string "undefined", a degenerate result
|
|
535
|
+
// (mirrors the `.includes()` / `.startsWith()` zero-arg refusal).
|
|
536
|
+
if (callee.property === 'replace') {
|
|
537
|
+
if (args.length < 2) {
|
|
538
|
+
return {
|
|
539
|
+
kind: 'unsupported',
|
|
540
|
+
raw,
|
|
541
|
+
reason: `\`.replace(${args.length === 0 ? '' : 'pattern'})\` needs both a pattern and a replacement — JS coerces the missing argument to the string "undefined", a degenerate result. Pass both arguments, or pre-compute the value before the template.`,
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// A regex-literal pattern is the deferred form (the Perl `s///`
|
|
545
|
+
// vs Go `regexp.ReplaceAllString` flavour gap, #1448) — detect it
|
|
546
|
+
// on the TS node so the message is accurate. Any OTHER unsupported
|
|
547
|
+
// pattern/replacement (an object literal, an unsupported call, …)
|
|
548
|
+
// surfaces ITS OWN reason rather than being mislabelled as the
|
|
549
|
+
// regex form.
|
|
550
|
+
const patternNode = node.arguments[0]
|
|
551
|
+
if (patternNode && ts.isRegularExpressionLiteral(patternNode)) {
|
|
552
|
+
return {
|
|
553
|
+
kind: 'unsupported',
|
|
554
|
+
raw,
|
|
555
|
+
reason:
|
|
556
|
+
'String.prototype.replace supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */',
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const badArg =
|
|
560
|
+
args[0].kind === 'unsupported'
|
|
561
|
+
? args[0]
|
|
562
|
+
: args[1].kind === 'unsupported'
|
|
563
|
+
? args[1]
|
|
564
|
+
: undefined
|
|
565
|
+
if (badArg && badArg.kind === 'unsupported') {
|
|
566
|
+
return { kind: 'unsupported', raw, reason: badArg.reason }
|
|
567
|
+
}
|
|
568
|
+
return { kind: 'array-method', method: 'replace', object: callee.object, args }
|
|
569
|
+
}
|
|
570
|
+
// `.repeat(n)` — string → string (the receiver concatenated `n`
|
|
571
|
+
// times). Go uses `bf_repeat` (`strings.Repeat`, clamping a
|
|
572
|
+
// negative count to "" instead of panicking); Mojo uses
|
|
573
|
+
// `bf->repeat` (Perl's `x` operator). JS throws RangeError for a
|
|
574
|
+
// negative count, but SSR templates degrade to the empty string
|
|
575
|
+
// rather than crashing the render. See #1448 Tier B.
|
|
576
|
+
// Full JS arity: `.repeat()` (no count) is `repeat(0)` → "" (JS
|
|
577
|
+
// coerces the missing count to 0, not a RangeError), and a
|
|
578
|
+
// second+ argument is ignored. The adapter supplies the `0` for
|
|
579
|
+
// the no-argument form. See #1448 Tier B.
|
|
580
|
+
if (callee.property === 'repeat') {
|
|
581
|
+
return { kind: 'array-method', method: 'repeat', object: callee.object, args }
|
|
582
|
+
}
|
|
583
|
+
// `.padStart(target, pad?)` / `.padEnd(target, pad?)` — string →
|
|
584
|
+
// string, padded to `target` length with `pad` (default a single
|
|
585
|
+
// space) repeated + truncated to fill. Go uses `bf_pad_start` /
|
|
586
|
+
// `bf_pad_end`; Mojo uses `bf->pad_start` / `bf->pad_end`. Both
|
|
587
|
+
// count length in code points (Go runes / Perl chars) so they
|
|
588
|
+
// stay byte-equal — this differs from JS's UTF-16-unit length
|
|
589
|
+
// only for astral-plane receivers, which are vanishingly rare in
|
|
590
|
+
// numeric / space padding. See #1448 Tier B.
|
|
591
|
+
// Full JS arity: `.padStart()` (no target) is `padStart(0)` → the
|
|
592
|
+
// receiver unchanged (JS coerces the missing target to 0), and a
|
|
593
|
+
// third+ argument is ignored. The adapter supplies the `0` for the
|
|
594
|
+
// no-argument form and reads only target + padString.
|
|
595
|
+
if (callee.property === 'padStart' || callee.property === 'padEnd') {
|
|
596
|
+
return { kind: 'array-method', method: callee.property, object: callee.object, args }
|
|
597
|
+
}
|
|
415
598
|
// `.sort(cmp)` / `.toSorted(cmp)` (#1448 Tier B). The comparator
|
|
416
599
|
// is extracted into a structured `SortComparator` at parse time;
|
|
417
600
|
// unrecognised shapes fall through to `unsupported` so adapters
|
|
@@ -3,55 +3,170 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp, pickAttrMetaFromIR } from '../types'
|
|
6
|
-
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, NestedLoop } from './types'
|
|
6
|
+
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types'
|
|
7
7
|
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils'
|
|
8
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity'
|
|
9
9
|
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr } from './html-template'
|
|
10
10
|
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling'
|
|
11
11
|
import { walkIR, stopAt } from './walker'
|
|
12
|
+
import { buildLoopChainExpr } from '../loop-chain'
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
14
|
+
/** Expressions that render nothing (0 DOM nodes) — `&&` / `?:` empty branches. */
|
|
15
|
+
const EMPTY_RENDER_EXPRS = new Set(['null', 'undefined', 'false', "''", '""', '``'])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Number of *element* children a node contributes to its parent's `.children`
|
|
19
|
+
* run — the collection that `container.children[idx]` indexes and that event
|
|
20
|
+
* delegation's `Array.from(container.children).indexOf(...)` walks. `.children`
|
|
21
|
+
* is element-only, so text / comment nodes never count.
|
|
22
|
+
*
|
|
23
|
+
* Returns a folded integer when the count is statically known, a JS expression
|
|
24
|
+
* string when it depends on runtime state, or `null` when the element count is
|
|
25
|
+
* statically undecidable (the caller then falls back to the legacy count):
|
|
26
|
+
* - element / component / provider / async → `1` (one root element)
|
|
27
|
+
* - text / empty-render expression (`null`/`false`/…) → `0`
|
|
28
|
+
* - plain loop → `(arr).length`; per-item-conditional / flatMap loop → `null`
|
|
29
|
+
* (renders a runtime-variable count, not `array.length`) (#1693)
|
|
30
|
+
* - conditional → fold to a number when both branches match, else
|
|
31
|
+
* `(cond ? t : f)`; `null` when a branch is undecidable (e.g. the `??`/`||`
|
|
32
|
+
* left operand, a bare expression that may render an element OR text)
|
|
33
|
+
* - fragment → sum of its children (transparent wrapper)
|
|
34
|
+
* - bare expression / slot / everything else → `null` (undecidable)
|
|
35
|
+
*/
|
|
36
|
+
function domElementCount(node: IRNode): number | string | null {
|
|
37
|
+
switch (node.type) {
|
|
38
|
+
case 'element':
|
|
39
|
+
case 'component':
|
|
40
|
+
case 'provider':
|
|
41
|
+
case 'async':
|
|
42
|
+
return 1
|
|
43
|
+
case 'text':
|
|
44
|
+
return 0
|
|
45
|
+
case 'expression':
|
|
46
|
+
// `&&` / `?:` empty branches (`null`, `false`, …) render nothing; any
|
|
47
|
+
// other expression may resolve to an element or to text — undecidable.
|
|
48
|
+
return EMPTY_RENDER_EXPRS.has(node.expr.trim()) ? 0 : null
|
|
49
|
+
case 'loop':
|
|
50
|
+
// A per-item-conditional body (#1665) or flatMap renders a
|
|
51
|
+
// runtime-variable element count per item, not `array.length`.
|
|
52
|
+
if (node.bodyIsItemConditional || node.method === 'flatMap') return null
|
|
53
|
+
return `(${buildLoopChainExpr({
|
|
54
|
+
base: node.array,
|
|
55
|
+
sortComparator: node.sortComparator,
|
|
56
|
+
filterPredicate: node.filterPredicate,
|
|
57
|
+
chainOrder: node.chainOrder,
|
|
58
|
+
})}).length`
|
|
59
|
+
case 'conditional': {
|
|
60
|
+
const t = domElementCount(node.whenTrue)
|
|
61
|
+
const f = domElementCount(node.whenFalse)
|
|
62
|
+
if (t === null || f === null) return null
|
|
63
|
+
if (typeof t === 'number' && typeof f === 'number' && t === f) return t
|
|
64
|
+
// Active branch chosen at runtime — reuse the raw `condition`, the exact
|
|
65
|
+
// form `insert()` evaluates in the same init scope.
|
|
66
|
+
return `(${node.condition} ? ${t} : ${f})`
|
|
67
|
+
}
|
|
68
|
+
case 'fragment':
|
|
69
|
+
return sumElementCounts(node.children)
|
|
70
|
+
default:
|
|
71
|
+
// slot / if-statement: element count not statically known.
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sum `domElementCount` over a run of nodes, folding the static part. Returns
|
|
78
|
+
* `null` if any child's count is undecidable — the whole run is then unknown.
|
|
79
|
+
*/
|
|
80
|
+
function sumElementCounts(nodes: readonly IRNode[]): number | string | null {
|
|
81
|
+
let staticCount = 0
|
|
82
|
+
const dynamic: string[] = []
|
|
83
|
+
for (const n of nodes) {
|
|
84
|
+
const c = domElementCount(n)
|
|
85
|
+
if (c === null) return null
|
|
86
|
+
if (typeof c === 'number') staticCount += c
|
|
87
|
+
else dynamic.push(c)
|
|
88
|
+
}
|
|
89
|
+
if (dynamic.length === 0) return staticCount
|
|
90
|
+
const parts = staticCount > 0 ? [String(staticCount), ...dynamic] : dynamic
|
|
91
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(' + ')})`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pre-#1693 element-count heuristic, used as the fallback for nodes whose count
|
|
96
|
+
* `domElementCount` cannot decide. Mirrors the old `producesDomChild` exactly,
|
|
97
|
+
* so an undecidable sibling contributes precisely what it did before this fix —
|
|
98
|
+
* guaranteeing no regression on shapes the new counting can't improve (a bare
|
|
99
|
+
* expression, a `??`/`||` fallback, a per-item-conditional loop).
|
|
100
|
+
*/
|
|
101
|
+
function legacyElementCount(node: IRNode): number {
|
|
15
102
|
return node.type === 'element' || node.type === 'component' || node.type === 'provider'
|
|
16
103
|
|| node.type === 'async'
|
|
17
104
|
|| node.type === 'text' || (node.type === 'expression' && !node.reactive)
|
|
18
105
|
|| node.type === 'conditional'
|
|
106
|
+
? 1
|
|
107
|
+
: 0
|
|
19
108
|
}
|
|
20
109
|
|
|
21
110
|
/**
|
|
22
|
-
* Pre-pass: for every loop node in the IR tree, record the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
111
|
+
* Pre-pass: for every loop node in the IR tree, record the sibling nodes that
|
|
112
|
+
* appear before it in its parent container. Read when constructing
|
|
113
|
+
* TopLevelLoop and NestedLoop so the client JS can offset children[idx]
|
|
114
|
+
* access past everything rendered ahead of the loop's items.
|
|
26
115
|
*
|
|
27
116
|
* Counting must happen for every container whose children render as a
|
|
28
117
|
* contiguous run of DOM siblings into the same parent — not just `element`.
|
|
29
118
|
* A loop nested directly inside a component (`<Wrapper><span/>{xs.map(...)}`
|
|
30
119
|
* </Wrapper>`), fragment, provider, or async boundary has its preceding
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* (
|
|
35
|
-
*
|
|
120
|
+
* siblings rendered as siblings of the loop's items too, so `children[idx]`
|
|
121
|
+
* access is shifted exactly as it is under an element parent (#1688).
|
|
122
|
+
*
|
|
123
|
+
* Transparent containers (fragment / provider / async) render no DOM element
|
|
124
|
+
* wrapper, so their children are siblings in the nearest ancestor element —
|
|
125
|
+
* not in a container of their own. `recordRun` therefore threads ONE
|
|
126
|
+
* preceding-sibling accumulator through them, so a loop inside a fragment sees
|
|
127
|
+
* the parent element's earlier siblings too, not just the fragment's own
|
|
128
|
+
* children (#1699). `<Box><hr/><hr/><>{xs.map(...)}</></Box>` must offset the
|
|
129
|
+
* items past both `<hr/>`s.
|
|
130
|
+
*
|
|
131
|
+
* The siblings are stored raw; `resolveLoopOffset` turns each into its element
|
|
132
|
+
* count via `domElementCount`. That generalisation closes the #1688 follow-up
|
|
133
|
+
* (#1693): a preceding `.map()` contributes `array.length` and a preceding
|
|
134
|
+
* conditional contributes a `(cond ? … : …)` term, both resolved at runtime —
|
|
135
|
+
* a static-only count resolved later groups' nested children against the wrong
|
|
136
|
+
* `children[idx]`, leaving them inert after hydration.
|
|
36
137
|
*
|
|
37
138
|
* Computed once up front (instead of during collection) so the offset data
|
|
38
139
|
* lives in an explicit value rather than a module-level WeakMap mutated by
|
|
39
140
|
* two separate traversals.
|
|
40
141
|
*/
|
|
41
|
-
export function computeLoopSiblingOffsets(root: IRNode): Map<IRLoop,
|
|
42
|
-
const offsets = new Map<IRLoop,
|
|
43
|
-
|
|
44
|
-
|
|
142
|
+
export function computeLoopSiblingOffsets(root: IRNode): Map<IRLoop, IRNode[]> {
|
|
143
|
+
const offsets = new Map<IRLoop, IRNode[]>()
|
|
144
|
+
// Walk a flat DOM run, flattening transparent containers inline so their
|
|
145
|
+
// children join the same preceding-sibling accumulator.
|
|
146
|
+
const recordRun = (children: IRNode[], preceding: IRNode[]): void => {
|
|
45
147
|
for (const child of children) {
|
|
46
148
|
if (child.type === 'loop') {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
149
|
+
// Record the preceding run only when something precedes this loop (a
|
|
150
|
+
// leading loop keeps bare `children[idx]`). `!offsets.has`: the
|
|
151
|
+
// enclosing run records the loop first, in pre-order, with the full
|
|
152
|
+
// preceding context; a later standalone visit of the transparent
|
|
153
|
+
// wrapper (still descended for loops that sit *directly* in a root /
|
|
154
|
+
// loop-body / branch fragment) must not overwrite it with a shorter
|
|
155
|
+
// run.
|
|
156
|
+
if (preceding.length > 0 && !offsets.has(child)) {
|
|
157
|
+
offsets.set(child, [...preceding])
|
|
158
|
+
}
|
|
159
|
+
preceding.push(child)
|
|
160
|
+
} else if (child.type === 'fragment' || child.type === 'provider' || child.type === 'async') {
|
|
161
|
+
// Transparent: no element wrapper — its children render into this run.
|
|
162
|
+
recordRun(child.children, preceding)
|
|
163
|
+
} else {
|
|
164
|
+
preceding.push(child)
|
|
50
165
|
}
|
|
51
166
|
}
|
|
52
167
|
}
|
|
53
168
|
const containerVisit = ({ node, descend }: { node: { children: IRNode[] }; descend: () => void }): void => {
|
|
54
|
-
|
|
169
|
+
recordRun(node.children, [])
|
|
55
170
|
descend()
|
|
56
171
|
}
|
|
57
172
|
walkIR(root, null, {
|
|
@@ -68,6 +183,29 @@ export function computeLoopSiblingOffsets(root: IRNode): Map<IRLoop, number> {
|
|
|
68
183
|
return offsets
|
|
69
184
|
}
|
|
70
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Resolve a loop's preceding-sibling run into the `LoopOffset` value object
|
|
188
|
+
* stored on `TopLevelLoop` / `NestedLoop`: the folded static element count
|
|
189
|
+
* plus one dynamic term (`(arr).length`, `(cond ? … : …)`) per sibling whose
|
|
190
|
+
* count is only known at runtime. Siblings whose count is statically
|
|
191
|
+
* undecidable fall back to `legacyElementCount` (the pre-#1693 behaviour).
|
|
192
|
+
* Returns `undefined` when nothing precedes the loop (or only non-element
|
|
193
|
+
* nodes do), so the loop keeps bare `children[idx]`.
|
|
194
|
+
*/
|
|
195
|
+
function resolveLoopOffset(preceding: IRNode[] | undefined): LoopOffset | undefined {
|
|
196
|
+
if (!preceding || preceding.length === 0) return undefined
|
|
197
|
+
let staticCount = 0
|
|
198
|
+
const dynamicTerms: string[] = []
|
|
199
|
+
for (const node of preceding) {
|
|
200
|
+
const c = domElementCount(node)
|
|
201
|
+
if (c === null) staticCount += legacyElementCount(node)
|
|
202
|
+
else if (typeof c === 'number') staticCount += c
|
|
203
|
+
else dynamicTerms.push(c)
|
|
204
|
+
}
|
|
205
|
+
if (staticCount === 0 && dynamicTerms.length === 0) return undefined
|
|
206
|
+
return { staticCount, dynamicTerms }
|
|
207
|
+
}
|
|
208
|
+
|
|
71
209
|
/**
|
|
72
210
|
* Options controlling `collectInnerLoops` traversal and payload collection.
|
|
73
211
|
*
|
|
@@ -129,7 +267,7 @@ export const branchInnerLoopOptions: CollectInnerLoopsOptions = {
|
|
|
129
267
|
*/
|
|
130
268
|
export function collectInnerLoops(
|
|
131
269
|
nodes: IRNode[],
|
|
132
|
-
siblingOffsets: Map<IRLoop,
|
|
270
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
133
271
|
outerLoopParam?: string,
|
|
134
272
|
ctx?: ClientJsContext,
|
|
135
273
|
options?: CollectInnerLoopsOptions,
|
|
@@ -258,7 +396,7 @@ export function collectInnerLoops(
|
|
|
258
396
|
refsOuterParam: refsOuter,
|
|
259
397
|
childComponents,
|
|
260
398
|
insideConditional: !flat && scope.insideCond ? true : undefined,
|
|
261
|
-
|
|
399
|
+
offset: flat ? undefined : resolveLoopOffset(siblingOffsets.get(n)),
|
|
262
400
|
bindings,
|
|
263
401
|
})
|
|
264
402
|
// Branch-mode callers handle deeper nesting via their own collection paths.
|
|
@@ -286,7 +424,7 @@ export function collectInnerLoops(
|
|
|
286
424
|
*/
|
|
287
425
|
function decideLoopRendering(
|
|
288
426
|
loop: IRLoop,
|
|
289
|
-
siblingOffsets: Map<IRLoop,
|
|
427
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
290
428
|
ctx: ClientJsContext | undefined,
|
|
291
429
|
): { useElementReconciliation: boolean; innerLoops: NestedLoop[] | undefined } {
|
|
292
430
|
const hasNestedComps = (loop.nestedComponents?.length ?? 0) > 0
|
|
@@ -440,7 +578,7 @@ function buildBranchChildComponents(
|
|
|
440
578
|
export function collectElements(
|
|
441
579
|
node: IRNode,
|
|
442
580
|
ctx: ClientJsContext,
|
|
443
|
-
siblingOffsets: Map<IRLoop,
|
|
581
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
444
582
|
insideConditional = false,
|
|
445
583
|
): void {
|
|
446
584
|
walkIR<boolean>(node, insideConditional, {
|
|
@@ -595,7 +733,7 @@ export function collectElements(
|
|
|
595
733
|
isStaticArray: l.isStaticArray,
|
|
596
734
|
useElementReconciliation,
|
|
597
735
|
innerLoops: (useElementReconciliation || (l.isStaticArray && innerLoops?.length)) ? innerLoops : undefined,
|
|
598
|
-
|
|
736
|
+
offset: resolveLoopOffset(siblingOffsets.get(l)),
|
|
599
737
|
filterPredicate: l.filterPredicate ? {
|
|
600
738
|
param: l.filterPredicate.param,
|
|
601
739
|
raw: l.filterPredicate.raw,
|
|
@@ -854,7 +992,7 @@ function collectBranchTextEffects(node: IRNode): ConditionalBranchTextEffect[] {
|
|
|
854
992
|
function collectBranchLoops(
|
|
855
993
|
node: IRNode,
|
|
856
994
|
ctx: ClientJsContext | undefined,
|
|
857
|
-
siblingOffsets: Map<IRLoop,
|
|
995
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
858
996
|
): BranchLoop[] {
|
|
859
997
|
const loops: BranchLoop[] = []
|
|
860
998
|
const restNames = ctx ? buildRestSpreadNames(ctx) : undefined
|
|
@@ -953,7 +1091,7 @@ function collectBranchLoops(
|
|
|
953
1091
|
function buildConditionalMetadata(
|
|
954
1092
|
node: IRNode & { type: 'conditional' },
|
|
955
1093
|
ctx: ClientJsContext,
|
|
956
|
-
siblingOffsets: Map<IRLoop,
|
|
1094
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
957
1095
|
): ConditionalElement {
|
|
958
1096
|
const restNames = buildRestSpreadNames(ctx)
|
|
959
1097
|
// Use loopDepth=-1 so the first loop encountered inside the branch emits
|
|
@@ -983,7 +1121,7 @@ function buildConditionalMetadata(
|
|
|
983
1121
|
function summarizeBranch(
|
|
984
1122
|
node: IRNode,
|
|
985
1123
|
ctx: ClientJsContext,
|
|
986
|
-
siblingOffsets: Map<IRLoop,
|
|
1124
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
987
1125
|
): import('./types').BranchSummary {
|
|
988
1126
|
return {
|
|
989
1127
|
events: collectConditionalBranchEvents(node),
|
|
@@ -1003,7 +1141,7 @@ function summarizeBranch(
|
|
|
1003
1141
|
function collectBranchConditionals(
|
|
1004
1142
|
node: IRNode,
|
|
1005
1143
|
ctx: ClientJsContext,
|
|
1006
|
-
siblingOffsets: Map<IRLoop,
|
|
1144
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1007
1145
|
): ConditionalElement[] {
|
|
1008
1146
|
const result: ConditionalElement[] = []
|
|
1009
1147
|
walkIR(node, null, {
|
|
@@ -1051,7 +1189,7 @@ function collectBranchConditionals(
|
|
|
1051
1189
|
export function collectLoopChildBindings(
|
|
1052
1190
|
children: readonly IRNode[],
|
|
1053
1191
|
ctx: ClientJsContext,
|
|
1054
|
-
siblingOffsets: Map<IRLoop,
|
|
1192
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1055
1193
|
loopParam: string,
|
|
1056
1194
|
loopParamBindings: readonly import('../types').LoopParamBinding[] | undefined,
|
|
1057
1195
|
): LoopChildBindings {
|
|
@@ -1069,7 +1207,7 @@ export function collectLoopChildBindings(
|
|
|
1069
1207
|
export function collectLoopChildConditionals(
|
|
1070
1208
|
node: IRNode,
|
|
1071
1209
|
ctx: ClientJsContext,
|
|
1072
|
-
siblingOffsets: Map<IRLoop,
|
|
1210
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1073
1211
|
loopParam?: string,
|
|
1074
1212
|
loopParamBindings?: readonly import('../types').LoopParamBinding[],
|
|
1075
1213
|
): LoopChildConditional[] {
|
|
@@ -1144,7 +1282,7 @@ export function collectLoopChildConditionals(
|
|
|
1144
1282
|
function summarizeLoopChildBranch(
|
|
1145
1283
|
node: IRNode,
|
|
1146
1284
|
ctx: ClientJsContext,
|
|
1147
|
-
siblingOffsets: Map<IRLoop,
|
|
1285
|
+
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1148
1286
|
loopParam?: string,
|
|
1149
1287
|
loopParamBindings?: readonly import('../types').LoopParamBinding[],
|
|
1150
1288
|
): LoopChildBranchSummary {
|
|
@@ -69,7 +69,7 @@ export function buildStaticArrayDelegationPlan(elem: TopLevelLoop): EventDelegat
|
|
|
69
69
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
70
70
|
param: elem.param,
|
|
71
71
|
mapPreamble: elem.mapPreamble ?? null,
|
|
72
|
-
|
|
72
|
+
offset: elem.offset ?? null,
|
|
73
73
|
},
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
} from '../../types'
|
|
26
26
|
import {
|
|
27
27
|
buildChainedArrayExpr,
|
|
28
|
+
buildLoopChildIndexExpr,
|
|
28
29
|
setIntersects,
|
|
29
30
|
varSlotId,
|
|
30
31
|
wrapLoopParamAsAccessor,
|
|
@@ -129,7 +130,7 @@ export function buildStaticLoopPlan(elem: TopLevelLoop, unsafeLocalNames: Set<st
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
const indexParam = elem.index || '__idx'
|
|
132
|
-
const childIndexExpr =
|
|
133
|
+
const childIndexExpr = buildLoopChildIndexExpr(indexParam, elem.offset)
|
|
133
134
|
|
|
134
135
|
return {
|
|
135
136
|
kind: 'static',
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* container variable and the per-event item-lookup strategy.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { LoopChildEvent, TopLevelLoop } from '../../types'
|
|
8
|
+
import type { LoopChildEvent, LoopOffset, TopLevelLoop } from '../../types'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Plan for a loop's event-delegation block. Covers three legacy emitters:
|
|
@@ -72,6 +72,11 @@ export interface StaticIndexItemLookup {
|
|
|
72
72
|
arrayExpr: string
|
|
73
73
|
param: string
|
|
74
74
|
mapPreamble: string | null
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Offset of the loop's items past its preceding container siblings. Its
|
|
77
|
+
* terms are subtracted from the DOM child index to recover the array index,
|
|
78
|
+
* so later `static + .map()` groups resolve the correct item (#1693).
|
|
79
|
+
* `null` when nothing precedes the loop.
|
|
80
|
+
*/
|
|
81
|
+
offset: LoopOffset | null
|
|
77
82
|
}
|