@barefootjs/jsx 0.25.0 → 0.26.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.
- package/dist/adapters/dangerous-inner-html.d.ts +52 -24
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.js +248 -110
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +11 -5
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +24 -2
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +13 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +31 -10
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
- package/src/__tests__/nested-loop-conditional.test.ts +133 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
- package/src/__tests__/reactive-factory-cross-file.test.ts +252 -1
- package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
- package/src/adapters/dangerous-inner-html.ts +101 -48
- package/src/analyzer.ts +222 -58
- package/src/ir-to-client-js/collect-elements.ts +28 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
- package/src/ir-to-client-js/reactivity.ts +61 -7
- package/src/ir-to-client-js/types.ts +13 -0
- package/src/rich-type-refusal.ts +3 -2
- package/src/to-locale-date-lowering.ts +50 -13
- package/src/types.ts +8 -4
|
@@ -381,3 +381,136 @@ describe('nested loops/conditionals inside mapArray (#830, #839)', () => {
|
|
|
381
381
|
expect(outsideInitChildCount).toBe(0)
|
|
382
382
|
})
|
|
383
383
|
})
|
|
384
|
+
|
|
385
|
+
describe('per-item conditional wrapping a dynamic attr/event element binds exactly once (#2347)', () => {
|
|
386
|
+
// A per-item conditional (`cond ? null : <el/>`) whose element itself has a
|
|
387
|
+
// dynamic attribute and/or event handler used to get bound twice: once
|
|
388
|
+
// directly against the loop item's own initial template clone (querying
|
|
389
|
+
// `qsa(__el, ...)` right in the mapArray callback), and again inside the
|
|
390
|
+
// conditional's own `insert()` bindEvents against whatever node it mounts.
|
|
391
|
+
// At runtime this doubled the click handler and left the attribute effect
|
|
392
|
+
// pointed at a detached node once insert() swapped branches.
|
|
393
|
+
|
|
394
|
+
test('Repro 1: reactive className on a conditionally-rendered element binds once, inside bindEvents', () => {
|
|
395
|
+
const source = `
|
|
396
|
+
'use client'
|
|
397
|
+
import { createSignal } from '@barefootjs/client'
|
|
398
|
+
|
|
399
|
+
interface Item { handle: string; active: 0 | 1 }
|
|
400
|
+
|
|
401
|
+
export function List(props: { viewerHandle: string }) {
|
|
402
|
+
const [items, setItems] = createSignal<Item[]>([])
|
|
403
|
+
return (
|
|
404
|
+
<div>
|
|
405
|
+
{items().map(u => (
|
|
406
|
+
<div key={u.handle}>
|
|
407
|
+
{u.handle === props.viewerHandle ? null : (
|
|
408
|
+
<button className={u.active ? 'btn on' : 'btn'} onClick={() => {}}>
|
|
409
|
+
{u.active ? 'On' : 'Off'}
|
|
410
|
+
</button>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
))}
|
|
414
|
+
</div>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
`
|
|
418
|
+
const result = compileJSX(source, 'List.tsx', { adapter })
|
|
419
|
+
expect(result.errors).toHaveLength(0)
|
|
420
|
+
const content = result.files.find(f => f.type === 'clientJs')!.content
|
|
421
|
+
|
|
422
|
+
// Exactly one class effect, exactly one click listener.
|
|
423
|
+
expect((content.match(/setAttribute\('class'/g) ?? []).length).toBe(1)
|
|
424
|
+
expect((content.match(/addEventListener\('click'/g) ?? []).length).toBe(1)
|
|
425
|
+
|
|
426
|
+
// The class effect must live inside the conditional's own bindEvents
|
|
427
|
+
// (querying `__branchScope`, the node insert() actually mounts) —
|
|
428
|
+
// not directly against `__el` in the outer mapArray scope, which would
|
|
429
|
+
// go stale the moment insert() swaps in a fresh clone.
|
|
430
|
+
const bindEventsIndex = content.indexOf('bindEvents:')
|
|
431
|
+
const classEffectIndex = content.indexOf("setAttribute('class'")
|
|
432
|
+
expect(bindEventsIndex).toBeGreaterThan(-1)
|
|
433
|
+
expect(classEffectIndex).toBeGreaterThan(bindEventsIndex)
|
|
434
|
+
expect(content).toContain("qsa(__branchScope, '[bf=\"s2\"]')")
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
test('Repro 2: a doubly-nested per-item conditional binds click exactly once, in the innermost arm', () => {
|
|
438
|
+
const source = `
|
|
439
|
+
'use client'
|
|
440
|
+
import { createSignal } from '@barefootjs/client'
|
|
441
|
+
|
|
442
|
+
interface Item { handle: string; active: 0 | 1 }
|
|
443
|
+
|
|
444
|
+
export function List(props: { viewerHandle: string; signedIn: boolean }) {
|
|
445
|
+
const [items, setItems] = createSignal<Item[]>([])
|
|
446
|
+
const onClick = (u: Item) => {}
|
|
447
|
+
return (
|
|
448
|
+
<div>
|
|
449
|
+
{items().map(u => (
|
|
450
|
+
<div key={u.handle}>
|
|
451
|
+
{u.handle === props.viewerHandle ? null : props.signedIn ? (
|
|
452
|
+
<button className={u.active ? 'btn on' : 'btn'} onClick={() => onClick(u)}>
|
|
453
|
+
{u.active ? 'On' : 'Off'}
|
|
454
|
+
</button>
|
|
455
|
+
) : (
|
|
456
|
+
<a href="/login" rel="nofollow noreferrer">Off</a>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
))}
|
|
460
|
+
</div>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
`
|
|
464
|
+
const result = compileJSX(source, 'List.tsx', { adapter })
|
|
465
|
+
expect(result.errors).toHaveLength(0)
|
|
466
|
+
const content = result.files.find(f => f.type === 'clientJs')!.content
|
|
467
|
+
|
|
468
|
+
// A single click bound in the browser used to fire the handler twice —
|
|
469
|
+
// once from the outer arm's own qsa()+addEventListener, once again from
|
|
470
|
+
// the nested insert()'s bindEvents — silently double-toggling state.
|
|
471
|
+
expect((content.match(/addEventListener\('click'/g) ?? []).length).toBe(1)
|
|
472
|
+
expect((content.match(/setAttribute\('class'/g) ?? []).length).toBe(1)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('a JSX-callback prop call as the WHOLE branch content does not get a nested text effect', () => {
|
|
476
|
+
// Regression pin for a fix-of-the-fix: the initial #2347 patch made
|
|
477
|
+
// `summarizeLoopChildBranch` collect reactive texts for any branch,
|
|
478
|
+
// which newly reached a per-item conditional whose branch is a single
|
|
479
|
+
// bare `expression` (no wrapping element) — e.g. a hoisted JSX-callback
|
|
480
|
+
// prop call (`renderNode={(n) => <Pill/>}`-shaped, #1211/#1213). That
|
|
481
|
+
// value is already fully re-evaluated and spliced via `__bfSlot`
|
|
482
|
+
// whenever insert() (re-)mounts the branch; wrapping it in an
|
|
483
|
+
// *additional* createEffect re-invokes the callback (producing a
|
|
484
|
+
// second, independent live element) on the very first run, and the
|
|
485
|
+
// loop-child arm's `$t()`-based anchor lookup can't cleanly displace an
|
|
486
|
+
// already-mounted Element — so the second instance renders beside the
|
|
487
|
+
// first instead of replacing it (surfaced via barefootjs-xyflow's
|
|
488
|
+
// renderNode reference page in CI).
|
|
489
|
+
const source = `
|
|
490
|
+
'use client'
|
|
491
|
+
import { createSignal } from '@barefootjs/client'
|
|
492
|
+
|
|
493
|
+
interface Node { id: string }
|
|
494
|
+
|
|
495
|
+
export function Flow(props: { renderNode?: (n: Node) => unknown }) {
|
|
496
|
+
const [nodes] = createSignal<Node[]>([])
|
|
497
|
+
return (
|
|
498
|
+
<div>
|
|
499
|
+
{nodes().map(n => (
|
|
500
|
+
<div key={n.id}>
|
|
501
|
+
{props.renderNode ? props.renderNode(n) : <span>{n.id}</span>}
|
|
502
|
+
</div>
|
|
503
|
+
))}
|
|
504
|
+
</div>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
`
|
|
508
|
+
const result = compileJSX(source, 'Flow.tsx', { adapter })
|
|
509
|
+
expect(result.errors).toHaveLength(0)
|
|
510
|
+
const content = result.files.find(f => f.type === 'clientJs')!.content
|
|
511
|
+
|
|
512
|
+
// No nested createEffect re-invoking `props.renderNode(n)` — the
|
|
513
|
+
// conditional's own template()/insert() already owns this value.
|
|
514
|
+
expect(content).not.toMatch(/createEffect\(\(\) => \{ __rt_\w+ = __bfText\(__rt_\w+, \(?\s*props\.renderNode\(n\(\)\)/)
|
|
515
|
+
})
|
|
516
|
+
})
|
|
@@ -111,8 +111,11 @@ describe('nested loop binding ids (#1795 Phase 3)', () => {
|
|
|
111
111
|
|
|
112
112
|
test('loop-child conditional insert() and its branch text carry ids', () => {
|
|
113
113
|
const on = clientJs(nestedSource, 'Nested', true)
|
|
114
|
-
// The branch-arm text (`{r.label}` inside the conditional's true arm)
|
|
115
|
-
|
|
114
|
+
// The branch-arm text (`{r.label}` inside the conditional's true arm) —
|
|
115
|
+
// __bfText (not a naive `.textContent = String(...)`) so a Child-position
|
|
116
|
+
// expression whose value is a live Node splices by identity instead of
|
|
117
|
+
// stringifying (#2347).
|
|
118
|
+
expect(on).toMatch(/__rt_s\d+ = __bfText\(__rt_s\d+, r\(\)\.label\) }, "Nested#binding:s\d+"\)/)
|
|
116
119
|
// The inner loop's child text (`{t}`).
|
|
117
120
|
expect(on).toMatch(/String\(t\(\)\) }, "Nested#binding:s\d+"\)/)
|
|
118
121
|
})
|
|
@@ -878,7 +878,7 @@ export function Ticker() {
|
|
|
878
878
|
expect(clientJs!.content).toMatch(/import\s*\{[^}]*onMount[^}]*\}\s*from\s*'@barefootjs\/client\/runtime'/)
|
|
879
879
|
})
|
|
880
880
|
|
|
881
|
-
test('M18: a type-only helper import does not trigger BF112 and is
|
|
881
|
+
test('M18: a type-only helper import does not trigger BF112 and is re-provisioned as `import type` (#2350)', () => {
|
|
882
882
|
writeFixture('matrix18/todo-types.ts', `export interface Todo {
|
|
883
883
|
id: number
|
|
884
884
|
text: string
|
|
@@ -906,11 +906,262 @@ export function TodoList() {
|
|
|
906
906
|
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
907
907
|
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
908
908
|
expect(result.errors.find(e => e.code === 'BF112')).toBeUndefined()
|
|
909
|
+
// Re-provisioned into the still-typed rewritten source (#2350) so tsc on
|
|
910
|
+
// the compiled output can resolve `Todo` in the inlined `createSignal<Todo[]>` —
|
|
911
|
+
// otherwise this is exactly Sora's SavedList gap (see App.tsx's #2350 comment).
|
|
912
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
913
|
+
expect(template).toBeDefined()
|
|
914
|
+
expect(template!.content).toMatch(/import\s+type\s*\{\s*Todo\s*\}\s*from\s*'\.\/todo-types'/)
|
|
915
|
+
// But never reaches the runtime bundle — type-only imports are erased
|
|
916
|
+
// before clientJs, same as any other TypeScript type annotation.
|
|
909
917
|
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
910
918
|
expect(clientJs).toBeDefined()
|
|
911
919
|
expect(clientJs!.content).not.toContain('./todo-types')
|
|
912
920
|
})
|
|
913
921
|
|
|
922
|
+
test('M18b: an already-imported type at the call site is not re-provisioned a second time (#2350)', () => {
|
|
923
|
+
// Mirrors Sora's App.tsx: the compiler doesn't re-provision a factory
|
|
924
|
+
// body's TYPE-only references on its own (extractFreeIdentifiersFromNode
|
|
925
|
+
// stops at type nodes), so Sora's App.tsx carries a manual
|
|
926
|
+
// `import type { SavedList }` for exactly this reason. Once #2350 adds
|
|
927
|
+
// re-provisioning, that manual import must dedupe against it, not
|
|
928
|
+
// produce a second, colliding `import type { SavedList }` line.
|
|
929
|
+
writeFixture('matrix18b/schema.ts', `export interface SavedList {
|
|
930
|
+
id: string
|
|
931
|
+
}
|
|
932
|
+
`)
|
|
933
|
+
writeFixture('matrix18b/useListStore.tsx', `'use client'
|
|
934
|
+
import { createSignal } from '@barefootjs/client'
|
|
935
|
+
import type { SavedList } from './schema'
|
|
936
|
+
|
|
937
|
+
export function useListStore() {
|
|
938
|
+
function emptyCard(): SavedList {
|
|
939
|
+
return { id: 'x' }
|
|
940
|
+
}
|
|
941
|
+
const [lists, setLists] = createSignal<SavedList[]>([emptyCard()])
|
|
942
|
+
return { lists, setLists }
|
|
943
|
+
}
|
|
944
|
+
`)
|
|
945
|
+
const consumerSource = `'use client'
|
|
946
|
+
import { useListStore } from './useListStore'
|
|
947
|
+
import type { SavedList } from './schema'
|
|
948
|
+
|
|
949
|
+
export function App() {
|
|
950
|
+
const { lists, setLists } = useListStore()
|
|
951
|
+
return <button onClick={() => setLists([])}>{lists().length}</button>
|
|
952
|
+
}
|
|
953
|
+
`
|
|
954
|
+
const consumerPath = writeFixture('matrix18b/App.tsx', consumerSource)
|
|
955
|
+
|
|
956
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
957
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
958
|
+
expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
|
|
959
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
960
|
+
expect(template).toBeDefined()
|
|
961
|
+
const matches = template!.content.match(/import\s+type\s*\{\s*SavedList\s*\}/g) ?? []
|
|
962
|
+
expect(matches).toHaveLength(1)
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
test('M18c: a value need is never satisfied by an existing type-only import of the same name (#2350)', () => {
|
|
966
|
+
// The inverse of M18b: the entry file's own `import type { Shared }`
|
|
967
|
+
// has no runtime binding, so a factory needing `Shared` as a VALUE must
|
|
968
|
+
// still decline (BF113) rather than silently treat the type-only import
|
|
969
|
+
// as satisfying it — that would compile clean and crash at hydration
|
|
970
|
+
// (the same dangling-reference direction as #2341 BUG-2), the one
|
|
971
|
+
// failure mode this whole feature exists to avoid.
|
|
972
|
+
writeFixture('matrix18c/shared.ts', `export class Shared {
|
|
973
|
+
static make(): Shared { return new Shared() }
|
|
974
|
+
}
|
|
975
|
+
`)
|
|
976
|
+
writeFixture('matrix18c/useShared.tsx', `'use client'
|
|
977
|
+
import { createSignal } from '@barefootjs/client'
|
|
978
|
+
import { Shared } from './shared'
|
|
979
|
+
|
|
980
|
+
export function useShared() {
|
|
981
|
+
const [value, setValue] = createSignal(Shared.make())
|
|
982
|
+
return { value, setValue }
|
|
983
|
+
}
|
|
984
|
+
`)
|
|
985
|
+
const consumerSource = `'use client'
|
|
986
|
+
import { useShared } from './useShared'
|
|
987
|
+
import type { Shared } from './shared'
|
|
988
|
+
|
|
989
|
+
export function App() {
|
|
990
|
+
const { value, setValue } = useShared()
|
|
991
|
+
return <button onClick={() => setValue(null as unknown as Shared)}>{String(value())}</button>
|
|
992
|
+
}
|
|
993
|
+
`
|
|
994
|
+
const consumerPath = writeFixture('matrix18c/App.tsx', consumerSource)
|
|
995
|
+
|
|
996
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
997
|
+
const bf113 = result.errors.find(e => e.code === 'BF113')
|
|
998
|
+
expect(bf113).toBeDefined()
|
|
999
|
+
expect(bf113!.message).toContain('Shared')
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
test('M18d: a helper VALUE import used only in type position is still re-provisioned (Copilot review, PR #2351)', () => {
|
|
1003
|
+
// `Shape` is a plain value import in the helper file — never wrapped in
|
|
1004
|
+
// `type`/`import type` — but the factory body only ever uses it as a
|
|
1005
|
+
// type annotation. The type-position walk must still find it via
|
|
1006
|
+
// moduleBindings.imported (not just importedTypes), or this regresses
|
|
1007
|
+
// to the exact #2350 gap for names that happen not to be type-only
|
|
1008
|
+
// imports in their OWN file.
|
|
1009
|
+
writeFixture('matrix18d/shape.ts', `export class Shape {
|
|
1010
|
+
area(): number { return 0 }
|
|
1011
|
+
}
|
|
1012
|
+
`)
|
|
1013
|
+
writeFixture('matrix18d/useShape.tsx', `'use client'
|
|
1014
|
+
import { createSignal } from '@barefootjs/client'
|
|
1015
|
+
import { Shape } from './shape'
|
|
1016
|
+
|
|
1017
|
+
export function useShape() {
|
|
1018
|
+
function makeDefault(): Shape {
|
|
1019
|
+
return new Shape()
|
|
1020
|
+
}
|
|
1021
|
+
const [shape, setShape] = createSignal<Shape>(makeDefault())
|
|
1022
|
+
return { shape, setShape }
|
|
1023
|
+
}
|
|
1024
|
+
`)
|
|
1025
|
+
const consumerSource = `'use client'
|
|
1026
|
+
import { useShape } from './useShape'
|
|
1027
|
+
|
|
1028
|
+
export function App() {
|
|
1029
|
+
const { shape, setShape } = useShape()
|
|
1030
|
+
return <button onClick={() => setShape(shape())}>{String(shape())}</button>
|
|
1031
|
+
}
|
|
1032
|
+
`
|
|
1033
|
+
const consumerPath = writeFixture('matrix18d/App.tsx', consumerSource)
|
|
1034
|
+
|
|
1035
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
1036
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1037
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
1038
|
+
expect(template).toBeDefined()
|
|
1039
|
+
// Re-provisioned as a normal VALUE import (not `import type`) — a value
|
|
1040
|
+
// import already brings the type into scope, and the value form is what
|
|
1041
|
+
// the helper file itself declared.
|
|
1042
|
+
expect(template!.content).toMatch(/import\s*\{\s*Shape\s*\}\s*from\s*'\.\/shape'/)
|
|
1043
|
+
expect(template!.content).not.toMatch(/import\s+type\s*\{\s*Shape\s*\}/)
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
test('M18e: a generic type parameter is not misclassified as a module-scope type reference (Copilot review, PR #2351)', () => {
|
|
1047
|
+
// The factory's own <Item> type parameter shadows an unrelated
|
|
1048
|
+
// module-scope `Item` type import — referencing the PARAMETER inside
|
|
1049
|
+
// the factory body must not trigger re-provisioning of the import (it
|
|
1050
|
+
// isn't actually referenced at all).
|
|
1051
|
+
writeFixture('matrix18e/item-types.ts', `export interface Item {
|
|
1052
|
+
id: string
|
|
1053
|
+
}
|
|
1054
|
+
`)
|
|
1055
|
+
writeFixture('matrix18e/useBox.tsx', `'use client'
|
|
1056
|
+
import { createSignal } from '@barefootjs/client'
|
|
1057
|
+
import type { Item } from './item-types'
|
|
1058
|
+
|
|
1059
|
+
export function useBox<Item>(initial: Item) {
|
|
1060
|
+
const [value, setValue] = createSignal<Item>(initial)
|
|
1061
|
+
return { value, setValue }
|
|
1062
|
+
}
|
|
1063
|
+
`)
|
|
1064
|
+
const consumerSource = `'use client'
|
|
1065
|
+
import { useBox } from './useBox'
|
|
1066
|
+
|
|
1067
|
+
export function App() {
|
|
1068
|
+
const { value, setValue } = useBox(0)
|
|
1069
|
+
return <button onClick={() => setValue(1)}>{String(value())}</button>
|
|
1070
|
+
}
|
|
1071
|
+
`
|
|
1072
|
+
const consumerPath = writeFixture('matrix18e/App.tsx', consumerSource)
|
|
1073
|
+
|
|
1074
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
1075
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1076
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
1077
|
+
expect(template).toBeDefined()
|
|
1078
|
+
expect(template!.content).not.toContain('./item-types')
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
test('M18f: a `typeof` reference to a helper value import is re-provisioned as a value import (Copilot review, PR #2351)', () => {
|
|
1082
|
+
// `typeof DEFAULT_SHAPE` is type position syntactically (a TypeQueryNode),
|
|
1083
|
+
// but the name it names — DEFAULT_SHAPE — is a VALUE import, and only its
|
|
1084
|
+
// TYPE is being borrowed here (the nested makeShape's return type). The
|
|
1085
|
+
// body never reads DEFAULT_SHAPE as a value directly, so this isolates
|
|
1086
|
+
// the TypeQueryNode path from the plain-value-walk path that already
|
|
1087
|
+
// covers a direct `DEFAULT_SHAPE` reference regardless of this fix.
|
|
1088
|
+
writeFixture('matrix18f/shapes.ts', `export const DEFAULT_SHAPE = { kind: 'circle' as const, radius: 1 }
|
|
1089
|
+
`)
|
|
1090
|
+
writeFixture('matrix18f/useShape.tsx', `'use client'
|
|
1091
|
+
import { createSignal } from '@barefootjs/client'
|
|
1092
|
+
import { DEFAULT_SHAPE } from './shapes'
|
|
1093
|
+
|
|
1094
|
+
export function useShape() {
|
|
1095
|
+
function makeShape(): typeof DEFAULT_SHAPE {
|
|
1096
|
+
return { kind: 'circle', radius: 1 }
|
|
1097
|
+
}
|
|
1098
|
+
const [shape, setShape] = createSignal(makeShape())
|
|
1099
|
+
return { shape, setShape }
|
|
1100
|
+
}
|
|
1101
|
+
`)
|
|
1102
|
+
const consumerSource = `'use client'
|
|
1103
|
+
import { useShape } from './useShape'
|
|
1104
|
+
|
|
1105
|
+
export function App() {
|
|
1106
|
+
const { shape, setShape } = useShape()
|
|
1107
|
+
return <button onClick={() => setShape({ kind: 'circle', radius: 2 })}>{shape().kind}</button>
|
|
1108
|
+
}
|
|
1109
|
+
`
|
|
1110
|
+
const consumerPath = writeFixture('matrix18f/App.tsx', consumerSource)
|
|
1111
|
+
|
|
1112
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
1113
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1114
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
1115
|
+
expect(template).toBeDefined()
|
|
1116
|
+
expect(template!.content).toMatch(/import\s*\{\s*DEFAULT_SHAPE\s*\}\s*from\s*'\.\/shapes'/)
|
|
1117
|
+
expect(template!.content).not.toMatch(/import\s+type\s*\{\s*DEFAULT_SHAPE\s*\}/)
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
test('M18g: value and type-only re-provisioned imports are globally sorted by specifier, not grouped by kind (Copilot review, PR #2351)', () => {
|
|
1121
|
+
// A type-only need from 'a-types' and a value need from 'z-value' — a
|
|
1122
|
+
// two-separately-sorted-lists implementation would put ALL value lines
|
|
1123
|
+
// before ALL type lines regardless of specifier, landing 'z-value'
|
|
1124
|
+
// before 'a-types'. The correct order interleaves by specifier: 'a-types'
|
|
1125
|
+
// (type-only) first, then 'z-value' (value).
|
|
1126
|
+
writeFixture('matrix18g/a-types.ts', `export interface Early { id: string }
|
|
1127
|
+
`)
|
|
1128
|
+
writeFixture('matrix18g/z-value.ts', `export function makeLate(): number { return 1 }
|
|
1129
|
+
`)
|
|
1130
|
+
writeFixture('matrix18g/useBoth.tsx', `'use client'
|
|
1131
|
+
import { createSignal } from '@barefootjs/client'
|
|
1132
|
+
import type { Early } from './a-types'
|
|
1133
|
+
import { makeLate } from './z-value'
|
|
1134
|
+
|
|
1135
|
+
export function useBoth() {
|
|
1136
|
+
function makeEarly(): Early {
|
|
1137
|
+
return { id: 'x' }
|
|
1138
|
+
}
|
|
1139
|
+
const [n, setN] = createSignal(makeLate())
|
|
1140
|
+
const [e, setE] = createSignal(makeEarly())
|
|
1141
|
+
return { n, setN, e, setE }
|
|
1142
|
+
}
|
|
1143
|
+
`)
|
|
1144
|
+
const consumerSource = `'use client'
|
|
1145
|
+
import { useBoth } from './useBoth'
|
|
1146
|
+
|
|
1147
|
+
export function App() {
|
|
1148
|
+
const { n, setN, e, setE } = useBoth()
|
|
1149
|
+
return <button onClick={() => setN(n() + 1)}>{n()} {e().id}</button>
|
|
1150
|
+
}
|
|
1151
|
+
`
|
|
1152
|
+
const consumerPath = writeFixture('matrix18g/App.tsx', consumerSource)
|
|
1153
|
+
|
|
1154
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
1155
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1156
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
1157
|
+
expect(template).toBeDefined()
|
|
1158
|
+
const aTypesIndex = template!.content.indexOf("from './a-types'")
|
|
1159
|
+
const zValueIndex = template!.content.indexOf("from './z-value'")
|
|
1160
|
+
expect(aTypesIndex).toBeGreaterThan(-1)
|
|
1161
|
+
expect(zValueIndex).toBeGreaterThan(-1)
|
|
1162
|
+
expect(aTypesIndex).toBeLessThan(zValueIndex)
|
|
1163
|
+
})
|
|
1164
|
+
|
|
914
1165
|
test('M19: a colliding binding nested inside a JSX callback still triggers BF113', () => {
|
|
915
1166
|
// Pins collectEntryBindingNames's depth: the ONLY `doubleIt` binding in
|
|
916
1167
|
// the consumer file is declared inside an onClick callback, not at any
|
|
@@ -88,14 +88,39 @@ describe('matchToLocaleDateStringCall accept/decline table', () => {
|
|
|
88
88
|
})
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
test('a canonical IANA zone timeZone is admitted via the build probe (#2344)', () => {
|
|
92
|
+
const node = match(`createdAt.toLocaleDateString('ja-JP', { timeZone: 'Asia/Tokyo' })`)
|
|
93
|
+
expect(node).toMatchObject({
|
|
94
|
+
helper: 'format_date',
|
|
95
|
+
args: [
|
|
96
|
+
{ kind: 'identifier', name: 'createdAt' },
|
|
97
|
+
{ kind: 'literal', value: 'YYYY/M/D' },
|
|
98
|
+
{ kind: 'literal', value: 'Asia/Tokyo' },
|
|
99
|
+
{ kind: 'array-literal', elements: [] },
|
|
100
|
+
],
|
|
101
|
+
})
|
|
102
|
+
expect(match(`createdAt.toLocaleDateString('en-US', { timeZone: 'America/New_York' })`)).toMatchObject({
|
|
103
|
+
helper: 'format_date',
|
|
104
|
+
args: [expect.anything(), { value: 'M/D/YYYY' }, { value: 'America/New_York' }, expect.anything()],
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('unverifiable timeZone literals decline (#2344 probe-and-verify)', () => {
|
|
109
|
+
// unknown zone: real toLocaleDateString throws RangeError
|
|
110
|
+
expect(match(`createdAt.toLocaleDateString('ja-JP', { timeZone: 'Asia/Tokyoo' })`)).toBeNull()
|
|
111
|
+
// non-canonical case: the probe canonicalizes it away, and backend
|
|
112
|
+
// tzdata layers disagree on case-folded spellings
|
|
113
|
+
expect(match(`createdAt.toLocaleDateString('ja-JP', { timeZone: 'asia/tokyo' })`)).toBeNull()
|
|
114
|
+
// host-environment aliases
|
|
115
|
+
expect(match(`createdAt.toLocaleDateString('ja-JP', { timeZone: 'Local' })`)).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
|
|
91
118
|
test('implicit-environment and runtime-value shapes all decline', () => {
|
|
92
119
|
// zero-arg / locale-only: reads host locale and/or timezone
|
|
93
120
|
expect(match(`createdAt.toLocaleDateString()`)).toBeNull()
|
|
94
121
|
expect(match(`createdAt.toLocaleDateString('ja-JP')`)).toBeNull()
|
|
95
122
|
// non-literal locale: no build-time CLDR resolution
|
|
96
123
|
expect(match(`createdAt.toLocaleDateString(locale, { timeZone: 'UTC' })`)).toBeNull()
|
|
97
|
-
// IANA zone name: host-tzdata coupling
|
|
98
|
-
expect(match(`createdAt.toLocaleDateString('ja-JP', { timeZone: 'Asia/Tokyo' })`)).toBeNull()
|
|
99
124
|
// non-literal timeZone
|
|
100
125
|
expect(match(`createdAt.toLocaleDateString('ja-JP', { timeZone: tz })`)).toBeNull()
|
|
101
126
|
// out-of-range fixed offsets: real toLocaleDateString throws RangeError
|