@barefootjs/jsx 0.13.0 → 0.15.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 (46) hide show
  1. package/dist/adapters/env-signal.d.ts +40 -0
  2. package/dist/adapters/env-signal.d.ts.map +1 -0
  3. package/dist/adapters/parsed-expr-emitter.d.ts +2 -1
  4. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/augment-inherited-props.d.ts +42 -1
  7. package/dist/augment-inherited-props.d.ts.map +1 -1
  8. package/dist/builtins.d.ts +33 -0
  9. package/dist/builtins.d.ts.map +1 -0
  10. package/dist/compiler.d.ts.map +1 -1
  11. package/dist/errors.d.ts +1 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/expression-parser.d.ts +48 -1
  14. package/dist/expression-parser.d.ts.map +1 -1
  15. package/dist/index.d.ts +5 -4
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +411 -26
  18. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  19. package/dist/jsx-to-ir.d.ts.map +1 -1
  20. package/dist/profiler.d.ts +37 -0
  21. package/dist/profiler.d.ts.map +1 -1
  22. package/dist/ssr-defaults.d.ts.map +1 -1
  23. package/dist/types.d.ts +16 -0
  24. package/dist/types.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/src/__tests__/compiler-stress-1244.test.ts +4 -2
  27. package/src/__tests__/expression-parser.test.ts +92 -1
  28. package/src/__tests__/ir-async.test.ts +8 -0
  29. package/src/__tests__/ir-builtin-import-scope.test.ts +188 -0
  30. package/src/__tests__/ir-region.test.ts +86 -0
  31. package/src/__tests__/profiler.test.ts +69 -0
  32. package/src/__tests__/ssr-defaults.test.ts +25 -0
  33. package/src/adapters/env-signal.ts +75 -0
  34. package/src/adapters/parsed-expr-emitter.ts +11 -0
  35. package/src/analyzer.ts +9 -0
  36. package/src/augment-inherited-props.ts +170 -2
  37. package/src/builtins.ts +63 -0
  38. package/src/compiler.ts +6 -2
  39. package/src/errors.ts +10 -0
  40. package/src/expression-parser.ts +156 -2
  41. package/src/index.ts +5 -2
  42. package/src/ir-to-client-js/imports.ts +5 -0
  43. package/src/jsx-to-ir.ts +189 -8
  44. package/src/profiler.ts +63 -0
  45. package/src/ssr-defaults.ts +55 -17
  46. package/src/types.ts +16 -0
