@barefootjs/jsx 0.16.0 → 0.17.1

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 (65) hide show
  1. package/dist/adapters/env-signal.d.ts +73 -15
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  4. package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
  5. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  6. package/dist/analyzer-context.d.ts +29 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/builtin-lowering-plugins.d.ts +34 -0
  10. package/dist/builtin-lowering-plugins.d.ts.map +1 -0
  11. package/dist/compiler.d.ts.map +1 -1
  12. package/dist/expression-parser.d.ts +264 -163
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.d.ts +10 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7839 -7019
  17. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
  20. package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
  21. package/dist/jsx-to-ir.d.ts.map +1 -1
  22. package/dist/lowering-registry.d.ts +122 -0
  23. package/dist/lowering-registry.d.ts.map +1 -0
  24. package/dist/query-href-lowering.d.ts +63 -0
  25. package/dist/query-href-lowering.d.ts.map +1 -0
  26. package/dist/ssr-defaults.d.ts.map +1 -1
  27. package/dist/ssr-seed-plan.d.ts +84 -0
  28. package/dist/ssr-seed-plan.d.ts.map +1 -0
  29. package/dist/types.d.ts +180 -11
  30. package/dist/types.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
  33. package/src/__tests__/analyzer.test.ts +53 -0
  34. package/src/__tests__/expression-parser.test.ts +714 -392
  35. package/src/__tests__/free-identifiers.test.ts +55 -0
  36. package/src/__tests__/ir-reduce-op.test.ts +18 -21
  37. package/src/__tests__/ir-sort-comparator.test.ts +19 -20
  38. package/src/__tests__/lowering-registry.test.ts +141 -0
  39. package/src/__tests__/materialize-getter-calls.test.ts +58 -0
  40. package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
  41. package/src/__tests__/query-href-recognition.test.ts +58 -0
  42. package/src/__tests__/serialize-parsed-expr.test.ts +223 -0
  43. package/src/__tests__/ssr-seed-plan.test.ts +212 -0
  44. package/src/__tests__/unsupported-expression.test.ts +98 -4
  45. package/src/adapters/env-signal.ts +108 -21
  46. package/src/adapters/jsx-adapter.ts +17 -0
  47. package/src/adapters/parsed-expr-emitter.ts +39 -41
  48. package/src/analyzer-context.ts +72 -27
  49. package/src/analyzer.ts +226 -9
  50. package/src/builtin-lowering-plugins.ts +54 -0
  51. package/src/compiler.ts +6 -1
  52. package/src/expression-parser.ts +1375 -929
  53. package/src/index.ts +31 -3
  54. package/src/ir-to-client-js/csr-substitute.ts +5 -0
  55. package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
  56. package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
  57. package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
  58. package/src/jsx-to-ir.ts +182 -43
  59. package/src/lowering-registry.ts +160 -0
  60. package/src/query-href-lowering.ts +147 -0
  61. package/src/ssr-defaults.ts +5 -1
  62. package/src/ssr-seed-plan.ts +146 -0
  63. package/src/types.ts +182 -12
  64. package/src/__tests__/flatmap-support.test.ts +0 -218
  65. package/src/__tests__/reduce-op.test.ts +0 -201
