@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.
- package/dist/analyzer.d.ts +17 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts +21 -5
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +14 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +817 -457
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +7 -7
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +60 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/prop-handling.d.ts +30 -0
- package/dist/ir-to-client-js/prop-handling.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +5 -0
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts +36 -8
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +26 -2
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/props-binding.d.ts +35 -0
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +165 -98
- package/src/__tests__/binding-scope-ratchet.test.ts +5 -1
- package/src/__tests__/child-component-ref-not-mirrored.test.ts +90 -0
- package/src/__tests__/client-js-generation.test.ts +11 -0
- package/src/__tests__/ir-to-client-js/imports.test.ts +107 -0
- package/src/__tests__/ir-to-client-js/merge-compiled-client-js-imports.test.ts +138 -0
- package/src/__tests__/issue-2723-prop-alias-reactivity.test.ts +124 -0
- package/src/__tests__/issue-2754-rest-spread-needs-slot.test.ts +85 -0
- package/src/__tests__/issue-2756-loop-row-honors-client-only.test.ts +173 -0
- package/src/__tests__/merge-template-imports.test.ts +41 -1
- package/src/__tests__/multi-component-shared-default-import.test.ts +55 -0
- package/src/__tests__/rewrite-props-object.test.ts +41 -4
- package/src/__tests__/root-key-relay.test.ts +170 -0
- package/src/__tests__/signal-getter-not-called.test.ts +149 -0
- package/src/__tests__/state-only-file-default-import.test.ts +47 -0
- package/src/analyzer.ts +36 -0
- package/src/compiler.ts +94 -104
- package/src/expression-parser.ts +26 -0
- package/src/index.ts +2 -2
- package/src/ir-to-client-js/collect-elements.ts +45 -30
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +6 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -2
- package/src/ir-to-client-js/emit-registration.ts +26 -7
- package/src/ir-to-client-js/generate-init.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +130 -20
- package/src/ir-to-client-js/imports.ts +178 -5
- package/src/ir-to-client-js/index.ts +11 -4
- package/src/ir-to-client-js/prop-handling.ts +83 -0
- package/src/ir-to-client-js/reactivity.ts +59 -0
- package/src/ir-to-client-js/rewrite-props-object.ts +50 -10
- package/src/ir-to-client-js/utils.ts +30 -2
- package/src/jsx-to-ir.ts +523 -49
- package/src/props-binding.ts +51 -0
- 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
|
-
*
|
|
4
|
-
* `_p` across the joined
|
|
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`
|
|
31
|
-
* to `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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(
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
44
|
-
|
|
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
|
/**
|