@barefootjs/jsx 0.17.0 → 0.18.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.
Files changed (88) hide show
  1. package/dist/adapters/env-signal.d.ts +42 -7
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/interface.d.ts +20 -4
  4. package/dist/adapters/interface.d.ts.map +1 -1
  5. package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
  6. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/augment-inherited-props.d.ts +19 -0
  10. package/dist/augment-inherited-props.d.ts.map +1 -1
  11. package/dist/compiler.d.ts.map +1 -1
  12. package/dist/expression-parser.d.ts +48 -2
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.d.ts +24 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1728 -969
  17. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
  24. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
  26. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/html-template.d.ts +41 -0
  28. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/types.d.ts +22 -1
  30. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  31. package/dist/jsx-to-ir.d.ts.map +1 -1
  32. package/dist/loop-destructure.d.ts +55 -18
  33. package/dist/loop-destructure.d.ts.map +1 -1
  34. package/dist/lowering-registry.d.ts +13 -0
  35. package/dist/lowering-registry.d.ts.map +1 -1
  36. package/dist/relocate.d.ts +28 -0
  37. package/dist/relocate.d.ts.map +1 -1
  38. package/dist/ssr-defaults.d.ts.map +1 -1
  39. package/dist/ssr-seed-plan.d.ts +84 -0
  40. package/dist/ssr-seed-plan.d.ts.map +1 -0
  41. package/dist/types.d.ts +79 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +2 -2
  44. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
  45. package/src/__tests__/augment-inherited-props.test.ts +96 -0
  46. package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
  47. package/src/__tests__/compiler-stress-1244.test.ts +13 -4
  48. package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
  49. package/src/__tests__/destructured-map-params.test.ts +11 -1
  50. package/src/__tests__/expression-parser.test.ts +74 -3
  51. package/src/__tests__/free-identifiers.test.ts +55 -0
  52. package/src/__tests__/ir-sort-comparator.test.ts +261 -0
  53. package/src/__tests__/loop-destructure.test.ts +313 -0
  54. package/src/__tests__/loop-hoisted-template.test.ts +235 -0
  55. package/src/__tests__/materialize-getter-calls.test.ts +58 -0
  56. package/src/__tests__/props-destructuring.test.ts +110 -0
  57. package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
  58. package/src/__tests__/ssr-defaults.test.ts +20 -0
  59. package/src/__tests__/ssr-seed-plan.test.ts +212 -0
  60. package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
  61. package/src/__tests__/tagged-template-interleave.test.ts +268 -0
  62. package/src/__tests__/unsupported-expression.test.ts +194 -7
  63. package/src/adapters/env-signal.ts +57 -9
  64. package/src/adapters/interface.ts +20 -4
  65. package/src/adapters/parsed-expr-emitter.ts +19 -2
  66. package/src/analyzer-context.ts +20 -0
  67. package/src/analyzer.ts +74 -1
  68. package/src/augment-inherited-props.ts +139 -9
  69. package/src/compiler.ts +10 -1
  70. package/src/expression-parser.ts +421 -50
  71. package/src/index.ts +30 -3
  72. package/src/ir-to-client-js/collect-elements.ts +15 -1
  73. package/src/ir-to-client-js/compute-inlinability.ts +6 -1
  74. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
  75. package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
  76. package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
  77. package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
  78. package/src/ir-to-client-js/emit-registration.ts +4 -2
  79. package/src/ir-to-client-js/html-template.ts +198 -1
  80. package/src/ir-to-client-js/index.ts +1 -0
  81. package/src/ir-to-client-js/types.ts +22 -0
  82. package/src/jsx-to-ir.ts +409 -24
  83. package/src/loop-destructure.ts +89 -36
  84. package/src/lowering-registry.ts +16 -0
  85. package/src/relocate.ts +201 -14
  86. package/src/ssr-defaults.ts +34 -32
  87. package/src/ssr-seed-plan.ts +146 -0
  88. package/src/types.ts +76 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Backend-neutral SSR seed plan — which signals/memos an adapter may seed
