@barefootjs/jsx 0.26.2 → 0.26.3

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.
Files changed (80) hide show
  1. package/dist/adapters/interface.d.ts +29 -0
  2. package/dist/adapters/interface.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts +9 -0
  4. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  5. package/dist/adapters/loop-bound-names.d.ts.map +1 -1
  6. package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
  7. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  8. package/dist/adapters/test-adapter.d.ts.map +1 -1
  9. package/dist/analyzer-context.d.ts +11 -1
  10. package/dist/analyzer-context.d.ts.map +1 -1
  11. package/dist/analyzer.d.ts +40 -1
  12. package/dist/analyzer.d.ts.map +1 -1
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.js +662 -223
  15. package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +2 -0
  17. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +12 -0
  25. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts +1 -1
  28. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/html-template.d.ts +19 -1
  31. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
  33. package/dist/ir-to-client-js/types.d.ts +4 -4
  34. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  35. package/dist/jsx-to-ir.d.ts.map +1 -1
  36. package/dist/loop-destructure.d.ts.map +1 -1
  37. package/dist/strip-types.d.ts +18 -0
  38. package/dist/strip-types.d.ts.map +1 -1
  39. package/dist/types.d.ts +96 -32
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +94 -0
  43. package/src/__tests__/client-js-generation.test.ts +10 -3
  44. package/src/__tests__/compiler-stress-1244.test.ts +12 -1
  45. package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
  46. package/src/__tests__/flatmap-segments.test.ts +80 -0
  47. package/src/__tests__/map-arbitrary-body.test.ts +226 -0
  48. package/src/__tests__/map-body-no-silent-divergence.test.ts +360 -0
  49. package/src/__tests__/map-multi-return-body.test.ts +172 -0
  50. package/src/__tests__/unsupported-expression.test.ts +20 -2
  51. package/src/adapters/interface.ts +40 -0
  52. package/src/adapters/jsx-adapter.ts +10 -0
  53. package/src/adapters/loop-bound-names.ts +6 -2
  54. package/src/adapters/parsed-expr-emitter.ts +5 -10
  55. package/src/adapters/test-adapter.ts +10 -0
  56. package/src/analyzer-context.ts +35 -1
  57. package/src/analyzer.ts +162 -24
  58. package/src/compiler.ts +2 -2
  59. package/src/expression-parser.ts +27 -19
  60. package/src/ir-to-client-js/build-references.ts +3 -2
  61. package/src/ir-to-client-js/collect-elements.ts +3 -3
  62. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +2 -0
  63. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +7 -2
  64. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
  65. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
  66. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +16 -3
  67. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
  68. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +17 -2
  69. package/src/ir-to-client-js/control-flow/plan/loop.ts +12 -0
  70. package/src/ir-to-client-js/control-flow/shared.ts +2 -1
  71. package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +9 -2
  72. package/src/ir-to-client-js/control-flow.ts +11 -0
  73. package/src/ir-to-client-js/html-template.ts +133 -37
  74. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
  75. package/src/ir-to-client-js/types.ts +4 -3
  76. package/src/jsx-to-ir.ts +690 -164
  77. package/src/loop-destructure.ts +9 -5
  78. package/src/rich-type-refusal.ts +6 -2
  79. package/src/strip-types.ts +47 -0
  80. package/src/types.ts +110 -35
@@ -1,4 +1,5 @@
1
1
  import type { IRLoop, IRNode, AttrValue, IRTemplatePart } from './types.ts'
2
+ import { preambleAnalysisText, preambleAnalysisTemplateText } from './types.ts'
2
3
 