@@ -0,0 +1,188 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { analyzeComponent } from '../analyzer'
3
+ import { jsxToIR } from '../jsx-to-ir'
4
+ import { ErrorCodes } from '../errors'
5
+ import { stripClientBuiltinImports } from '../builtins'
6
+ import type { IRAsync, IRElement, IRComponent, ImportInfo } from '../types'
7
+
8
+ // Import-scoped recognition for the compile-away built-ins `<Async>` /
9
+ // `<Region>` (#1915). The compiler recognises them by their
10
+ // `@barefootjs/client` import — never by a bare capitalized tag name — so a
11
+ // user's own component does not collide with the built-in, and the import is
12
+ // elided on emit.
13
+
14
+ function ir(source: string, path = 'Comp.tsx') {
15
+ const ctx = analyzeComponent(source, path)
16
+ return { ir: jsxToIR(ctx), ctx }
17
+ }
18
+
19
+ describe('import-scoped recognition for <Async> / <Region>', () => {
20
+ test('<Async> imported from @barefootjs/client lowers to IRAsync', () => {
21
+ const { ir: root, ctx } = ir(`
22
+ import { Async } from '@barefootjs/client'
23
+ export function Page() {
24
+ return <div><Async fallback={<p>Loading</p>}><Body /></Async></div>
25
+ }
26
+ `)
27
+ expect(ctx.errors.filter(e => e.severity === 'error')).toEqual([])
28
+ const div = root as IRElement
29
+ const async = div.children.find(c => c.type === 'async') as IRAsync
30
+ expect(async?.type).toBe('async')
31
+ })
32
+
33
+ test('<Region> imported from @barefootjs/client lowers to a region element', () => {
34
+ const { ir: root } = ir(`
35
+ import { Region } from '@barefootjs/client'
36
+ export function Shell({ children }) {
37
+ return <div><Region>{children}</Region></div>
38
+ }
39
+ `)
40
+ const div = root as IRElement
41
+ const region = div.children.find(
42
+ (c): c is IRElement => c.type === 'element' && c.regionId !== undefined,
43
+ )
44
+ expect(region).toBeDefined()
45
+ })
46
+
47
+ test('aliased import `Async as Boundary` recognises <Boundary> as the built-in', () => {
48
+ const { ir: root } = ir(`
49
+ import { Async as Boundary } from '@barefootjs/client'
50
+ export function Page() {
51
+ return <Boundary fallback={<p>Loading</p>}><Body /></Boundary>
52
+ }
53
+ `)
54
+ expect((root as IRAsync).type).toBe('async')
55
+ })
56
+
57
+ test("a user's own <Async> (imported elsewhere) is NOT lowered and emits no diagnostic", () => {
58
+ const { ir: root, ctx } = ir(`
59
+ import { Async } from './my-async'
60
+ export function Page() {
61
+ return <div><Async fallback={<p>x</p>}><Body /></Async></div>
62
+ }
63
+ `)
64
+ expect(ctx.errors.find(e => e.code === ErrorCodes.BUILTIN_REQUIRES_IMPORT)).toBeUndefined()
65
+ const div = root as IRElement
66
+ expect(div.children.some(c => c.type === 'async')).toBe(false)
67
+ const comp = div.children.find((c): c is IRComponent => c.type === 'component')
68
+ expect(comp?.name).toBe('Async')
69
+ })
70
+
71
+ test('a locally declared Async component is NOT treated as the built-in', () => {
72
+ const { ir: root, ctx } = ir(`
73
+ function Async(props) { return <section>{props.children}</section> }
74
+ export function Page() {
75
+ return <Async fallback={<p>x</p>}><Body /></Async>
76
+ }
77
+ `)
78
+ expect(ctx.errors.find(e => e.code === ErrorCodes.BUILTIN_REQUIRES_IMPORT)).toBeUndefined()
79
+ expect((root as IRElement).type).not.toBe('async')
80
+ })
81
+
82
+ test('bare <Async> with no import and no binding reports BF054', () => {
83
+ const { ctx } = ir(`
84
+ export function Page() {
85
+ return <Async fallback={<p>Loading</p>}><Body /></Async>
86
+ }
87
+ `)
88
+ const err = ctx.errors.find(e => e.code === ErrorCodes.BUILTIN_REQUIRES_IMPORT)
89
+ expect(err).toBeDefined()
90
+ expect(err?.severity).toBe('error')
91
+ expect(err?.message).toContain('@barefootjs/client')
92
+ })
93
+
94
+ test('a type-only import does NOT scope the built-in and does not suppress BF054', () => {
95
+ // `import type { Async }` brings no value binding into scope — the design
96
+ // is import-value-required, so <Async> must still raise BF054.
97
+ const { ir: root, ctx } = ir(`
98
+ import type { Async } from '@barefootjs/client'
99
+ export function Page() {
100
+ return <Async fallback={<p>Loading</p>}><Body /></Async>
101
+ }
102
+ `)
103
+ expect((root as IRElement).type).not.toBe('async')
104
+ expect(ctx.errors.find(e => e.code === ErrorCodes.BUILTIN_REQUIRES_IMPORT)).toBeDefined()
105
+ })
106
+
107
+ test('a per-specifier type-only import (`import { type Async }`) does NOT scope the built-in', () => {
108
+ const { ir: root, ctx } = ir(`
109
+ import { type Async } from '@barefootjs/client'
110
+ export function Page() {
111
+ return <Async fallback={<p>Loading</p>}><Body /></Async>
112
+ }
113
+ `)
114
+ expect((root as IRElement).type).not.toBe('async')
115
+ expect(ctx.errors.find(e => e.code === ErrorCodes.BUILTIN_REQUIRES_IMPORT)).toBeDefined()
116
+ })
117
+
118
+ test('a value specifier alongside a type-only one is still recognised (`import { type Async, Region }`)', () => {
119
+ const { ir: root } = ir(`
120
+ import { type Async, Region } from '@barefootjs/client'
121
+ export function Shell({ children }) {
122
+ return <div><Region>{children}</Region></div>
123
+ }
124
+ `)
125
+ const div = root as IRElement
126
+ expect(div.children.some(c => c.type === 'element' && c.regionId !== undefined)).toBe(true)
127
+ })
128
+
129
+ test('bare <Region /> with no import reports BF054', () => {
130
+ const { ctx } = ir(`
131
+ export function Shell() {
132
+ return <Region />
133
+ }
134
+ `)
135
+ expect(ctx.errors.find(e => e.code === ErrorCodes.BUILTIN_REQUIRES_IMPORT)).toBeDefined()
136
+ })
137
+ })
138
+
139
+ describe('stripClientBuiltinImports (emit-time elision)', () => {
140
+ const loc = { file: 'x.tsx', line: 1, column: 1 }
141
+ const imp = (source: string, names: string[]): ImportInfo => ({
142
+ source,
143
+ isTypeOnly: false,
144
+ loc,
145
+ specifiers: names.map(n => ({ name: n, alias: null, isDefault: false, isNamespace: false })),
146
+ })
147
+
148
+ test('drops the @barefootjs/client import when it only carried built-ins', () => {
149
+ expect(stripClientBuiltinImports([imp('@barefootjs/client', ['Async', 'Region'])])).toEqual([])
150
+ })
151
+
152
+ test('keeps non-built-in specifiers and drops the built-ins', () => {
153
+ const out = stripClientBuiltinImports([imp('@barefootjs/client', ['createSignal', 'Async'])])
154
+ expect(out).toHaveLength(1)
155
+ expect(out[0].specifiers.map(s => s.name)).toEqual(['createSignal'])
156
+ })
157
+
158
+ test('leaves imports from other sources untouched', () => {
159
+ const other = imp('./my-async', ['Async'])
160
+ expect(stripClientBuiltinImports([other])).toEqual([other])
161
+ })
162
+
163
+ test('does NOT strip type-only imports (never a runtime phantom)', () => {
164
+ const typeOnly: ImportInfo = { ...imp('@barefootjs/client', ['Async', 'Region']), isTypeOnly: true }
165
+ expect(stripClientBuiltinImports([typeOnly])).toEqual([typeOnly])
166
+ })
167
+
168
+ test('preserves a side-effect import of @barefootjs/client', () => {
169
+ const sideEffect = imp('@barefootjs/client', [])
170
+ expect(stripClientBuiltinImports([sideEffect])).toEqual([sideEffect])
171
+ })
172
+
173
+ test('does NOT strip a per-specifier type-only built-in, but strips the value one', () => {
174
+ const mixed: ImportInfo = {
175
+ source: '@barefootjs/client',
176
+ isTypeOnly: false,
177
+ loc,
178
+ specifiers: [
179
+ { name: 'Async', alias: null, isDefault: false, isNamespace: false, isTypeOnly: true },
180
+ { name: 'Region', alias: null, isDefault: false, isNamespace: false, isTypeOnly: false },
181
+ ],
182
+ }
183
+ const out = stripClientBuiltinImports([mixed])
184
+ expect(out).toHaveLength(1)
185
+ expect(out[0].specifiers.map(s => s.name)).toEqual(['Async'])
186
+ expect(out[0].specifiers[0].isTypeOnly).toBe(true)
187
+ })
188
+ })
@@ -0,0 +1,86 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { analyzeComponent } from '../analyzer'
3
+ import { jsxToIR } from '../jsx-to-ir'
4
+ import { compileJSX } from '../compiler'
5
+ import { HonoAdapter } from '../../../../packages/adapter-hono/src/adapter/hono-adapter'
6
+ import type { IRElement } from '../types'
7
+
8
+ const LAYOUT = `
9
+ import { Region } from '@barefootjs/client'
10
+ export function Layout({ title, children }) {
11
+ return (
12
+ <div>
13
+ <h1>{title}</h1>
14
+ <Region>{children}</Region>
15
+ </div>
16
+ )
17
+ }
18
+ `
19
+
20
+ function regionEl(source: string, path: string): IRElement {
21
+ const ir = jsxToIR(analyzeComponent(source, path))
22
+ const root = ir as IRElement
23
+ const region = root.children.find(
24
+ (c): c is IRElement => c.type === 'element' && c.regionId !== undefined,
25
+ )
26
+ if (!region) throw new Error('no region element found')
27
+ return region
28
+ }
29
+
30
+ function markedTemplate(source: string, path: string): string {
31
+ const result = compileJSX(source, path, { adapter: new HonoAdapter() })
32
+ expect(result.errors).toHaveLength(0)
33
+ const marked = result.files.find(f => f.type === 'markedTemplate')
34
+ if (!marked) throw new Error('no markedTemplate emitted')
35
+ return marked.content
36
+ }
37
+
38
+ describe('<Region> page-lifecycle boundary', () => {
39
+ test('lowers <Region>{children}</Region> to a div carrying a regionId', () => {
40
+ const region = regionEl(LAYOUT, 'Layout.tsx')
41
+ expect(region.tag).toBe('div')
42
+ expect(region.regionId).toMatch(/^[0-9a-f]{8}:0$/)
43
+ // The children passed to the region are preserved inside it.
44
+ expect(region.children.length).toBeGreaterThan(0)
45
+ })
46
+
47
+ test('assigns sequential structural indices within a file', () => {
48
+ const source = `
49
+ import { Region } from '@barefootjs/client'
50
+ export function Split() {
51
+ return (
52
+ <div>
53
+ <Region><aside /></Region>
54
+ <Region><main /></Region>
55
+ </div>
56
+ )
57
+ }
58
+ `
59
+ const ir = jsxToIR(analyzeComponent(source, 'Split.tsx')) as IRElement
60
+ const ids = ir.children
61
+ .filter((c): c is IRElement => c.type === 'element' && c.regionId !== undefined)
62
+ .map(c => c.regionId)
63
+ expect(ids).toHaveLength(2)
64
+ expect(ids[0]).toMatch(/:0$/)
65
+ expect(ids[1]).toMatch(/:1$/)
66
+ })
67
+
68
+ test('Hono adapter emits bf-region on the boundary element', () => {
69
+ expect(markedTemplate(LAYOUT, 'Layout.tsx')).toMatch(/bf-region="[0-9a-f]{8}:0"/)
70
+ })
71
+
72
+ test('region id is deterministic across compiles (not per-run random)', () => {
73
+ // The load-bearing requirement: a layout compiled twice — as it would be
74
+ // when two different pages each compose it — must emit the *same* id, so
75
+ // the client router can match the same region across page documents.
76
+ const a = regionEl(LAYOUT, '/app/shell.tsx').regionId
77
+ const b = regionEl(LAYOUT, '/app/shell.tsx').regionId
78
+ expect(a).toBe(b)
79
+ })
80
+
81
+ test('region id differs across layout files (cross-file uniqueness)', () => {
82
+ const a = regionEl(LAYOUT, '/app/shell-a.tsx').regionId
83
+ const b = regionEl(LAYOUT, '/app/shell-b.tsx').regionId
84
+ expect(a).not.toBe(b)
85
+ })
86
+ })
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { describe, test, expect } from 'bun:test'
11
11
  import {
12
+ PROFILE_SCHEMA_VERSION,
12
13
  buildStaticBudget,
13
14
  diffStaticBudget,
14
15
  formatStaticBudget,
@@ -179,6 +180,73 @@ describe('buildStaticBudget (SR5)', () => {
179
180
  expect(b.subscriptions).toBe(0)
180
181
  expect(b.crossComponentOnly).toBe(false)
181
182
  })
183
+
184
+ test('exposes the handlers --scenario auto would fire, name + loc (#1841 A1)', () => {
185
+ // Two handlers on distinct elements/events. The static list is the coverage
186
+ // gap an agent can read before any run.
187
+ const src = `
188
+ 'use client'
189
+ import { createSignal } from '@barefootjs/client'
190
+ export function Form() {
191
+ const [q, setQ] = createSignal('')
192
+ return (
193
+ <div>
194
+ <input onInput={(e) => setQ(e.currentTarget.value)} />
195
+ <button onClick={() => setQ('')}>Clear</button>
196
+ </div>
197
+ )
198
+ }
199
+ `
200
+ const b = buildStaticBudget(src, 'Form.tsx', 'Form')
201
+ expect(b.handlers.length).toBe(2)
202
+ // `name` is `<event>@<slotId>` — the slotId joins to dynamic coverage.
203
+ expect(b.handlers.every(h => /^\w+@\w+$/.test(h.name))).toBe(true)
204
+ const events = b.handlers.map(h => h.name.split('@')[0]).sort()
205
+ expect(events).toEqual(['click', 'input'])
206
+ // Each handler carries a source location.
207
+ for (const h of b.handlers) {
208
+ expect(h.loc.file).toContain('Form.tsx')
209
+ expect(h.loc.line).toBeGreaterThan(0)
210
+ }
211
+ })
212
+
213
+ test('handlers is empty when the component binds none', () => {
214
+ const src = `
215
+ 'use client'
216
+ import { createSignal } from '@barefootjs/client'
217
+ export function Display() {
218
+ const [n] = createSignal(0)
219
+ return <div>{n()}</div>
220
+ }
221
+ `
222
+ const b = buildStaticBudget(src, 'Display.tsx', 'Display')
223
+ expect(b.handlers).toEqual([])
224
+ })
225
+
226
+ test('renders a handlers section in text output when present, omits it otherwise', () => {
227
+ const withHandler = formatStaticBudget(buildStaticBudget(counterSource, 'Counter.tsx', 'Counter'))
228
+ expect(withHandler).toMatch(/handlers \(1\):/)
229
+ expect(withHandler).toMatch(/click@\w+\s+Counter\.tsx:\d+/)
230
+
231
+ const noHandler = `
232
+ 'use client'
233
+ import { createSignal } from '@barefootjs/client'
234
+ export function Display() {
235
+ const [n] = createSignal(0)
236
+ return <div>{n()}</div>
237
+ }
238
+ `
239
+ expect(formatStaticBudget(buildStaticBudget(noHandler, 'Display.tsx', 'Display'))).not.toContain('handlers (')
240
+ })
241
+ })
242
+
243
+ describe('schemaVersion (#1841)', () => {
244
+ test('every JSON mode carries the schema version', () => {
245
+ const budget = buildStaticBudget(counterSource, 'Counter.tsx', 'Counter')
246
+ expect(budget.schemaVersion).toBe(PROFILE_SCHEMA_VERSION)
247
+ const diff = diffStaticBudget(budget, buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc'))
248
+ expect(diff.schemaVersion).toBe(PROFILE_SCHEMA_VERSION)
249
+ })
182
250
  })
183
251
 
184
252
  describe('diffStaticBudget (SR6)', () => {
@@ -345,6 +413,7 @@ describe('buildProfileReport (dynamic, SR1–SR4 + analyses)', () => {
345
413
  ]
346
414
  const r = buildProfileReport({ source: src, filePath: 'Calc.tsx', scenario: 'auto', events })
347
415
  expect(r.kind).toBe('profile')
416
+ expect(r.schemaVersion).toBe(PROFILE_SCHEMA_VERSION)
348
417
  expect(r.componentName).toBe('Calc')
349
418
  expect(r.turns).toBe(1)
350
419
  expect(r.hotSubscribers.subscribers[0].name).toBe('a')
@@ -99,6 +99,31 @@ describe('extractSsrDefaults', () => {
99
99
  expect(defaults?.doubled).toEqual({ value: 10 })
100
100
  })
101
101
 
102
+ test('block-body memo with an early-return guard folds to the default-state branch (#1897)', () => {
103
+ // The data-table `sortedData` shape: a `/* @client */`-guarded sort
104
+ // whose early return yields the unsorted module-const array when the
105
+ // sort-key signal is at its initial (null) value. The SSR default is
106
+ // that early-return array, not `null` — the `if (!key)` guard is
107
+ // taken because `sortKey()` resolves to its seeded `null` initial.
108
+ const metadata = metadataFor(`
109
+ 'use client'
110
+ import { createSignal, createMemo } from '@barefootjs/client'
111
+ const rows = [{ id: 'a' }, { id: 'b' }]
112
+ function Table() {
113
+ const [sortKey, setSortKey] = createSignal<string | null>(null)
114
+ const sorted = createMemo(() => {
115
+ const key = sortKey()
116
+ if (!key) return rows
117
+ return /* @client */ [...rows].sort((a, b) => a.id < b.id ? -1 : 1)
118
+ })
119
+ return <ul>{sorted().map(r => <li>{r.id}</li>)}</ul>
120
+ }
121
+ `)
122
+
123
+ const defaults = extractSsrDefaults(metadata)
124
+ expect(defaults?.sorted).toEqual({ value: [{ id: 'a' }, { id: 'b' }] })
125
+ })
126
+
102
127
  test('non-evaluable initials yield null (caller falls back at render time)', () => {
103
128
  const metadata = metadataFor(`
104
129
  'use client'
@@ -0,0 +1,75 @@
1
+ // Shared recognition of the router v0.5 `searchParams()` environment signal
2
+ // for the template-string adapters (Mojo / Xslate today; Go has its own
3
+ // struct-field path). A request-scoped reactive env signal reads like an
4
+ // ordinary signal getter, but its value is a URLSearchParams-like reader with
5
+ // real methods (`.get(key)`) — not a hashref. The generic `member` lowering
6
+ // would deref `.get` as a property access and drop the call + argument, so the
7
+ // adapters special-case it to a real per-request reader method call. See #1922.
8
+
9
+ import type { ParsedExpr } from '../expression-parser.ts'
10
+ import type { IRMetadata } from '../types.ts'
11
+
12
+ /**
13
+ * The local binding name(s) that `searchParams` from `@barefootjs/client` is
14
+ * imported under in this component — the same import the analyzer allow-lists
15
+ * (`CLIENT_EXPORTS`). Usually the single name `searchParams`, but an aliased
16
+ * import (`import { searchParams as sp }`) binds it to `sp`, and the template
17
+ * expression then reads `sp()` — so adapters must gate + match against the
18
+ * LOCAL name(s), not the literal `searchParams`. Empty when the env signal is
19
+ * not imported (the component keeps the generic signal lowering).
20
+ *
21
+ * `ImportSpecifier.name` is the exported name and `alias` the local rebinding
22
+ * (see `collectImport` in analyzer.ts), so the import is detected by `name ===
23
+ * 'searchParams'` and the local binding is `alias ?? name`. Namespace / default
24
+ * specifiers bind a different identifier and are excluded.
25
+ */
26
+ export function searchParamsLocalNames(metadata: IRMetadata): Set<string> {
27
+ const names = new Set<string>()
28
+ for (const imp of metadata.imports) {
29
+ if (imp.source !== '@barefootjs/client' || imp.isTypeOnly) continue
30
+ for (const s of imp.specifiers) {
31
+ if (s.isTypeOnly || s.isNamespace || s.isDefault) continue
32
+ if (s.name === 'searchParams') names.add(s.alias ?? s.name)
33
+ }
34
+ }
35
+ return names
36
+ }
37
+
38
+ /**
39
+ * True when the component imports the `searchParams` env signal under any local
40
+ * name. Convenience for adapters/harnesses that only need to gate on presence
41
+ * (the lowering itself needs the {@link searchParamsLocalNames} set to match the
42
+ * actual binding in the expression).
43
+ */
44
+ export function importsSearchParams(metadata: IRMetadata): boolean {
45
+ return searchParamsLocalNames(metadata).size > 0
46
+ }
47
+
48
+ /**
49
+ * Recognise a `<binding>().<method>(<args>)` env-signal method call from a
50
+ * `call` node's callee + args, where `<binding>` is one of the local names
51
+ * `searchParams` was imported under (`localNames`, from
52
+ * {@link searchParamsLocalNames}). Returns the method name and argument nodes
53
+ * when the receiver is the zero-arg env-signal getter, else null. The caller
54
+ * lowers the match to a real method call on its per-request reader object
55
+ * (`$searchParams->get('sort')` in Mojo, `$searchParams.get('sort')` in
56
+ * Xslate — the canonical `$searchParams` var regardless of the JS alias);
57
+ * without it the generic `member` lowering drops the call + arg.
58
+ */
59
+ export function matchSearchParamsMethodCall(
60
+ callee: ParsedExpr,
61
+ args: ParsedExpr[],
62
+ localNames: ReadonlySet<string>,
63
+ ): { method: string; args: ParsedExpr[] } | null {
64
+ if (callee.kind !== 'member' || callee.computed) return null
65
+ const recv = callee.object
66
+ if (
67
+ recv.kind !== 'call' ||
68
+ recv.args.length !== 0 ||
69
+ recv.callee.kind !== 'identifier' ||
70
+ !localNames.has(recv.callee.name)
71
+ ) {
72
+ return null
73
+ }
74
+ return { method: callee.property, args }
75
+ }
@@ -66,6 +66,7 @@ export type ArrayMethod =
66
66
  | 'toLowerCase'
67
67
  | 'toUpperCase'
68
68
  | 'trim'
69
+ | 'toFixed'
69
70
  | 'split'
70
71
  | 'startsWith'
71
72
  | 'endsWith'
@@ -115,6 +116,14 @@ export interface ParsedExprEmitter {
115
116
  computed: boolean,
116
117
  emit: (e: ParsedExpr) => string,
117
118
  ): string
119
+ // Element access with a non-literal index (`arr[index]`). The index
120
+ // is a full `ParsedExpr` (loop variable, arithmetic, etc.); the
121
+ // adapter picks array vs hash deref per target language. #1897.
122
+ indexAccess(
123
+ object: ParsedExpr,
124
+ index: ParsedExpr,
125
+ emit: (e: ParsedExpr) => string,
126
+ ): string
118
127
  binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string
119
128
  unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string
120
129
  logical(
@@ -196,6 +205,8 @@ export function emitParsedExpr(expr: ParsedExpr, emitter: ParsedExprEmitter): st
196
205
  return emitter.call(expr.callee, expr.args, emit)
197
206
  case 'member':
198
207
  return emitter.member(expr.object, expr.property, expr.computed, emit)
208
+ case 'index-access':
209
+ return emitter.indexAccess(expr.object, expr.index, emit)
199
210
  case 'binary':
200
211
  return emitter.binary(expr.op, expr.left, expr.right, emit)
201
212
  case 'unary':
package/src/analyzer.ts CHANGED
@@ -1648,6 +1648,13 @@ const CLIENT_EXPORTS = new Set([
1648
1648
  'forwardProps', 'unwrap', '__slot',
1649
1649
  'createContext', 'useContext', 'provideContext',
1650
1650
  'createPortal', 'isSSRPortal', 'findSiblingSlot', 'cleanupPortalPlaceholder',
1651
+ // Request-scoped environment signal (router v0.5) — a real user-facing
1652
+ // reactive export the compiler lowers like any other `@barefootjs/client`
1653
+ // signal read (SSR: a template binding; client: a `createEffect`).
1654
+ 'searchParams',
1655
+ // Compile-away JSX built-ins (#1915) — importing them is what scopes the
1656
+ // compiler's `<Async>` / `<Region>` recognition; the import is elided on emit.
1657
+ 'Async', 'Region',
1651
1658
  ])
1652
1659
 
1653
1660
  /**
@@ -1748,6 +1755,8 @@ function collectImport(node: ts.ImportDeclaration, ctx: AnalyzerContext): void {
1748
1755
  alias: element.propertyName ? element.name.text : null,
1749
1756
  isDefault: false,
1750
1757
  isNamespace: false,
1758
+ // Per-specifier `import { type Foo }` — no value binding (#1915).
1759
+ isTypeOnly: element.isTypeOnly,
1751
1760
  })
1752
1761
  }
1753
1762
  }