3
+ * in-template at SSR time, and from what scope.
4
+ *
5
+ * Design principle: the IR/analyzer side ANALYZES and attaches structured
6
+ * information; adapters only EMIT. The "is this binding derivable from names
7
+ * already in template scope" decision used to live (triplicated) in the
8
+ * template adapters' seed paths; this module computes it once, on the IR, so
9
+ * every adapter consumes the same plan and only supplies its own syntax.
10
+ *
11
+ * Ordering / acyclicity guarantee: `steps` lists the component's signals
12
+ * first, then its memos, each group in declaration order (matching
13
+ * `IRMetadata.signals` / `IRMetadata.memos`). A binding's name only enters
14
+ * the scope set AFTER its own step is decided, so a `derived` step's `frees`
15
+ * can only name `baseScope` entries or EARLIER steps — self- and
16
+ * forward-references are rejected by construction, and a consumer emitting
17
+ * the steps top-to-bottom never reads an undeclared local.
18
+ *
19
+ * Module-scope pure-string consts count as in-scope (they are part of
20
+ * `baseScope`) because every adapter compile-time-inlines them to their
21
+ * literal value — `collectModuleStringConsts` is the shared source of that
22
+ * set — so a reference to one is never a template-variable read.
23
+ *
24
+ * A `derived` step with EMPTY `frees` is a constant expression (e.g.
25
+ * `createSignal('b')`): the plan still classifies it as derived because the
26
+ * expression is fully analyzable; emit-side constant-skipping (adapters keep
27
+ * their existing ssr-defaults seeding for such inits) is an adapter concern,
28
+ * not a plan concern. Likewise the plan makes no backend-specific choices —
29
+ * no target-variable checks, no self-shadowing rules, no per-backend shape
30
+ * catalogs — those stay in the adapters.
31
+ */
32
+
33
+ import { collectModuleStringConsts } from './augment-inherited-props.ts'
34
+ import { envSignalReaderFor, type EnvSignalReader } from './adapters/env-signal.ts'
35
+ import {
36
+ extractArrowBodyExpression,
37
+ freeIdentifiers,
38
+ isSupported,
39
+ parseExpression,
40
+ type ParsedExpr,
41
+ } from './expression-parser.ts'
42
+ import type { IRMetadata } from './types.ts'
43
+
44
+ /**
45
+ * One binding in component declaration order (signals first, then memos —
46
+ * matching `IRMetadata` order and the adapters' iteration).
47
+ *
48
+ * - `env-reader`: an env signal whose `envReader` key resolves in the shared
49
+ * registry (`envSignalReaderFor`). The runtime provides the per-request
50
+ * reader, so there is nothing to seed; the name still enters scope so a
51
+ * later derived step may reference it. An `envReader` key UNKNOWN to the
52
+ * registry falls through to the normal derived/opaque rules instead.
53
+ * - `derived`: the binding's value expression is a supported shape whose free
54
+ * identifiers are all in scope at this point (baseScope + earlier steps) —
55
+ * an adapter may seed it in-template by lowering `parsed`/`expr`.
56
+ * - `opaque`: not seedable this way (empty init, unsupported shape,
57
+ * unanalyzable free set, out-of-scope reference, or a block-bodied memo).
58
+ * The name still enters scope for later steps; adapters keep their static
59
+ * ssr-defaults seeding for it.
60
+ */
61
+ export type SsrSeedStep =
62
+ | { kind: 'env-reader'; name: string; reader: EnvSignalReader }
63
+ | { kind: 'derived'; name: string; origin: 'signal' | 'memo'; expr: string; parsed: ParsedExpr; frees: string[] }
64
+ | { kind: 'opaque'; name: string; origin: 'signal' | 'memo' }
65
+
66
+ export interface SsrSeedPlan {
67
+ /**
68
+ * Names in scope before any step: props params, the props-object name
69
+ * (when the component takes an undestructured props object), and module
70
+ * pure-string consts (compile-time inlined by every adapter).
71
+ */
72
+ baseScope: string[]
73
+ steps: SsrSeedStep[]
74
+ }
75
+
76
+ /**
77
+ * Classify one value expression against the current scope: `derived` when it
78
+ * parses to a supported shape whose free identifiers are all `available`
79
+ * (an unanalyzable free set — `freeIdentifiers` → null — fails safe to
80
+ * opaque). The scope check runs over the parsed SOURCE tree, so a shadowed
81
+ * name (`items.filter((p) => p.ok) && p`, where the trailing `p` is a
82
+ * different, unbound reference from the callback's own param) is rejected.
83
+ */
84
+ function classify(
85
+ name: string,
86
+ origin: 'signal' | 'memo',
87
+ expr: string,
88
+ parsed: ParsedExpr,
89
+ available: ReadonlySet<string>,
90
+ ): SsrSeedStep {
91
+ if (!isSupported(parsed).supported) return { kind: 'opaque', name, origin }
92
+ const frees = freeIdentifiers(parsed)
93
+ if (frees === null) return { kind: 'opaque', name, origin }
94
+ for (const free of frees) {
95
+ if (!available.has(free)) return { kind: 'opaque', name, origin }
96
+ }
97
+ return { kind: 'derived', name, origin, expr, parsed, frees: [...frees] }
98
+ }
99
+
100
+ /**
101
+ * Compute the component's SSR seed plan from its metadata. See the module
102
+ * doc for the contract. Memo steps are gated to EXPRESSION-BODIED memos
103
+ * (`extractArrowBodyExpression` returns the body): a block-bodied memo is
104
+ * `opaque` even when the analyzer folded it to a `parsed` expression.
105
+ */
106
+ export function computeSsrSeedPlan(metadata: IRMetadata): SsrSeedPlan {
107
+ const baseScope: string[] = metadata.propsParams.map(p => p.name)
108
+ if (metadata.propsObjectName) baseScope.push(metadata.propsObjectName)
109
+ for (const name of collectModuleStringConsts(metadata.localConstants).keys()) {
110
+ baseScope.push(name)
111
+ }
112
+
113
+ const available = new Set<string>(baseScope)
114
+ const steps: SsrSeedStep[] = []
115
+
116
+ for (const signal of metadata.signals) {
117
+ if (signal.envReader) {
118
+ const reader = envSignalReaderFor(signal.envReader)
119
+ if (reader) {
120
+ steps.push({ kind: 'env-reader', name: signal.getter, reader })
121
+ available.add(signal.getter)
122
+ continue
123
+ }
124
+ }
125
+ const expr = signal.initialValue.trim()
126
+ steps.push(
127
+ expr === ''
128
+ ? { kind: 'opaque', name: signal.getter, origin: 'signal' }
129
+ : classify(signal.getter, 'signal', expr, parseExpression(expr), available),
130
+ )
131
+ available.add(signal.getter)
132
+ }
133
+
134
+ for (const memo of metadata.memos) {
135
+ const body = extractArrowBodyExpression(memo.computation)
136
+ const expr = body?.trim() ?? ''
137
+ steps.push(
138
+ expr === ''
139
+ ? { kind: 'opaque', name: memo.name, origin: 'memo' }
140
+ : classify(memo.name, 'memo', expr, memo.parsed ?? parseExpression(expr), available),
141
+ )
142
+ available.add(memo.name)
143
+ }
144
+
145
+ return { baseScope, steps }
146
+ }
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ParsedExpr, ParsedStatement } from './expression-parser.ts'
8
+ import type { SsrSeedPlan } from './ssr-seed-plan.ts'
8
9
 