@@ -1 +1 @@
1
- {"version":3,"file":"csr-substitute.d.ts","sourceRoot":"","sources":["../../src/ir-to-client-js/csr-substitute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACrC;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC,CAAA;AAEtE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,YAAY,CAAA;IAC3B,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAA;IACnB,qFAAqF;IACrF,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACrC;AAED,MAAM,WAAW,MAAM;IACrB;;;;;OAKG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC3C,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAE,CAmB7D;AAmMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGtF;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,eAAe,EAAE,MAAM,GAAG,IAAI,GAC7B,MAAM,CAiBR"}
1
+ {"version":3,"file":"csr-substitute.d.ts","sourceRoot":"","sources":["../../src/ir-to-client-js/csr-substitute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACrC;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC,CAAA;AAEtE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,YAAY,CAAA;IAC3B,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAA;IACnB,qFAAqF;IACrF,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACrC;AAED,MAAM,WAAW,MAAM;IACrB;;;;;OAKG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC3C,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAE,CAmB7D;AAmMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGtF;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,eAAe,EAAE,MAAM,GAAG,IAAI,GAC7B,MAAM,CAsBR"}
@@ -1 +1 @@
1
- {"version":3,"file":"build-declaration-emit.d.ts","sourceRoot":"","sources":["../../../src/ir-to-client-js/plan/build-declaration-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,uBAAuB,CAAA;AAE9B;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,EAAE,WAAW,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAA;IAC7D,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;CAC3C;AAED,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,eAAe,EACpB,iBAAiB,EAAE,SAAS,gBAAgB,EAAE,GAC7C,sBAAsB,CAMxB;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,sBAAsB,GAC9B,mBAAmB,CAkCrB"}
1
+ {"version":3,"file":"build-declaration-emit.d.ts","sourceRoot":"","sources":["../../../src/ir-to-client-js/plan/build-declaration-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,uBAAuB,CAAA;AAE9B;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,EAAE,WAAW,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAA;IAC7D,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;CAC3C;AAED,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,eAAe,EACpB,iBAAiB,EAAE,SAAS,gBAAgB,EAAE,GAC7C,sBAAsB,CAMxB;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,sBAAsB,GAC9B,mBAAmB,CAkCrB"}
@@ -52,6 +52,15 @@ export interface SignalEmitPlan {
52
52
  branchCondition?: string;
53
53
  /** Profile-mode IR-aligned id, appended as the `createSignal` 2nd arg (#1690). */
54
54
  bfId?: string;
55
+ /**
56
+ * When set, the full initializer expression to emit verbatim instead of
57
+ * `createSignal(<initialValueExpr>)`. Env signals (#2057) emit their own
58
+ * factory call — e.g. `createSearchParams()` — with no baked initial value,
59
+ * profile id, controlled effect, or branch condition (the tuple is a stable
60
+ * request-scoped view, not stored state). When present the stringifier emits
61
+ * `const [<getter>, <setter>] = <initializerOverride>` and nothing else.
62
+ */
63
+ initializerOverride?: string;
55
64
  }
56
65
  export interface ControlledSignalEffectPlan {
57
66
  /** Receiver setter — must equal the parent signal's `setter`. */
@@ -1 +1 @@
1
- {"version":3,"file":"declaration-emit.d.ts","sourceRoot":"","sources":["../../../src/ir-to-client-js/plan/declaration-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAA;IAChB,qDAAqD;IACrD,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,KAAK,CAAA;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,CAAA;IACd,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,gBAAgB,EAAE,0BAA0B,GAAG,IAAI,CAAA;IACnD;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,0BAA0B;IACzC,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAA;IACpB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAA;IACvB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,MAAM,mBAAmB,GAC3B,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,gBAAgB,CAAA"}
1
+ {"version":3,"file":"declaration-emit.d.ts","sourceRoot":"","sources":["../../../src/ir-to-client-js/plan/declaration-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAA;IAChB,qDAAqD;IACrD,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,KAAK,CAAA;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,CAAA;IACd,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,gBAAgB,EAAE,0BAA0B,GAAG,IAAI,CAAA;IACnD;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAA;IACpB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAA;IACvB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,MAAM,mBAAmB,GAC3B,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,gBAAgB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-to-ir.d.ts","sourceRoot":"","sources":["../src/jsx-to-ir.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,KAAK,MAAM,EAyBZ,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,KAAK,eAAe,EAA8C,MAAM,uBAAuB,CAAA;AA0gBxG,wBAAgB,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAkDhE"}
1
+ {"version":3,"file":"jsx-to-ir.d.ts","sourceRoot":"","sources":["../src/jsx-to-ir.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,KAAK,MAAM,EAyBZ,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,KAAK,eAAe,EAA0E,MAAM,uBAAuB,CAAA;AAqnBpI,wBAAgB,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAIhE"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Call-lowering plugin registry (#2057).
3
+ *
4
+ * The compiler core carries no bespoke per-API recognition branches. Instead, a
5
+ * lowering plugin *recognises* a call — by the import it comes from and its
6
+ * argument shape — and returns a **backend-neutral `LoweringNode`**. Each adapter
7
+ * renders that node in its own template syntax. This is a deliberate two-layer
8
+ * split:
9
+ *
10
+ * - **Layer 1 (this module + plugins):** adapter-agnostic. A plugin matches a
11
+ * call to a neutral node and never mentions Go/Perl/… syntax.
12
+ * - **Layer 2 (adapters):** plugin-agnostic. Each adapter has ONE renderer per
13
+ * node kind, so SSR/CSR parity is enforced once, not per plugin.
14
+ *
15
+ * Everything flows through this one seam — including first-party APIs. A
16
+ * userland package registers its plugin via {@link registerLoweringPlugin}, and
17
+ * a built-in like `queryHref` is registered by the compiler *as a default
18
+ * plugin* (see `builtin-lowering-plugins.ts`), not as an adapter branch. So an
19
+ * adapter can't tell a shipped API from a third-party one: both are just entries
20
+ * in this registry. That uniformity is the point — there is no "special" path to
21
+ * keep in sync.
22
+ *
23
+ * This is NOT the "output-rewriting hook" CLAUDE.md forbids: a plugin returns a
24
+ * structured IR node, never a rewritten output string, so the compiler's output
25
+ * stays determined by the compiler, not by whatever munges the emitted text.
26
+ */
27
+ import type { ParsedExpr } from './expression-parser.ts';
28
+ import type { IRMetadata } from './types.ts';
29
+ /**
30
+ * A backend-neutral include triple for a {@link LoweringNode} `guard-list`.
31
+ * `guard` is the conditional test of a `key: cond ? v : <omit>` include, or null
32
+ * for a plain `key: v` (included purely on value-truthiness). An adapter renders
33
+ * the guard to decide inclusion; the *runtime helper* then applies the emptiness
34
+ * / array-append rules to the value. (Structurally identical to the query-href
35
+ * lowering's own triple, which the `queryHref` plugin passes through unchanged.)
36
+ */
37
+ export interface LoweringTriple {
38
+ guard: ParsedExpr | null;
39
+ key: string;
40
+ value: ParsedExpr;
41
+ }
42
+ /**
43
+ * A backend-neutral lowering result. Adapters render each variant in their own
44
+ * template language; the shapes carry everything a renderer needs and nothing
45
+ * adapter-specific.
46
+ */
47
+ export type LoweringNode =
48
+ /**
49
+ * A guard/key/value include list lowered to a query helper — the shape of
50
+ * `queryHref(base, { … })`. `helper` is the logical helper id (`'query'`),
51
+ * which each adapter maps to its own runtime helper (`bf_query` in go,
52
+ * `bf->query` in mojo, `$bf.query` in xslate). Each triple's `guard` controls
53
+ * inclusion; the runtime helper then applies the non-empty / array-append
54
+ * rules to the value (so an included-but-empty value is dropped and array
55
+ * members are appended), matching the client `queryHref` exactly. Adapters
56
+ * MUST switch on `helper` — a `guard-list` is not implicitly `query`.
57
+ */
58
+ {
59
+ kind: 'guard-list';
60
+ helper: string;
61
+ base: ParsedExpr;
62
+ triples: LoweringTriple[];
63
+ }
64
+ /**
65
+ * A plain helper call `helper(...args)` — the general escape hatch for a pure
66
+ * builder that lowers to a single runtime-helper invocation. Unused today;
67
+ * present so the neutral vocabulary isn't single-purpose.
68
+ */
69
+ | {
70
+ kind: 'helper-call';
71
+ helper: string;
72
+ args: readonly ParsedExpr[];
73
+ };
74
+ /**
75
+ * A matcher bound to one component's metadata: given a parsed call's callee +
76
+ * args, returns a neutral node or null to decline. Produced by a plugin's
77
+ * {@link LoweringPlugin.prepare} so the per-component import-name resolution runs
78
+ * once (at adapter init), not on every emit.
79
+ */
80
+ export type LoweringMatcher = (callee: ParsedExpr, args: readonly ParsedExpr[]) => LoweringNode | null;
81
+ /**
82
+ * A lowering plugin. `prepare` resolves the local names its import is bound
83
+ * under in this component and returns a bound {@link LoweringMatcher}, or null
84
+ * when the component doesn't use it (so the adapter skips it entirely). A
85
+ * plugin never emits adapter syntax — only neutral nodes.
86
+ */
87
+ export interface LoweringPlugin {
88
+ /** Stable id, for dedup/diagnostics (e.g. `'my-pkg-url'`). */
89
+ name: string;
90
+ prepare(metadata: IRMetadata): LoweringMatcher | null;
91
+ }
92
+ /**
93
+ * Register a lowering plugin. Idempotent by `name` — re-registering the same
94
+ * name replaces the prior plugin, so a double side-effect import can't stack
95
+ * duplicates. First-party packages call this at module load.
96
+ */
97
+ export declare function registerLoweringPlugin(plugin: LoweringPlugin): void;
98
+ /** The registered plugins, in registration order (a copy — mutating the result
99
+ * can't reorder or corrupt the registry). */
100
+ export declare function getLoweringPlugins(): readonly LoweringPlugin[];
101
+ /**
102
+ * Bind every registered plugin to a component's metadata, returning the matchers
103
+ * that are active for it (import present). Adapters call this once at init and
104
+ * store the result, then try each matcher on the calls they lower — replacing a
105
+ * hardcoded per-API recognizer.
106
+ */
107
+ export declare function prepareLoweringMatchers(metadata: IRMetadata): LoweringMatcher[];
108
+ /**
109
+ * Convenience one-shot match against all registered plugins for a given
110
+ * metadata. Prefer {@link prepareLoweringMatchers} on a hot path (it resolves
111
+ * import names once); this re-resolves per call and is meant for tests / cold
112
+ * call sites.
113
+ */
114
+ export declare function matchLoweringCall(callee: ParsedExpr, args: readonly ParsedExpr[], metadata: IRMetadata): LoweringNode | null;
115
+ /**
116
+ * Test-only: replace the registry contents wholesale. The double-underscore
117
+ * prefix marks it as an internal seam — tests use it to restore global state in
118
+ * `afterEach` so a sample plugin can't leak into other suites. Never call from
119
+ * production code.
120
+ */
121
+ export declare function __resetLoweringPluginsForTest(next?: readonly LoweringPlugin[]): void;
122
+ //# sourceMappingURL=lowering-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lowering-registry.d.ts","sourceRoot":"","sources":["../src/lowering-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,UAAU,CAAA;CAClB;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY;AACtB;;;;;;;;;GASG;AACD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,cAAc,EAAE,CAAA;CAAE;AACrF;;;;GAIG;GACD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAAA;AAExE;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,SAAS,UAAU,EAAE,KACxB,YAAY,GAAG,IAAI,CAAA;AAExB;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,QAAQ,EAAE,UAAU,GAAG,eAAe,GAAG,IAAI,CAAA;CACtD;AAID;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAInE;AAED;8CAC8C;AAC9C,wBAAgB,kBAAkB,IAAI,SAAS,cAAc,EAAE,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,UAAU,GAAG,eAAe,EAAE,CAO/E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,QAAQ,EAAE,UAAU,GACnB,YAAY,GAAG,IAAI,CAMrB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,GAAE,SAAS,cAAc,EAAO,GAAG,IAAI,CAGxF"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Backend-neutral destructuring of a recognised `queryHref(base, { … })` call
3
+ * (#2042) into a base expression plus include triples, shared by the SSR
4
+ * adapters' query lowering.
5
+ *
6
+ * `queryHref` is the pure functional URL-query builder (the counterpart to
7
+ * `searchParams()`); its call + object literal are already structured IR, so an
8
+ * adapter lowers it to its query helper without any block-body recognition or
9
+ * re-parse. This module only does the structural match — turning the object
10
+ * literal's properties into `{ guard, key, value }` triples — leaving each
11
+ * adapter to format the include condition and the helper call in its own
12
+ * template language.
13
+ *
14
+ * Inclusion is truthy-omit over string values (matching the client `queryHref`'s
15
+ * `if (value)`): a plain `key: v` is included iff `v` is a non-empty string
16
+ * (`guard: null`); a conditional `key: cond ? a : <undefined|null|''>` is
17
+ * included iff `cond` AND `a` is non-empty (`guard: cond`, `value: a`).
18
+ */
19
+ import type { ParsedExpr } from './expression-parser.ts';
20
+ export interface QueryHrefTriple {
21
+ /**
22
+ * The conditional test of a `key: cond ? a : <omit>` include, or null for a
23
+ * plain `key: v` (which is included purely on value-truthiness). An adapter
24
+ * combines this with the value's non-emptiness to form the include condition.
25
+ */
26
+ guard: ParsedExpr | null;
27
+ /** The literal search-param key. */
28
+ key: string;
29
+ /** The value expression (the consequent for a conditional include). */
30
+ value: ParsedExpr;
31
+ }
32
+ export interface QueryHrefCall {
33
+ base: ParsedExpr;
34
+ triples: QueryHrefTriple[];
35
+ }
36
+ /**
37
+ * Match a `queryHref(base, { … })` call from its callee + args, returning the
38
+ * base and include triples, or null when it isn't a `queryHref` call with a
39
+ * plain object-literal second argument (→ the adapter falls back to its generic
40
+ * lowering). `localNames` are the bindings `queryHref` is imported under (from
41
+ * `queryHrefLocalNames`).
42
+ */
43
+ export declare function matchQueryHrefCall(callee: ParsedExpr, args: readonly ParsedExpr[], localNames: ReadonlySet<string>): QueryHrefCall | null;
44
+ /**
45
+ * Format a {@link QueryHrefCall} as the flat argument list for a guard-list
46
+ * query helper (`bf->query(base, guard, key, value, …)` in Mojo / `$bf.query(…)`
47
+ * in Xslate — the two adapters whose helper does the non-empty check itself).
48
+ * Each triple contributes a guard (`'1'` for a plain include, or the lowered
49
+ * condition for a conditional one), the key as a string literal, and the value —
50
+ * all lowered through the adapter's `emit`. The caller wraps the result in its
51
+ * own `<helper>(…)` call. (The go-template adapter folds the non-empty check
52
+ * into the include condition itself, so it formats its own form instead.)
53
+ *
54
+ * A conditional guard that is NOT already boolean-shaped (a bare value, a member
55
+ * access, `&&`/`||`) is JS *string* truthiness — `'0'` is a truthy string in JS
56
+ * but false under Perl's `unless`. To keep SSR byte-identical to the client (and
57
+ * to the go adapter, whose `lowerUrlGuard` does the same), such a guard is
58
+ * normalised to a `guard !== ''` test, emitted against a string literal so each
59
+ * adapter renders string `ne`, not numeric `!=`. Comparisons / `!x` / boolean
60
+ * literals already yield a real boolean and pass through unchanged.
61
+ */
62
+ export declare function queryHrefArgs(q: QueryHrefCall, emit: (e: ParsedExpr) => string): string[];
63
+ //# sourceMappingURL=query-href-lowering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-href-lowering.d.ts","sourceRoot":"","sources":["../src/query-href-lowering.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAExD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACxB,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,uEAAuE;IACvE,KAAK,EAAE,UAAU,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,EAAE,eAAe,EAAE,CAAA;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,GAC9B,aAAa,GAAG,IAAI,CAkBtB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,EAAE,CAoBzF"}
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-defaults.d.ts","sourceRoot":"","sources":["../src/ssr-defaults.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,KAAK,EAAE,OAAO,CAAA;IACd;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS,CAmG/F"}
1
+ {"version":3,"file":"ssr-defaults.d.ts","sourceRoot":"","sources":["../src/ssr-defaults.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,KAAK,EAAE,OAAO,CAAA;IACd;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS,CAuG/F"}
@@ -0,0 +1,84 @@
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
+ import { type EnvSignalReader } from './adapters/env-signal.ts';
33
+ import { type ParsedExpr } from './expression-parser.ts';
34
+ import type { IRMetadata } from './types.ts';
35
+ /**
36
+ * One binding in component declaration order (signals first, then memos —
37
+ * matching `IRMetadata` order and the adapters' iteration).
38
+ *
39
+ * - `env-reader`: an env signal whose `envReader` key resolves in the shared
40
+ * registry (`envSignalReaderFor`). The runtime provides the per-request
41
+ * reader, so there is nothing to seed; the name still enters scope so a
42
+ * later derived step may reference it. An `envReader` key UNKNOWN to the
43
+ * registry falls through to the normal derived/opaque rules instead.
44
+ * - `derived`: the binding's value expression is a supported shape whose free
45
+ * identifiers are all in scope at this point (baseScope + earlier steps) —
46
+ * an adapter may seed it in-template by lowering `parsed`/`expr`.
47
+ * - `opaque`: not seedable this way (empty init, unsupported shape,
48
+ * unanalyzable free set, out-of-scope reference, or a block-bodied memo).
49
+ * The name still enters scope for later steps; adapters keep their static
50
+ * ssr-defaults seeding for it.
51
+ */
52
+ export type SsrSeedStep = {
53
+ kind: 'env-reader';
54
+ name: string;
55
+ reader: EnvSignalReader;
56
+ } | {
57
+ kind: 'derived';
58
+ name: string;
59
+ origin: 'signal' | 'memo';
60
+ expr: string;
61
+ parsed: ParsedExpr;
62
+ frees: string[];
63
+ } | {
64
+ kind: 'opaque';
65
+ name: string;
66
+ origin: 'signal' | 'memo';
67
+ };
68
+ export interface SsrSeedPlan {
69
+ /**
70
+ * Names in scope before any step: props params, the props-object name
71
+ * (when the component takes an undestructured props object), and module
72
+ * pure-string consts (compile-time inlined by every adapter).
73
+ */
74
+ baseScope: string[];
75
+ steps: SsrSeedStep[];
76
+ }
77
+ /**
78
+ * Compute the component's SSR seed plan from its metadata. See the module
79
+ * doc for the contract. Memo steps are gated to EXPRESSION-BODIED memos
80
+ * (`extractArrowBodyExpression` returns the body): a block-bodied memo is
81
+ * `opaque` even when the analyzer folded it to a `parsed` expression.
82
+ */
83
+ export declare function computeSsrSeedPlan(metadata: IRMetadata): SsrSeedPlan;
84
+ //# sourceMappingURL=ssr-seed-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr-seed-plan.d.ts","sourceRoot":"","sources":["../src/ssr-seed-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAA;AACnF,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/G;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAA;CAAE,CAAA;AAE/D,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,KAAK,EAAE,WAAW,EAAE,CAAA;CACrB;AA0BD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,GAAG,WAAW,CAwCpE"}
package/dist/types.d.ts CHANGED
@@ -3,7 +3,25 @@
3
3
  *
4
4
  * JSX-independent intermediate representation for multi-backend support.
5
5
  */
6
- import type { ParsedExpr, ParsedStatement, SortComparator } from './expression-parser.ts';
6
+ import type { ParsedExpr, ParsedStatement } from './expression-parser.ts';
7
+ import type { SsrSeedPlan } from './ssr-seed-plan.ts';
8
+ /**
9
+ * Loop-hoisted sort comparator for the `.sort().map()` / `.toSorted().map()`
10
+ * pattern (#2018 P5). Carries the generic comparator `arrow` (params + body)
11
+ * that the SSR adapter serializes to the runtime evaluator (eval-first) or, for
12
+ * a `localeCompare` comparator the evaluator can't model, recovers a structured
13
+ * comparator from via `sortComparatorFromArrow`. The `paramA` / `paramB` / `raw`
14
+ * fields round-trip the comparator to native JS for the client / CSR path
15
+ * (`(paramA, paramB) => raw`), so the client is untouched.
16
+ */
17
+ export type IRLoopSort = {
18
+ arrow: Extract<ParsedExpr, {
19
+ kind: 'arrow';
20
+ }>;
21
+ paramA: string;
22
+ paramB: string;
23
+ raw: string;
24
+ };
7
25
  export interface Position {
8
26
  line: number;
9
27
  column: number;
@@ -201,6 +219,20 @@ export interface IRExpression {
201
219
  expr: string;
202
220
  /** Pre-transformed expr with destructured prop refs rewritten to _p.xxx (for client JS templates). */
203
221
  templateExpr?: string;
222
+ /**
223
+ * Structured parse of `expr` (`parseExpression(expr.trim())`), attached once
224
+ * during IR construction so SSR adapters emit from the tree instead of each
225
+ * re-parsing the string at emit time (and so a multi-adapter build parses it
226
+ * once, not per adapter). Plain serializable data.
227
+ *
228
+ * OPTIONAL by design — consumers MUST fall back to parsing `expr` when it is
229
+ * missing. It is absent for an empty/whitespace `expr`, and may also be
230
+ * absent for a node the IR-build walk doesn't reach (the walk is best-effort;
231
+ * under-coverage is a missed optimization, never a behavioural change). When
232
+ * present, an unparsable expression is a `{ kind: 'unsupported' }` node (the
233
+ * adapter's own support gate handles it).
234
+ */
235
+ parsed?: ParsedExpr;
204
236
  typeInfo: TypeInfo | null;
205
237
  reactive: boolean;
206
238
  slotId: string | null;
@@ -224,6 +256,13 @@ export interface IRConditional {
224
256
  condition: string;
225
257
  /** Pre-transformed condition with destructured prop refs rewritten to _p.xxx. */
226
258
  templateCondition?: string;
259
+ /**
260
+ * Structured parse of `condition` (`parseExpression(condition.trim())`),
261
+ * attached during IR construction so adapters lower the condition from the
262
+ * tree instead of re-parsing the string. Optional/best-effort — see
263
+ * `IRExpression.parsed`; consumers fall back to parsing `condition`.
264
+ */
265
+ parsedCondition?: ParsedExpr;
227
266
  conditionType: TypeInfo | null;
228
267
  reactive: boolean;
229
268
  whenTrue: IRNode;
@@ -275,6 +314,14 @@ export interface IRLoop {
275
314
  */
276
315
  method?: 'flatMap';
277
316
  array: string;
317
+ /**
318
+ * Structured parse of `array` (`parseExpression(array.trim())`), attached
319
+ * during IR construction so adapters lower the loop's array from the tree
320
+ * instead of re-parsing the string (e.g. the Go adapter's scalar-literal
321
+ * loop typing). Optional/best-effort — mirrors `IRExpression.parsed`;
322
+ * consumers fall back to parsing `array`.
323
+ */
324
+ arrayParsed?: ParsedExpr;
278
325
  /** Pre-transformed array expr with destructured prop refs rewritten to _p.xxx. */
279
326
  templateArray?: string;
280
327
  arrayType: TypeInfo | null;
@@ -336,14 +383,16 @@ export interface IRLoop {
336
383
  * When present, the loop renders with an if-condition wrapping each iteration.
337
384
  * Example: todos.filter(t => !t.done).map(...) stores { param: 't', predicate: ParsedExpr, raw: '!t.done' }
338
385
  *
339
- * For block-body filters like:
386
+ * Block-body filters like
340
387
  * filter(t => { const f = filter(); if (f === 'active') return !t.done; return true })
341
- * The blockBody field contains the parsed statements.
388
+ * are normalized to a single boolean `predicate` expression at IR-build time
389
+ * (#2040, `foldBlockToExpr` + `predicateTernaryToLogical` in `jsx-to-ir`), so
390
+ * adapters only ever see the unified expression form — there is no separate
391
+ * block-statement shape to lower.
342
392
  */
343
393
  filterPredicate?: {
344
394
  param: string;
345
395
  predicate?: ParsedExpr;
346
- blockBody?: ParsedStatement[];
347
396
  raw: string;
348
397
  };
349
398
  /**
@@ -351,14 +400,14 @@ export interface IRLoop {
351
400
  * When present, the loop array is sorted before iteration.
352
401
  * Example: todos.sort((a, b) => a.priority - b.priority).map(...)
353
402
  *
354
- * The structured shape carries enough info for both adapters to
355
- * emit the same `bf_sort` / `bf->sort` call (`SortComparator` is
356
- * defined in `expression-parser.ts` because the standalone
357
- * `array-method` IR variant uses the same type). The loop-hoist
358
- * path lifts a comparator off a sibling `array-method` node
359
- * during `jsx-to-ir.ts` chain detection — see `extractSortComparator`.
403
+ * The {@link IRLoopSort} struct carries the generic comparator `arrow`
404
+ * (params + body) the SSR adapter serializes to the runtime evaluator
405
+ * (eval-first; `sortComparatorFromArrow` fallback for `localeCompare`), plus
406
+ * the param names + raw body for the client JS round-trip. Lifted off the
407
+ * `.sort()` callback during `jsx-to-ir.ts` chain detection see
408
+ * `extractSortComparator`. (#2018 P5)
360
409
  */
361
- sortComparator?: SortComparator;
410
+ sortComparator?: IRLoopSort;
362
411
  /**
363
412
  * When both filter and sort are chained, indicates the order of operations.
364
413
  * 'filter-sort': filter first, then sort (e.g., filter().sort().map())
@@ -595,6 +644,13 @@ export interface IRIfStatement {
595
644
  condition: string;
596
645
  /** Pre-transformed condition with destructured prop refs rewritten to _p.xxx. */
597
646
  templateCondition?: string;
647
+ /**
648
+ * Structured parse of `condition` (`parseExpression(condition.trim())`),
649
+ * attached during IR construction so adapters lower the condition from the
650
+ * tree instead of re-parsing the string. Optional/best-effort — see
651
+ * `IRExpression.parsed`; consumers fall back to parsing `condition`.
652
+ */
653
+ parsedCondition?: ParsedExpr;
598
654
  /** The JSX return in the then branch */
599
655
  consequent: IRNode;
600
656
  /** The else branch: either another IRIfStatement (else if) or IRNode (final else) */
@@ -686,6 +742,14 @@ export interface ExpressionAttr {
686
742
  /** `expr` with destructured prop refs rewritten to `_p.xxx`, for SSR
687
743
  * template inlining. Absent when no rewrite was needed. */
688
744
  templateExpr?: string;
745
+ /**
746
+ * Structured parse of `expr` (`parseExpression(expr.trim())`), attached
747
+ * during IR construction so adapters lower the attribute value from the tree
748
+ * instead of re-parsing the string (often several times per attribute).
749
+ * Optional/best-effort — see `IRExpression.parsed`; consumers fall back to
750
+ * parsing `expr`.
751
+ */
752
+ parsed?: ParsedExpr;
689
753
  /** Set when the producer peeled an `expr || undefined` boolean-presence
690
754
  * pattern; adapters fold this back into `(expr) || undefined` at emit. */
691
755
  presenceOrUndefined?: boolean;
@@ -716,6 +780,16 @@ export interface SpreadAttr {
716
780
  kind: 'spread';
717
781
  expr: string;
718
782
  templateExpr?: string;
783
+ /**
784
+ * Structured parse of `expr` (`parseExpression(expr.trim())`), attached
785
+ * during IR construction so adapters lower the spread bag from the tree
786
+ * instead of re-parsing the string with `ts.createSourceFile`. Optional /
787
+ * best-effort — mirrors `ExpressionAttr.parsed`: it may be absent (a node the
788
+ * attach walk misses, or an empty `expr`), and parsing may yield
789
+ * `{ kind: 'unsupported' }`, which adapters treat as unlowerable and handle
790
+ * via their existing non-conditional spread paths (or BF101).
791
+ */
792
+ parsed?: ParsedExpr;
719
793
  /**
720
794
  * Component-scoped, stable slot ID assigned at IR-build time for
721
795
  * adapters that need to plumb the spread bag through a structured
@@ -837,6 +911,14 @@ export interface SignalInfo {
837
911
  getter: string;
838
912
  setter: string | null;
839
913
  initialValue: string;
914
+ /**
915
+ * `initialValue` parsed into a structured tree (Roadmap A). Attached
916
+ * best-effort by the analyzer so adapters can lower a literal initial value
917
+ * (e.g. `useState(['a', 'b'])`) from structure instead of re-parsing the
918
+ * string with `ts.createSourceFile`. Absent when the shape isn't supported;
919
+ * consumers fall back to parsing `initialValue`.
920
+ */
921
+ parsed?: ParsedExpr;
840
922
  /** Initial value with TypeScript type annotations preserved, for .tsx output */
841
923
  typedInitialValue?: string;
842
924
  type: TypeInfo;
@@ -873,12 +955,78 @@ export interface SignalInfo {
873
955
  isModule?: boolean;
874
956
  /** When true, the declaration carries an `export` keyword. */
875
957
  isExported?: boolean;
958
+ /**
959
+ * Request-scoped environment-signal key when this signal was produced by an
960
+ * env-signal factory (`createSearchParams()` → `'search'`), rather than by
961
+ * `createSignal`. Set structurally by the analyzer (#2057) — the getter is a
962
+ * normal reactive getter (so it lands in the fold purity oracle for free, no
963
+ * name allow-list), but its *value* is a request-scoped reader with methods
964
+ * (`.get(key)`), which adapters lower to their per-request reader object
965
+ * instead of a plain template field. This flag is how adapters recognise an
966
+ * env signal from structure instead of matching the import name.
967
+ */
968
+ envReader?: string;
969
+ /**
970
+ * For an env signal (`envReader` set), the exact callee text of its factory
971
+ * call as written — `'createSearchParams'`, an alias (`'csp'` for
972
+ * `import { createSearchParams as csp }`), or a namespace access
973
+ * (`'bf.createSearchParams'`). Backends that re-emit the declaration (client
974
+ * JS, JSX/Hono SSR) emit `<envFactory>()` so the call resolves to the binding
975
+ * actually in scope, not a hardcoded canonical name (#2057).
976
+ */
977
+ envFactory?: string;
876
978
  }
877
979
  export interface MemoInfo {
878
980
  name: string;
879
981
  computation: string;
880
982
  /** Computation with TypeScript type annotations preserved, for .tsx output */
881
983
  typedComputation?: string;
984
+ /**
985
+ * Structured parse of the memo's BODY as a single value expression, computed
986
+ * once at analysis time. Lets adapters pattern-match the memo's shape on the
987
+ * structured tree instead of re-parsing `computation` with their own AST walks
988
+ * / regexes.
989
+ *
990
+ * Set for an expression-bodied arrow (`() => <body>`) whose body
991
+ * `parseExpression` supports, AND — since #2040 — for a block-bodied memo
992
+ * (`() => { … }`) whose statements `foldBlockToExpr` can normalize to one
993
+ * expression (`let`-inline + value `if` / early `return` → ternary). A block
994
+ * the fold refuses (imperative residue) or a shape `parseExpression` can't
995
+ * represent leaves `parsed` undefined, so consumers must still fall back to
996
+ * `parsedBlock` / `computation` when it's missing. NOTE: a present `parsed`
997
+ * therefore no longer implies an expression-bodied arrow.
998
+ */
999
+ parsed?: ParsedExpr;
1000
+ /**
1001
+ * Whether the memo's effective body is a template literal (`() => `…`` or a
1002
+ * block body whose first `return` is one), classified once at analysis time
1003
+ * from the real arrow AST. Lets the Go adapter pick the `string` field type
1004
+ * without re-parsing `computation` with `ts.createSourceFile`. A template
1005
+ * literal — including a no-substitution `` `plain` `` — folds to a plain
1006
+ * string `ParsedExpr` literal, losing the backtick distinction, so this is a
1007
+ * dedicated flag rather than a `parsed.kind` check.
1008
+ */
1009
+ bodyIsTemplateLiteral?: boolean;
1010
+ /**
1011
+ * A block-bodied memo's statements, parsed best-effort (tolerant: a statement
1012
+ * the parser can't represent is omitted). Lets the Go adapter pattern-match
1013
+ * block-body memo shapes — e.g. the `const k = getter(); if (!k) return CONST`
1014
+ * guard — on the structured statements instead of re-parsing `computation`
1015
+ * with `ts.createSourceFile`. Absent for expression-bodied memos (those carry
1016
+ * `parsed` instead) and when the arrow has no block body.
1017
+ */
1018
+ parsedBlock?: ParsedStatement[];
1019
+ /**
1020
+ * Whether {@link parsedBlock} represents EVERY statement of the block (true)
1021
+ * or the tolerant parser omitted at least one it couldn't represent (false).
1022
+ * A consumer that must reason about the whole block — e.g. one that bails on
1023
+ * any statement it doesn't recognise (the template-literal memo lowering) —
1024
+ * reads this and falls back when it's `false`, since omitted statements are
1025
+ * otherwise invisible. Consumers that scan for a recognised prefix and ignore
1026
+ * the rest (the guard-and-return-const lowering) can disregard it. Only set
1027
+ * when `parsedBlock` is.
1028
+ */
1029
+ parsedBlockComplete?: boolean;
882
1030
  type: TypeInfo;
883
1031
  deps: string[];
884
1032
  loc: SourceLocation;
@@ -1069,6 +1217,17 @@ export interface FunctionInfo {
1069
1217
  export interface ConstantInfo {
1070
1218
  name: string;
1071
1219
  value?: string;
1220
+ /**
1221
+ * `value` parsed into a structured tree (Roadmap A). Attached best-effort by
1222
+ * the analyzer (parsed from the parenthesised value so a bare object literal
1223
+ * — which TS reads as a block at statement position — resolves to an
1224
+ * `object-literal` rather than failing). Lets adapters lower a constant value
1225
+ * (e.g. a module-scope record's `{ … }`) from structure instead of
1226
+ * re-parsing the string with `ts.createSourceFile`. Absent when the constant
1227
+ * has no `value` string (e.g. an inlined-JSX const) or when the analyzer
1228
+ * couldn't structure it (best-effort — consumers fall back to the string).
1229
+ */
1230
+ parsed?: ParsedExpr;
1072
1231
  /** Value with TypeScript type annotations preserved, for .tsx output */
1073
1232
  typedValue?: string;
1074
1233
  valueBranches?: string[];
@@ -1152,6 +1311,16 @@ export interface IRMetadata {
1152
1311
  * resolves the compiled module rather than the source `.tsx`.
1153
1312
  */
1154
1313
  clientSignalImportSources?: Set<string>;
1314
+ /**
1315
+ * Backend-neutral SSR seed plan: per-binding derived/opaque/env-reader
1316
+ * classification in declaration order, plus the base scope, computed by
1317
+ * `computeSsrSeedPlan` from this metadata. Attached by `buildMetadata` and
1318
+ * serialized into IR JSON like the rest of the metadata; template adapters
1319
+ * consume it instead of re-deriving scope/derivability themselves (their
1320
+ * target-syntax choices — lowering, self-shadow rules, constant-emit
1321
+ * guards — stay adapter-side).
1322
+ */
1323
+ ssrSeedPlan?: SsrSeedPlan;
1155
1324
  }
1156
1325
  /**
1157
1326
  * Where a reference appears in the emitted client JS. The emitter's rule