3
4
  /**
4
5
  * True when a loop's `.map()` destructure param is one of the shapes this
@@ -154,12 +155,15 @@ function restNamesMisused(loop: IRLoop, names: string[]): boolean {
154
155
  check(l.array)
155
156
  check(l.templateArray)
156
157
  check(l.key)
157
- check(l.mapPreamble)
158
- check(l.templateMapPreamble)
158
+ if (l.preamble) {
159
+ check(preambleAnalysisText(l.preamble))
160
+ check(preambleAnalysisTemplateText(l.preamble))
161
+ }
159
162
  if (l.flatMapCallback) {
160
- check(l.flatMapCallback.body)
161
- check(l.flatMapCallback.templateBody)
162
- l.flatMapCallback.fragments.forEach(f => visit(f.ir))
163
+ check(preambleAnalysisText(l.flatMapCallback))
164
+ for (const seg of l.flatMapCallback.segments) {
165
+ if (seg.kind === 'jsx') visit(seg.ir)
166
+ }
163
167
  }
164
168
  l.children.forEach(visit)
165
169
  }
@@ -299,8 +299,12 @@ function walkNode(
299
299
  for (const nested of node.nestedComponents ?? []) {
300
300
  for (const child of nested.children) walkNode(child, meta, loopBindings, matchers, errors, seen)
301
301
  }
302
- for (const frag of node.flatMapCallback?.fragments ?? []) {
303
- walkNode(frag.ir, meta, loopBindings, matchers, errors, seen)
302
+ for (const seg of node.flatMapCallback?.segments ?? []) {
303
+ if (seg.kind === 'jsx') walkNode(seg.ir, meta, loopBindings, matchers, errors, seen)
304
+ }
305
+ // Preamble leaves (array-builder bodies) are nested IR the same way.
306
+ for (const seg of node.preamble?.segments ?? []) {
307
+ if (seg.kind === 'jsx') walkNode(seg.ir, meta, loopBindings, matchers, errors, seen)
304
308
  }
305
309
  break
306
310
  }
@@ -71,6 +71,53 @@ export function reconstructWithoutTypes(
71
71
  return result
72
72
  }
73
73
 
74
+ /**
75
+ * Split a node's text into JS/marker segments: type ranges are stripped from
76
+ * the JS text, and each `markers` span becomes its own segment boundary
77
+ * (emitting `{ marker: i }` for the i-th marker, in the order given). Used by
78
+ * the Stage-3 preamble collector to carry mixed content (JS text + JSX leaves)
79
+ * as structured segments instead of a sentinel-bearing string — the marker
80
+ * index maps to the compiled leaf IR. Markers are absolute-position,
81
+ * non-overlapping sub-spans of `node`; a type range inside a marker span is
82
+ * subsumed.
83
+ */
84
+ export function reconstructAsSegments(
85
+ node: ts.Node,
86
+ sourceFile: ts.SourceFile,
87
+ ranges: ExcludeRange[],
88
+ markers: Array<{ start: number; end: number }>
89
+ ): Array<{ js: string } | { marker: number }> {
90
+ const nodeStart = node.getStart(sourceFile)
91
+ const nodeEnd = node.getEnd()
92
+ const fullText = sourceFile.text
93
+
94
+ type Edit = { start: number; end: number; marker: number | null }
95
+ const edits: Edit[] = []
96
+ for (const r of ranges) {
97
+ if (r.end <= nodeStart || r.start >= nodeEnd) continue
98
+ edits.push({ start: r.start, end: r.end, marker: null })
99
+ }
100
+ markers.forEach((m, i) => edits.push({ start: m.start, end: m.end, marker: i }))
101
+ edits.sort((a, b) => a.start - b.start || b.end - a.end)
102
+
103
+ const segments: Array<{ js: string } | { marker: number }> = []
104
+ let jsBuf = ''
105
+ let pos = nodeStart
106
+ for (const edit of edits) {
107
+ if (edit.start < pos) continue // subsumed by an already-emitted span
108
+ jsBuf += fullText.slice(pos, edit.start)
109
+ if (edit.marker !== null) {
110
+ if (jsBuf) segments.push({ js: jsBuf })
111
+ jsBuf = ''
112
+ segments.push({ marker: edit.marker })
113
+ }
114
+ pos = edit.end
115
+ }
116
+ if (pos < nodeEnd) jsBuf += fullText.slice(pos, nodeEnd)
117
+ if (jsBuf) segments.push({ js: jsBuf })
118
+ return segments
119
+ }
120
+
74
121
  function mergeRanges(ranges: ExcludeRange[]): ExcludeRange[] {
75
122
  if (ranges.length === 0) return []
76
123
  const merged: ExcludeRange[] = [ranges[0]]
package/src/types.ts CHANGED
@@ -371,6 +371,15 @@ export interface IRExpression {
371
371
  loc: SourceLocation
372
372
  /** When true, expression should be evaluated on client side only */
373
373
  clientOnly?: boolean
374
+ /**
375
+ * Stage 3 / D4 (spec/callback-fidelity.md) — this expression child references
376
+ * an array built by an arbitrary `.map()` preamble (`return <tr>{out}</tr>`
377
+ * where `out` is `push`-populated with element strings). Phase-2 string-
378
+ * template emission must join it (`Array.isArray(out) ? out.join('') : …`)
379
+ * rather than `String([])`-comma-collapse it. JSX SSR adapters ignore the
380
+ * flag (their JSX runtime renders an array child natively).
381
+ */
382
+ joinArrayChild?: boolean
374
383
  /** When true, expression calls signal getters or memos (has reactive `foo()` pattern). */
375
384
  callsReactiveGetters?: boolean
376
385
  /** When true, expression contains function call(s) — any `identifier()` pattern (computed from AST). */
@@ -678,21 +687,20 @@ export interface IRLoop {
678
687
  */
679
688
  bodyIsItemConditional?: boolean
680
689
 
681
- /**
682
- * Raw JS of pre-return statements in block body .map() callback.
683
- * Example: `items.map(item => { const label = item.name.toUpperCase(); return <li>{label}</li> })`
684
- * stores "const label = item.name.toUpperCase();" as mapPreamble.
685
- */
686
- mapPreamble?: string
687
- /** Pre-transformed mapPreamble with destructured prop refs rewritten to _p.xxx. */
688
- templateMapPreamble?: string
689
-
690
690
  /** Type annotation for loop param (e.g., 'Desk'), preserved for .tsx output */
691
691
  paramType?: string
692
692
  /** Type annotation for loop index param (e.g., 'number'), preserved for .tsx output */
693
693
  indexType?: string
694
- /** mapPreamble with TypeScript type annotations preserved, for .tsx output */
695
- typedMapPreamble?: string
694
+ /**
695
+ * Structured pre-return statements of a block-body `.map()` callback
696
+ * (Stage 3 root cure, spec/callback-fidelity.md). Replaces the former
697
+ * `mapPreamble` / `templateMapPreamble` / `typedMapPreamble` string carriers
698
+ * and `preambleFragments`: mixed content (JS text + JSX leaves) is carried as
699
+ * a segment list, never as a sentinel-bearing string, so an emitter that
700
+ * lacks preamble support cannot accidentally interpolate it — rendering goes
701
+ * through `renderPreamble()` (html-template.ts) or not at all.
702
+ */
703
+ preamble?: MapCallbackPreamble
696
704
 
697
705
  /**
698
706
  * When `.map(callback)` destructures its item parameter (array or object
@@ -720,9 +728,9 @@ export interface IRLoop {
720
728
  /**
721
729
  * For flatMap callbacks whose body can't be decomposed into simple
722
730
  * `children` (block bodies with conditional returns, variable-assigned
723
- * JSX, etc.). Carries the full callback body with JSX fragments
724
- * replaced by `__BF_JSX_N__` placeholders, plus the IR for each
725
- * fragment so each adapter can render them appropriately.
731
+ * JSX, etc.). Carries the body as structured segments (JS text +
732
+ * compiled JSX-leaf IR) rendered through `renderPreamble()`, plus the
733
+ * raw TSX for JSX-runtime SSR adapters.
726
734
  *
727
735
  * When present, `children` is empty — emitters use this field instead.
728
736
  */
@@ -730,31 +738,98 @@ export interface IRLoop {
730
738
  }
731
739
 
732
740
  /**
733
- * A compiled flatMap callback body. JSX elements in the original
734
- * callback have been transformed to IR nodes and replaced with
735
- * `__BF_JSX_0__`, `__BF_JSX_1__`, placeholders in `body`.
736
- *
737
- * Each emitter (html-template, hono-adapter) renders the IR fragments
738
- * in its own format and substitutes the placeholders accordingly.
741
+ * A compiled flatMap callback body, carried as structured segments (the same
742
+ * shape as {@link MapCallbackPreamble}, rendered through the same
743
+ * `renderPreamble()` door) never as a sentinel-bearing string. JSX-runtime
744
+ * SSR adapters emit `rawBody` (real TSX, branded) instead.
739
745
  */
740
746
  export interface FlatMapCallback {
741
747
  /** Callback parameters text, e.g. `"(frame, i)"` */
742
748
  params: string
743
- /** Callback body text with JSX replaced by `__BF_JSX_N__` placeholders */
744
- body: string
745
- /** Same as `body` but with prop refs rewritten for template context */
746
- templateBody?: string
747
- /** Original callback body text with JSX preserved (for Hono .tsx output) */
748
- rawBody: string
749
- /** IR nodes for each JSX placeholder, ordered by placeholder index */
750
- fragments: FlatMapJsxFragment[]
751
- }
752
-
753
- export interface FlatMapJsxFragment {
754
- /** Placeholder string, e.g. `"__BF_JSX_0__"` */
755
- placeholder: string
756
- /** Compiled IR node for this JSX fragment */
757
- ir: IRNode
749
+ /** Callback body as js-text / compiled-JSX-leaf segments. */
750
+ segments: PreambleSegment[]
751
+ /** Original callback body text (JSX intact) for JSX-runtime SSR adapters. */
752
+ rawBody: TsxSourceText
753
+ }
754
+
755
+ /**
756
+ * Raw TSX source text (types + JSX intact), branded so it is only assignable
757
+ * where a JSX-runtime SSR adapter emits real TSX (Hono / TestAdapter). Plain
758
+ * strings destined for the client bundle cannot satisfy this type, and this
759
+ * type must never be spliced into plain-JS emission — the write-side twin of
760
+ * the "never parse JS with regex" rule: raw source crossing into an emitted
761
+ * artifact is tagged with its only legal destination.
762
+ */
763
+ export type TsxSourceText = string & { readonly __tsxSourceBrand: unique symbol }
764
+
765
+ /** Constructor for {@link TsxSourceText} — the single place the brand is applied. */
766
+ export function tsxSourceText(raw: string): TsxSourceText {
767
+ return raw as TsxSourceText
768
+ }
769
+
770
+ /**
771
+ * One segment of a `.map()` callback preamble. Mixed content is structured,
772
+ * never a sentinel-bearing string: `js` segments carry JSX-free JS source text
773
+ * (types stripped; `templateText` set when the destructured-prop rewrite
774
+ * differs), `jsx` segments carry the compiled IR of one JSX leaf. Segments
775
+ * concatenate in source order with no separators (they are sub-statement
776
+ * spans).
777
+ */
778
+ export type PreambleSegment =
779
+ | {
780
+ kind: 'js'
781
+ /** Type-stripped JS text for the client bundle. */
782
+ text: string
783
+ /** `text` with destructured prop refs rewritten to `_p.xxx`, when different. */
784
+ templateText?: string
785
+ }
786
+ | { kind: 'jsx'; ir: IRNode }
787
+
788
+ /**
789
+ * The structured pre-return statements of a block-body `.map()` callback
790
+ * (Stage 3 root cure, spec/callback-fidelity.md). Client emission renders
791
+ * `segments` via `renderPreamble()` (html-template.ts) — the only door; a
792
+ * consumer that can't call it has no way to splice the preamble, so a missing
793
+ * wire-up is a type error or an explicit refusal, never a silent leak.
794
+ * JSX-runtime SSR adapters emit `ssrText` (real TSX) instead.
795
+ */
796
+ export interface MapCallbackPreamble {
797
+ segments: PreambleSegment[]
798
+ /** Raw preamble source (types + JSX intact) for JSX-runtime SSR adapters. */
799
+ ssrText: TsxSourceText
800
+ /** const/let/function names the preamble declares (D5 key-derivability guard). */
801
+ declaredNames: string[]
802
+ /**
803
+ * The subset of {@link declaredNames} that accumulate JSX leaves — the
804
+ * `push`/`unshift` targets of a `jsx` segment and the declaration targets of
805
+ * a leaf-bearing initializer (`const out = xs.map(x => <td/>)`). Only these
806
+ * get the `{out}` array-join child emission; a value-only local (`{label}`)
807
+ * keeps the plain interpolation it always had.
808
+ */
809
+ builderNames: string[]
810
+ }
811
+
812
+ /**
813
+ * The preamble's JS text (js segments only, concatenated), for read-side
814
+ * analysis — free-identifier scans, reachability edges, rest-misuse checks.
815
+ * JSX-leaf interiors are compiled IR and are analyzed through their own IR
816
+ * walks, exactly as the former placeholder carrier excluded them. NEVER use
817
+ * this for emission — emission goes through `renderPreamble()`.
818
+ */
819
+ export function preambleAnalysisText(p: Pick<MapCallbackPreamble, 'segments'>): string {
820
+ let out = ''
821
+ for (const seg of p.segments) if (seg.kind === 'js') out += seg.text
822
+ return out
823
+ }
824
+
825
+ /**
826
+ * Template-context variant of {@link preambleAnalysisText} (destructured prop
827
+ * refs rewritten). Read-side analysis only.
828
+ */
829
+ export function preambleAnalysisTemplateText(p: Pick<MapCallbackPreamble, 'segments'>): string {
830
+ let out = ''
831
+ for (const seg of p.segments) if (seg.kind === 'js') out += seg.templateText ?? seg.text
832
+ return out
758
833
  }
759
834
 
760
835
  /**