@barefootjs/jsx 0.33.1 → 0.33.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 (65) hide show
  1. package/dist/analyzer.d.ts +17 -0
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/compiler.d.ts +21 -5
  4. package/dist/compiler.d.ts.map +1 -1
  5. package/dist/expression-parser.d.ts +14 -0
  6. package/dist/expression-parser.d.ts.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +817 -457
  10. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/html-template.d.ts +7 -7
  14. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/imports.d.ts +60 -2
  16. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/prop-handling.d.ts +30 -0
  19. package/dist/ir-to-client-js/prop-handling.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/reactivity.d.ts +5 -0
  21. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/rewrite-props-object.d.ts +36 -8
  23. package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/utils.d.ts +26 -2
  25. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  26. package/dist/jsx-to-ir.d.ts.map +1 -1
  27. package/dist/props-binding.d.ts +35 -0
  28. package/dist/props-binding.d.ts.map +1 -1
  29. package/dist/types.d.ts +51 -0
  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 +165 -98
  33. package/src/__tests__/binding-scope-ratchet.test.ts +5 -1
  34. package/src/__tests__/child-component-ref-not-mirrored.test.ts +90 -0
  35. package/src/__tests__/client-js-generation.test.ts +11 -0
  36. package/src/__tests__/ir-to-client-js/imports.test.ts +107 -0
  37. package/src/__tests__/ir-to-client-js/merge-compiled-client-js-imports.test.ts +138 -0
  38. package/src/__tests__/issue-2723-prop-alias-reactivity.test.ts +124 -0
  39. package/src/__tests__/issue-2754-rest-spread-needs-slot.test.ts +85 -0
  40. package/src/__tests__/issue-2756-loop-row-honors-client-only.test.ts +173 -0
  41. package/src/__tests__/merge-template-imports.test.ts +41 -1
  42. package/src/__tests__/multi-component-shared-default-import.test.ts +55 -0
  43. package/src/__tests__/rewrite-props-object.test.ts +41 -4
  44. package/src/__tests__/root-key-relay.test.ts +170 -0
  45. package/src/__tests__/signal-getter-not-called.test.ts +149 -0
  46. package/src/__tests__/state-only-file-default-import.test.ts +47 -0
  47. package/src/analyzer.ts +36 -0
  48. package/src/compiler.ts +94 -104
  49. package/src/expression-parser.ts +26 -0
  50. package/src/index.ts +2 -2
  51. package/src/ir-to-client-js/collect-elements.ts +45 -30
  52. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +6 -2
  53. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -2
  54. package/src/ir-to-client-js/emit-registration.ts +26 -7
  55. package/src/ir-to-client-js/generate-init.ts +1 -1
  56. package/src/ir-to-client-js/html-template.ts +130 -20
  57. package/src/ir-to-client-js/imports.ts +178 -5
  58. package/src/ir-to-client-js/index.ts +11 -4
  59. package/src/ir-to-client-js/prop-handling.ts +83 -0
  60. package/src/ir-to-client-js/reactivity.ts +59 -0
  61. package/src/ir-to-client-js/rewrite-props-object.ts +50 -10
  62. package/src/ir-to-client-js/utils.ts +30 -2
  63. package/src/jsx-to-ir.ts +523 -49
  64. package/src/props-binding.ts +51 -0
  65. package/src/types.ts +48 -0
@@ -5,6 +5,89 @@
5
5
  import type { ParamInfo, SignalInfo } from '../types.ts'
6
6
  import type { ClientJsContext } from './types.ts'
7
7
  import type { BindingScope } from '../scope/binding-scope.ts'