9
10
  /**
10
11
  * Loop-hoisted sort comparator for the `.sort().map()` / `.toSorted().map()`
@@ -694,6 +695,26 @@ export interface RestExcludeKey {
694
695
  isIdent: boolean
695
696
  }
696
697
 
698
+ /**
699
+ * Structured, non-string form of a `.map()` callback destructure accessor
700
+ * step — one entry per `.foo` / `["a-b"]` / `[0]` step folded into
701
+ * {@link LoopParamBinding.path}. Adapters that need to build their own
702
+ * accessor expression (rather than splice `path` in as a JS suffix) walk
703
+ * `segments` instead of parsing `path` with a regex (repo rule: never parse
704
+ * JS/TS syntax with regex or string matching).
705
+ *
706
+ * - `{ kind: 'field', key, isIdent }`: an object-property step. `isIdent`
707
+ * uses the same `ts.isIdentifierStart`/`isIdentifierPart` classification
708
+ * as {@link RestExcludeKey.isIdent} — an adapter choosing between a native
709
+ * `.foo` accessor and a quoted/bracketed one (`["data-priority"]`, a
710
+ * quoted map key, ...) reads this instead of re-deriving the identifier
711
+ * rule itself.
712
+ * - `{ kind: 'index', index }`: an array-position step (`[0]`, `[1]`, ...).
713
+ */
714
+ export type LoopBindingPathSegment =
715
+ | { kind: 'field'; key: string; isIdent: boolean }
716
+ | { kind: 'index'; index: number }
717
+
697
718
  /**
698
719
  * Destructured binding extracted from a `.map()` callback's item parameter.
699
720
  *
@@ -720,6 +741,24 @@ export interface LoopParamBinding {
720
741
  rest?:
721
742
  | { kind: 'object'; exclude: readonly RestExcludeKey[] }
722
743
  | { kind: 'array'; from: number }
744
+ /**
745
+ * Structured form of `path` (see {@link LoopBindingPathSegment}).
746
+ *
747
+ * - Fixed bindings (`rest` unset): the full accessor from the loop item to
748
+ * this binding, one segment per `path` step. Always non-empty — a fixed
749
+ * binding always has at least one accessor step.
750
+ * - Rest bindings (`rest` set): the structured form of the PARENT prefix
751
+ * (i.e. of `path`, which for rest bindings holds the parent prefix, not
752
+ * an accessor to the rest binding itself) — possibly empty when the
753
+ * rest sits at the loop root (`({ ...rest }) => …` / `([...rest]) => …`).
754
+ *
755
+ * Optional only so older/foreign IR (built before this field existed)
756
+ * still type-checks; `extractLoopParamBindings` always populates it now.
757
+ * Downstream gates (`isLowerableLoopDestructure`) treat a missing
758
+ * `segments` as "unknown shape" and refuse conservatively rather than
759
+ * reading it as "binding at the loop root".
760
+ */
761
+ segments?: readonly LoopBindingPathSegment[]
723
762
  }