8
+ import { resolveRestSpreadOriginCore } from '../props-binding.ts'
9
+
10
+ /**
11
+ * Which of the component's two "forwards the caller's leftover props"
12
+ * bindings `name` ultimately names — `'rest'` for `ctx.restPropsName`
13
+ * (the destructured `...rest` binding), `'props'` for `ctx.propsObjectName`
14
+ * (a whole undestructured `(props)` parameter spread whole), or `null` when
15
+ * `name` is neither, walking through any bare `const x__alias = <name>`
16
+ * alias chain to get there (#2723's `alias-props` mutation aliases every
17
+ * destructured binding, the rest parameter included, e.g.
18
+ * `const props__alias = props`).
19
+ *
20
+ * A `{...spread}` attribute is recognised as "forwards the caller's
21
+ * leftover props" (routed to the `applyRestAttrs` runtime helper /
22
+ * excluded from the SSR template's `spreadAttrs({...})` merge, and — only
23
+ * for the `'rest'` case — given the destructured prop names to exclude
24
+ * from what it forwards) by comparing its source expression against
25
+ * exactly `ctx.restPropsName` / `ctx.propsObjectName`. Without this
26
+ * resolver an alias hop makes that comparison fail even though the spread
27
+ * still forwards the SAME object — `collect-elements.ts` then never
28
+ * registers the rest-attrs application at all, and `html-template.ts`'s
29
+ * merge path stops filtering the spread out, folding it into a
30
+ * `spreadAttrs({...})` call keyed by the alias name instead of the
31
+ * runtime-visible one.
32
+ *
33
+ * The walk itself lives in `props-binding.ts`'s `resolveRestSpreadOriginCore`,
34
+ * shared with Phase 1's slot-id decision so both phases agree on which
35
+ * spreads forward the caller's leftover props (#2754). This wrapper only
36
+ * supplies the `ClientJsContext`-shaped inputs.
37
+ */
38
+ export function resolveRestSpreadOrigin(ctx: ClientJsContext, name: string): 'rest' | 'props' | null {
39
+ return resolveRestSpreadOriginCore(ctx, localConstantValues(ctx), name)
40
+ }
41
+
42
+ /**
43
+ * `ctx.localConstants` indexed by name, memoized per `ctx`.
44
+ *
45
+ * A `Map` rather than the `.find(` this file's other two constant lookups
46
+ * use, deliberately: those two are SHADOW-GUARDED lookups that
47
+ * `binding-scope-ratchet.test.ts` deliberately counts, and that ledger is
48
+ * shrink-only and at its floor. Resolving an alias chain hop-by-hop would
49
+ * have added a third counted use — and a hot one, since the walk queries
50
+ * once per hop — so it indexes instead, which is both outside the ledger's
51
+ * concern and cheaper than a linear scan per hop.
52
+ */
53
+ const _localConstantValuesCache: WeakMap<ClientJsContext, ReadonlyMap<string, string | undefined>> = new WeakMap()
54
+
55
+ function localConstantValues(ctx: ClientJsContext): ReadonlyMap<string, string | undefined> {
56
+ const cached = _localConstantValuesCache.get(ctx)
57
+ if (cached) return cached
58
+ const byName = new Map<string, string | undefined>()
59
+ for (const constant of ctx.localConstants) {
60
+ if (!byName.has(constant.name)) byName.set(constant.name, constant.value)
61
+ }
62
+ _localConstantValuesCache.set(ctx, byName)
63
+ return byName
64
+ }
65
+
66
+ /**
67
+ * Every name that resolves (via `resolveRestSpreadOrigin`) to either of the
68
+ * component's "forwards the caller's leftover props" bindings — used where
69
+ * callers need SET membership (`restSpreadNames?.has(...)` in
70
+ * `html-template.ts`) rather than a per-name resolution. Memoized per
71
+ * `ctx` (`WeakMap`, mirroring `free-refs.ts`'s `_bindingMapCache`) since
72
+ * some callers build this once per component and query it while walking
73
+ * the whole tree.
74
+ */
75
+ const _restSpreadNamesCache: WeakMap<ClientJsContext, ReadonlySet<string>> = new WeakMap()
76
+
77
+ export function resolveRestSpreadNames(ctx: ClientJsContext): ReadonlySet<string> {
78
+ const cached = _restSpreadNamesCache.get(ctx)
79
+ if (cached) return cached
80
+
81
+ const names = new Set<string>()
82
+ if (ctx.restPropsName) names.add(ctx.restPropsName)
83
+ if (ctx.propsObjectName) names.add(ctx.propsObjectName)
84
+ for (const constant of ctx.localConstants) {
85
+ if (resolveRestSpreadOrigin(ctx, constant.name) !== null) names.add(constant.name)
86
+ }
87
+
88
+ _restSpreadNamesCache.set(ctx, names)
89
+ return names
90
+ }
8
91
 
9
92
  /**
10
93
  * Expand dynamic prop value by resolving local constants.
@@ -174,11 +174,30 @@ export function decideWrapForChildProp(
174
174
  * The signal-getter and memo regexes (`\b<name>\s*\(`) still run against
175
175
  * the raw string — those are call-shape patterns, not bare-identifier
176
176
  * checks, and are outside the scope of #1267.
177
+ *
178
+ * A reference that only reaches a signal/memo/prop through an intervening
179
+ * `const x__alias = x` hop is walked via `ctx.localConstants` below (#2723)
180
+ * — see that block's own comment for why this lives here rather than on
181
+ * `BindingScope` or a new tracking structure.
177
182
  */
178
183
  export function needsEffectWrapper(
179
184
  expr: string,
180
185
  ctx: ClientJsContext,
181
186
  freeIdentifiers?: ReadonlySet<string>,
187
+ ): boolean {
188
+ return needsEffectWrapperCore(expr, ctx, freeIdentifiers, new Set())
189
+ }
190
+
191
+ /**
192
+ * `needsEffectWrapper`'s actual body, with a `visitedConstants` accumulator
193
+ * threaded through the local-constant recursion below so a cycle
194
+ * (`const a = b; const b = a`) terminates instead of looping forever.
195
+ */
196
+ function needsEffectWrapperCore(
197
+ expr: string,
198
+ ctx: ClientJsContext,
199
+ freeIdentifiers: ReadonlySet<string> | undefined,
200
+ visitedConstants: Set<string>,
182
201
  ): boolean {
183
202
  for (const signal of ctx.signals) {
184
203
  if (identifierCallPattern(signal.getter).test(expr)) {
@@ -209,6 +228,46 @@ export function needsEffectWrapper(
209
228
  if (propsAccess.test(expr)) return true
210
229
  }
211
230
 
231
+ // #2723: a bare `const x__alias = x` hop between a destructured prop and
232
+ // its use site breaks every check above — `expr` (or its precomputed
233
+ // `freeIdentifiers`) names the ALIAS, never the prop it stands for, so
234
+ // none of the direct prop/signal/memo checks fire even though the value
235
+ // is exactly as reactive as `x` itself. Phase 1's `isReactiveExpression`
236
+ // already sees through this (`isPropsReference` / `isSignalOrMemoReference`
237
+ // in jsx-to-ir.ts recursively walk `ctx.patterns.constants`, the
238
+ // `TransformContext` twin of this function's `ctx.localConstants`) —
239
+ // this is that SAME constant-chain walk, ported onto Phase 2's string
240
+ // expression so its independent wrap decision agrees with Phase 1's
241
+ // `hasReactiveAttributes` slotId decision instead of silently
242
+ // disagreeing on whether the attribute gets a `createEffect` at all.
243
+ //
244
+ // This is NOT loop/callback binding resolution — `BindingScope` (#2482)
245
+ // answers "what name is this loop row's own item/index/destructure
246
+ // binding," a question with no bearing on a component-body `const`
247
+ // aliasing a prop — so it does not belong there. It also isn't a new ad
248
+ // hoc tracking structure: `ctx.localConstants` is the existing per-
249
+ // component constant list this file already reads (see
250
+ // `expandConstantForReactivity` above), walked here with a `visited`
251
+ // guard for the same reason `free-refs.ts`'s `resolveConstantInitializerRefs`
252
+ // carries one — a constant cycle must terminate, not loop forever.
253
+ //
254
+ // Constants whose initializer contains an arrow/function expression are
255
+ // skipped, mirroring `resolveConstantInitializerRefs`'s `containsArrow`
256
+ // skip: refs inside a function body run when (and if) the function is
257
+ // invoked, not merely because something reads the bare function value.
258
+ for (const constant of ctx.localConstants) {
259
+ if (visitedConstants.has(constant.name)) continue
260
+ if (constant.value === undefined || constant.containsArrow) continue
261
+ const referenced = freeIdentifiers
262
+ ? freeIdentifiers.has(constant.name)
263
+ : tokenContainsIdent(expr, constant.name)
264
+ if (!referenced) continue
265
+ visitedConstants.add(constant.name)
266
+ if (needsEffectWrapperCore(constant.value, ctx, constant.freeIdentifiers, visitedConstants)) {
267
+ return true
268
+ }
269
+ }
270
+
212
271
  return false
213
272
  }
214
273
 
@@ -1,7 +1,8 @@
1
1
  /**
2
- * AST-based rename of the source-level props object name (e.g. `props`
3
- * or a user-supplied destructure name) the generated parameter name
4
- * `_p` across the joined init-body string.
2
+ * AST-based rename of the source-level props object name(s) (e.g. `props`,
3
+ * a user-supplied destructure name, or a destructured rest binding like
4
+ * `...rest`) → the generated parameter name `_p` across the joined
5
+ * init-body string.
5
6
  *
6
7
  * Replaces the pre-C2 regex hack `\\b<propsObjectName>\\b` which silently
7
8
  * matched contexts that should NOT have been rewritten:
@@ -27,15 +28,54 @@ import { PROPS_PARAM } from './utils.ts'
27
28
  import { identifierPattern } from '../identifier-pattern.ts'
28
29
 
29
30
  /**
30
- * Rename every value-position reference to `propsObjectName` in `code`
31
- * to `_p`. No-op when `propsObjectName` is null (destructured-prop mode
32
- * — the analyzer already pre-rewrites bare prop refs into `templateXxx`
33
- * fields) or already equals `_p`.
31
+ * Rename every value-position reference to `propsObjectName` and,
32
+ * independently, to `restPropsName` (#2723) in `code` to `_p`.
33
+ *
34
+ * `restPropsName` matters even in destructured-prop mode (where
35
+ * `propsObjectName` is null and the analyzer already pre-rewrites bare
36
+ * NAMED-prop refs into `templateXxx` fields, per the historical
37
+ * `propsObjectName ?? 'props'` fallback this replaces): a destructured
38
+ * rest binding (`const { className, ...rest } = props` /
39
+ * `function F({ className, ...rest })`) is not itself a "named prop," so
40
+ * nothing pre-rewrites a bare reference to it. Such a reference reaches
41
+ * this pass whenever the init body needs `rest`'s OWN VALUE rather than
42
+ * just recognising a `{...rest}` spread by name — e.g. a `const
43
+ * rest__alias = rest` hop (#2723's `alias-props` mutation aliases the
44
+ * rest parameter along with every named one). A rest binding named
45
+ * anything but "props" (`...rest`, `...leftover`) left such a reference
46
+ * dangling as a `ReferenceError`, so the analyzer's actual
47
+ * `restPropsName` is passed in to remove that guesswork.
48
+ *
49
+ * The `propsObjectName ?? 'props'` fallback is KEPT alongside it, not
50
+ * replaced by it. The two do different jobs, and reading the fallback as
51
+ * merely a lucky guess at the rest binding's name regressed the
52
+ * doc-example `StatementExample`: a component that destructures its
53
+ * parameters can still write `props.itemId` inside a handler body, and
54
+ * `propsObjectName` is null for exactly that shape — so dropping the
55
+ * fallback left `props.itemId` in the emitted init, where no `props`
56
+ * binding exists to satisfy it.
57
+ *
58
+ * Each candidate name is rewritten independently and skipped when null,
59
+ * already `_p`, or a duplicate of one already processed (a component
60
+ * whose props param IS its own rest destructure target, if that shape
61
+ * ever arises, would otherwise walk the AST twice for the same name).
34
62
  */
35
- export function rewritePropsObjectRef(code: string, propsObjectName: string | null): string {
36
- const srcPropsName = propsObjectName ?? 'props'
37
- if (srcPropsName === PROPS_PARAM) return code
63
+ export function rewritePropsObjectRef(
64
+ code: string,
65
+ propsObjectName: string | null,
66
+ restPropsName: string | null = null,
67
+ ): string {
68
+ let result = code
69
+ const seen = new Set<string>()
70
+ for (const srcPropsName of [propsObjectName ?? 'props', restPropsName]) {
71
+ if (srcPropsName === null || srcPropsName === PROPS_PARAM || seen.has(srcPropsName)) continue
72
+ seen.add(srcPropsName)
73
+ result = rewriteOneName(result, srcPropsName)
74
+ }
75
+ return result
76
+ }
38
77
 
78
+ function rewriteOneName(code: string, srcPropsName: string): string {
39
79
  // Quick exit when the name doesn't appear at all.
40
80
  if (!identifierPattern(srcPropsName).test(code)) return code
41
81
 
@@ -25,6 +25,7 @@ import {
25
25
  loopEndMarker,
26
26
  loopItemMarker,
27
27
  toHTMLAttrName as toHtmlAttrName,
28
+ keyAttrName as sharedKeyAttrName,
28
29
  } from '@barefootjs/shared'
29
30
 
30
31
  export { DATA_KEY, DATA_KEY_PREFIX, DATA_BF_PH, BF_LOOP_START, BF_LOOP_END, loopStartMarker, loopEndMarker, loopItemMarker, toHtmlAttrName }
@@ -39,9 +40,36 @@ export const PROPS_PARAM = '_p'
39
40
  * Get the data-key attribute name for a given loop depth.
40
41
  * Outer loop (depth 0): 'data-key'
41
42
  * Nested loops (depth N): 'data-key-N'
43
+ *
44
+ * Re-exported from `@barefootjs/shared` (the single source of truth also
45
+ * used by `jsx-to-ir.ts`'s `IRElement.keyAttr` resolution) so existing
46
+ * imports of this module keep working.
42
47
  */
43
- export function keyAttrName(loopDepth: number): string {
44
- return loopDepth > 0 ? `${DATA_KEY_PREFIX}${loopDepth}` : DATA_KEY
48
+ export const keyAttrName = sharedKeyAttrName
49
+
50
+ /**
51
+ * Build the trailing `, <bfId>, <keyAttrName>` arguments for a NESTED
52
+ * (`loopDepth > 0`) `mapArray(...)` call — `inner-loop.ts` / `loop-child-arm.ts`,
53
+ * the only two stringify sites whose loop can be nested (#2753 Shape B: the
54
+ * runtime otherwise has no way to know it isn't the outermost loop, and
55
+ * always stamped the plain `data-key` name).
56
+ *
57
+ * A depth-0 (or unkeyed) loop needs no change at all: `mapArray`'s own
58
+ * default (`BF_KEY`, `'data-key'`) is already correct there, and an unkeyed
59
+ * loop's runtime never stamps a key attribute regardless of the name — so
60
+ * this returns `bfIdArg` UNCHANGED, keeping every other call site (and every
61
+ * existing depth-0 call here) byte-identical.
62
+ *
63
+ * `bfIdArg` is the existing profiling-id suffix (e.g. `profileBindingId(...)`,
64
+ * either `''` or `, "<id>"`) already threaded through these two call sites —
65
+ * an empty one is widened to an explicit `, undefined` placeholder so the
66
+ * name lands in the right positional slot (`mapArray`'s 6th parameter is
67
+ * `bfId`, not `keyAttrName`).
68
+ */
69
+ export function mapArrayKeyArgs(bfIdArg: string, keyed: boolean, loopDepth: number): string {
70
+ if (!keyed || loopDepth <= 0) return bfIdArg
71
+ const bfIdSlot = bfIdArg || ', undefined'
72
+ return `${bfIdSlot}, ${JSON.stringify(keyAttrName(loopDepth))}`
45
73
  }
46
74
 
47
75
  /**