724
763
 
725
764
  export interface IRComponent {
@@ -1526,6 +1565,16 @@ export interface IRMetadata {
1526
1565
  * resolves the compiled module rather than the source `.tsx`.
1527
1566
  */
1528
1567
  clientSignalImportSources?: Set<string>
1568
+ /**
1569
+ * Backend-neutral SSR seed plan: per-binding derived/opaque/env-reader
1570
+ * classification in declaration order, plus the base scope, computed by
1571
+ * `computeSsrSeedPlan` from this metadata. Attached by `buildMetadata` and
1572
+ * serialized into IR JSON like the rest of the metadata; template adapters
1573
+ * consume it instead of re-deriving scope/derivability themselves (their
1574
+ * target-syntax choices — lowering, self-shadow rules, constant-emit
1575
+ * guards — stay adapter-side).
1576
+ */
1577
+ ssrSeedPlan?: SsrSeedPlan
1529
1578
  }
1530
1579
 
1531
1580
  // =============================================================================
@@ -1699,6 +1748,24 @@ export interface ErrorSuggestion {
1699
1748
  replacement?: string
1700
1749
  }
1701
1750
 
1751
+ /**
1752
+ * A diagnostic an adapter intentionally emits for a shared conformance
1753
+ * fixture (packages/adapter-tests) instead of lowering it — the adapter's
1754
+ * machine-readable known-limitations declaration. Consumed by the adapter's
1755
+ * own conformance test (as `expectedDiagnostics`) and by `bf compat`
1756
+ * (issue-URL attribution). Tracked limitations carry the `known-limitation`
1757
+ * label: https://github.com/piconic-ai/barefootjs/labels/known-limitation
1758
+ */
1759
+ export interface ConformancePin {
1760
+ /** Diagnostic code, e.g. 'BF101'. */
1761
+ code: string
1762
+ severity: 'error' | 'warning'
1763
+ /** Tracking issue URL (known-limitation label) for this refusal, when one exists. */
1764
+ issue?: string
1765
+ }
1766
+ /** Keyed by shared-fixture id (`JSXFixture.id`). */
1767
+ export type ConformancePins = Record<string, ReadonlyArray<ConformancePin>>
1768
+
1702
1769
  // =============================================================================
1703
1770
  // Compile Options & Results
1704
1771
  // =============================================================================
@@ -1759,6 +1826,15 @@ export interface FileOutput {
1759
1826
  path: string
1760
1827
  content: string
1761
1828
  type: 'markedTemplate' | 'clientJs' | 'ir' | 'sourceMap' | 'types' | 'ssrDefaults'
1829
+ /**
1830
+ * The exported component this file was generated for. Set on
1831
+ * `markedTemplate` / `ssrDefaults` outputs so the build pipeline can pair
1832
+ * them per component without guessing from file basenames — a
1833
+ * single-component file's template is named after the SOURCE file
1834
+ * (`index.html.ep`), not the component, so the basename alone can't
1835
+ * recover the component name (#2132).
1836
+ */
1837
+ componentName?: string
1762
1838
  }
1763
1839
 
1764
1840
  export interface CompileResult {