@pyreon/solid-compat 0.13.0 → 0.14.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/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/index.js +460 -20
- package/lib/index.js.map +1 -1
- package/lib/jsx-runtime.js +5 -0
- package/lib/jsx-runtime.js.map +1 -1
- package/lib/types/index.d.ts +194 -6
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/jsx-runtime.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +741 -25
- package/src/jsx-runtime.ts +9 -0
- package/src/tests/new-apis.test.ts +1539 -0
- package/src/tests/solid-compat.test.ts +366 -0
|
@@ -7,26 +7,36 @@ import {
|
|
|
7
7
|
createEffect,
|
|
8
8
|
createMemo,
|
|
9
9
|
createRenderEffect,
|
|
10
|
+
createResource,
|
|
10
11
|
createRoot,
|
|
11
12
|
createSelector,
|
|
12
13
|
createSignal,
|
|
14
|
+
createStore,
|
|
13
15
|
ErrorBoundary,
|
|
14
16
|
For,
|
|
17
|
+
from,
|
|
15
18
|
getOwner,
|
|
19
|
+
indexArray,
|
|
16
20
|
lazy,
|
|
21
|
+
mapArray,
|
|
17
22
|
Match,
|
|
18
23
|
mergeProps,
|
|
24
|
+
observable,
|
|
19
25
|
on,
|
|
20
26
|
onCleanup,
|
|
21
27
|
onMount,
|
|
28
|
+
produce,
|
|
22
29
|
runWithOwner,
|
|
23
30
|
Show,
|
|
31
|
+
startTransition,
|
|
24
32
|
Suspense,
|
|
25
33
|
Switch,
|
|
26
34
|
splitProps,
|
|
27
35
|
untrack,
|
|
28
36
|
useContext,
|
|
37
|
+
useTransition,
|
|
29
38
|
} from '../index'
|
|
39
|
+
import type { Accessor, Component, ParentComponent, Setter, Signal } from '../index'
|
|
30
40
|
import type { RenderContext } from '../jsx-runtime'
|
|
31
41
|
import { beginRender, endRender } from '../jsx-runtime'
|
|
32
42
|
|
|
@@ -620,6 +630,11 @@ describe('@pyreon/solid-compat', () => {
|
|
|
620
630
|
expect(useContext(Ctx)).toBe('default-value')
|
|
621
631
|
})
|
|
622
632
|
|
|
633
|
+
it('createContext returns object with Provider', () => {
|
|
634
|
+
const Ctx = createContext('test')
|
|
635
|
+
expect(typeof Ctx.Provider).toBe('function')
|
|
636
|
+
})
|
|
637
|
+
|
|
623
638
|
// ─── Re-exports ───────────────────────────────────────────────────────
|
|
624
639
|
|
|
625
640
|
it('Show is exported', () => {
|
|
@@ -783,6 +798,357 @@ describe('@pyreon/solid-compat', () => {
|
|
|
783
798
|
expect(result).toBe('hello')
|
|
784
799
|
})
|
|
785
800
|
|
|
801
|
+
// ─── createSignal equals option ────────────────────────────────────────
|
|
802
|
+
|
|
803
|
+
it('createSignal default skips update on same value (===)', () => {
|
|
804
|
+
let effectRuns = 0
|
|
805
|
+
createRoot((dispose) => {
|
|
806
|
+
const [count, setCount] = createSignal(5)
|
|
807
|
+
createEffect(() => {
|
|
808
|
+
count()
|
|
809
|
+
effectRuns++
|
|
810
|
+
})
|
|
811
|
+
expect(effectRuns).toBe(1)
|
|
812
|
+
setCount(5) // same value
|
|
813
|
+
expect(effectRuns).toBe(1) // no re-run
|
|
814
|
+
setCount(6)
|
|
815
|
+
expect(effectRuns).toBe(2)
|
|
816
|
+
dispose()
|
|
817
|
+
})
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
it('createSignal equals: false always notifies', () => {
|
|
821
|
+
let effectRuns = 0
|
|
822
|
+
createRoot((dispose) => {
|
|
823
|
+
const [count, setCount] = createSignal(5, { equals: false })
|
|
824
|
+
createEffect(() => {
|
|
825
|
+
count()
|
|
826
|
+
effectRuns++
|
|
827
|
+
})
|
|
828
|
+
expect(effectRuns).toBe(1)
|
|
829
|
+
setCount(5) // same value but equals: false
|
|
830
|
+
expect(effectRuns).toBe(2)
|
|
831
|
+
dispose()
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
it('createSignal custom equals function', () => {
|
|
836
|
+
let effectRuns = 0
|
|
837
|
+
createRoot((dispose) => {
|
|
838
|
+
const [obj, setObj] = createSignal(
|
|
839
|
+
{ id: 1, name: 'a' },
|
|
840
|
+
{ equals: (prev, next) => prev.id === next.id },
|
|
841
|
+
)
|
|
842
|
+
createEffect(() => {
|
|
843
|
+
obj()
|
|
844
|
+
effectRuns++
|
|
845
|
+
})
|
|
846
|
+
expect(effectRuns).toBe(1)
|
|
847
|
+
setObj({ id: 1, name: 'b' }) // same id
|
|
848
|
+
expect(effectRuns).toBe(1)
|
|
849
|
+
setObj({ id: 2, name: 'b' }) // different id
|
|
850
|
+
expect(effectRuns).toBe(2)
|
|
851
|
+
dispose()
|
|
852
|
+
})
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
// ─── createSignal getter/setter identity stability ────────────────────
|
|
856
|
+
|
|
857
|
+
it('createSignal returns stable getter/setter in component context', () => {
|
|
858
|
+
const runner = createHookRunner()
|
|
859
|
+
const [getter1, setter1] = runner.run(() => createSignal(0))
|
|
860
|
+
const [getter2, setter2] = runner.run(() => createSignal(0))
|
|
861
|
+
expect(getter1).toBe(getter2)
|
|
862
|
+
expect(setter1).toBe(setter2)
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
// ─── createResource ──────────────────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
it('createResource with fetcher only', async () => {
|
|
868
|
+
const [data] = createResource(() => Promise.resolve(42))
|
|
869
|
+
// While loading, data() throws the fetch promise (Suspense integration)
|
|
870
|
+
expect(() => data()).toThrow()
|
|
871
|
+
expect(data.loading).toBe(true)
|
|
872
|
+
|
|
873
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
874
|
+
|
|
875
|
+
expect(data()).toBe(42)
|
|
876
|
+
expect(data.loading).toBe(false)
|
|
877
|
+
expect(data.error).toBeUndefined()
|
|
878
|
+
expect(data.latest).toBe(42)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
it('createResource with sync fetcher', () => {
|
|
882
|
+
const [data] = createResource(() => 'sync-value')
|
|
883
|
+
expect(data()).toBe('sync-value')
|
|
884
|
+
expect(data.loading).toBe(false)
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it('createResource with source and fetcher', async () => {
|
|
888
|
+
const [userId, setUserId] = createSignal(1)
|
|
889
|
+
|
|
890
|
+
createRoot(async (dispose) => {
|
|
891
|
+
const [data] = createResource(userId, async (id) => `user-${id}`)
|
|
892
|
+
|
|
893
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
894
|
+
expect(data()).toBe('user-1')
|
|
895
|
+
|
|
896
|
+
setUserId(2)
|
|
897
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
898
|
+
expect(data()).toBe('user-2')
|
|
899
|
+
|
|
900
|
+
dispose()
|
|
901
|
+
})
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it('createResource mutate updates data', async () => {
|
|
905
|
+
const [data, { mutate }] = createResource(() => Promise.resolve(10))
|
|
906
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
907
|
+
expect(data()).toBe(10)
|
|
908
|
+
|
|
909
|
+
mutate(99)
|
|
910
|
+
expect(data()).toBe(99)
|
|
911
|
+
expect(data.latest).toBe(99)
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
it('createResource mutate with updater function', async () => {
|
|
915
|
+
const [data, { mutate }] = createResource(() => Promise.resolve(10))
|
|
916
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
917
|
+
|
|
918
|
+
mutate((prev) => (prev ?? 0) + 5)
|
|
919
|
+
expect(data()).toBe(15)
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
it('createResource handles errors', async () => {
|
|
923
|
+
const [data] = createResource(() => Promise.reject(new Error('fail')))
|
|
924
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
925
|
+
|
|
926
|
+
// data() throws the error (ErrorBoundary catches this)
|
|
927
|
+
expect(() => data()).toThrow('fail')
|
|
928
|
+
expect(data.error).toBeInstanceOf(Error)
|
|
929
|
+
expect(data.error?.message).toBe('fail')
|
|
930
|
+
expect(data.loading).toBe(false)
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
it('createResource handles sync errors', () => {
|
|
934
|
+
const [data] = createResource(() => {
|
|
935
|
+
throw new Error('sync-fail')
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
// data() throws the error
|
|
939
|
+
expect(() => data()).toThrow('sync-fail')
|
|
940
|
+
expect(data.error?.message).toBe('sync-fail')
|
|
941
|
+
expect(data.loading).toBe(false)
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
it('createResource with falsy source skips fetch', () => {
|
|
945
|
+
let fetchCount = 0
|
|
946
|
+
const [enabled] = createSignal(false)
|
|
947
|
+
|
|
948
|
+
createRoot((dispose) => {
|
|
949
|
+
createResource(enabled, () => {
|
|
950
|
+
fetchCount++
|
|
951
|
+
return 'fetched'
|
|
952
|
+
})
|
|
953
|
+
expect(fetchCount).toBe(0)
|
|
954
|
+
dispose()
|
|
955
|
+
})
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
it('createResource refetch re-fetches', async () => {
|
|
959
|
+
let fetchCount = 0
|
|
960
|
+
const [, { refetch }] = createResource(async () => {
|
|
961
|
+
fetchCount++
|
|
962
|
+
return fetchCount
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
966
|
+
expect(fetchCount).toBe(1)
|
|
967
|
+
|
|
968
|
+
refetch()
|
|
969
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
970
|
+
expect(fetchCount).toBe(2)
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
// ─── createStore / produce ────────────────────────────────────────────
|
|
974
|
+
|
|
975
|
+
it('createStore returns reactive proxy and setter', () => {
|
|
976
|
+
const [store, setStore] = createStore({ count: 0, name: 'test' })
|
|
977
|
+
expect(store.count).toBe(0)
|
|
978
|
+
expect(store.name).toBe('test')
|
|
979
|
+
|
|
980
|
+
setStore((s: { count: number; name: string }) => {
|
|
981
|
+
s.count = 5
|
|
982
|
+
})
|
|
983
|
+
expect(store.count).toBe(5)
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
it('createStore proxy is reactive in effects', () => {
|
|
987
|
+
let effectRuns = 0
|
|
988
|
+
createRoot((dispose) => {
|
|
989
|
+
const [store, setStore] = createStore({ value: 1 })
|
|
990
|
+
createEffect(() => {
|
|
991
|
+
void store.value // read through proxy triggers signal read
|
|
992
|
+
effectRuns++
|
|
993
|
+
})
|
|
994
|
+
expect(effectRuns).toBe(1)
|
|
995
|
+
setStore((s: { value: number }) => {
|
|
996
|
+
s.value = 2
|
|
997
|
+
})
|
|
998
|
+
expect(effectRuns).toBe(2)
|
|
999
|
+
dispose()
|
|
1000
|
+
})
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
it('createStore proxy prevents direct mutation', () => {
|
|
1004
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
1005
|
+
const [store] = createStore({ count: 0 })
|
|
1006
|
+
;(store as Record<string, unknown>).count = 5
|
|
1007
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1008
|
+
'[Pyreon] Direct mutation on store is not supported. Use the setter function.',
|
|
1009
|
+
)
|
|
1010
|
+
warn.mockRestore()
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
it('produce creates a reusable updater', () => {
|
|
1014
|
+
const increment = produce<{ count: number }>((s) => {
|
|
1015
|
+
s.count++
|
|
1016
|
+
})
|
|
1017
|
+
const result = increment({ count: 5 })
|
|
1018
|
+
expect(result.count).toBe(6)
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
// ─── startTransition / useTransition ──────────────────────────────────
|
|
1022
|
+
|
|
1023
|
+
it('startTransition runs fn synchronously', () => {
|
|
1024
|
+
let ran = false
|
|
1025
|
+
startTransition(() => {
|
|
1026
|
+
ran = true
|
|
1027
|
+
})
|
|
1028
|
+
expect(ran).toBe(true)
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
it('useTransition returns [isPending, start]', () => {
|
|
1032
|
+
const [isPending, start] = useTransition()
|
|
1033
|
+
expect(isPending()).toBe(false)
|
|
1034
|
+
let ran = false
|
|
1035
|
+
start(() => {
|
|
1036
|
+
ran = true
|
|
1037
|
+
})
|
|
1038
|
+
expect(ran).toBe(true)
|
|
1039
|
+
expect(isPending()).toBe(false)
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
// ─── observable ───────────────────────────────────────────────────────
|
|
1043
|
+
|
|
1044
|
+
it('observable converts signal to subscribable', () => {
|
|
1045
|
+
createRoot((dispose) => {
|
|
1046
|
+
const [count, setCount] = createSignal(0)
|
|
1047
|
+
const obs = observable(count)
|
|
1048
|
+
const values: number[] = []
|
|
1049
|
+
const sub = obs.subscribe({ next: (v) => values.push(v) })
|
|
1050
|
+
|
|
1051
|
+
expect(values).toEqual([0])
|
|
1052
|
+
setCount(1)
|
|
1053
|
+
expect(values).toEqual([0, 1])
|
|
1054
|
+
|
|
1055
|
+
sub.unsubscribe()
|
|
1056
|
+
setCount(2)
|
|
1057
|
+
expect(values).toEqual([0, 1]) // no more updates
|
|
1058
|
+
|
|
1059
|
+
dispose()
|
|
1060
|
+
})
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
// ─── from ─────────────────────────────────────────────────────────────
|
|
1064
|
+
|
|
1065
|
+
it('from with producer function', () => {
|
|
1066
|
+
createRoot((dispose) => {
|
|
1067
|
+
let setter: ((v: number) => void) | undefined
|
|
1068
|
+
const value = from<number>((set) => {
|
|
1069
|
+
setter = set
|
|
1070
|
+
return () => {} // cleanup
|
|
1071
|
+
})
|
|
1072
|
+
expect(value()).toBeUndefined()
|
|
1073
|
+
setter!(42)
|
|
1074
|
+
expect(value()).toBe(42)
|
|
1075
|
+
dispose()
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
it('from with observable', () => {
|
|
1080
|
+
createRoot((dispose) => {
|
|
1081
|
+
const [count, setCount] = createSignal(0)
|
|
1082
|
+
const obs = observable(count)
|
|
1083
|
+
const derived = from(obs)
|
|
1084
|
+
|
|
1085
|
+
// from subscribes to observable, initial value propagated
|
|
1086
|
+
expect(derived()).toBe(0)
|
|
1087
|
+
setCount(5)
|
|
1088
|
+
expect(derived()).toBe(5)
|
|
1089
|
+
|
|
1090
|
+
dispose()
|
|
1091
|
+
})
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
// ─── mapArray ─────────────────────────────────────────────────────────
|
|
1095
|
+
|
|
1096
|
+
it('mapArray maps items with reactive index', () => {
|
|
1097
|
+
createRoot((dispose) => {
|
|
1098
|
+
const [list] = createSignal(['a', 'b', 'c'])
|
|
1099
|
+
const mapped = mapArray(list, (item, index) => `${item}-${index()}`)
|
|
1100
|
+
expect(mapped()).toEqual(['a-0', 'b-1', 'c-2'])
|
|
1101
|
+
dispose()
|
|
1102
|
+
})
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
it('mapArray updates when source changes', () => {
|
|
1106
|
+
createRoot((dispose) => {
|
|
1107
|
+
const [list, setList] = createSignal([1, 2, 3])
|
|
1108
|
+
const doubled = mapArray(list, (item) => item * 2)
|
|
1109
|
+
expect(doubled()).toEqual([2, 4, 6])
|
|
1110
|
+
setList([4, 5])
|
|
1111
|
+
expect(doubled()).toEqual([8, 10])
|
|
1112
|
+
dispose()
|
|
1113
|
+
})
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
// ─── indexArray ───────────────────────────────────────────────────────
|
|
1117
|
+
|
|
1118
|
+
it('indexArray maps items with static index', () => {
|
|
1119
|
+
createRoot((dispose) => {
|
|
1120
|
+
const [list] = createSignal(['x', 'y', 'z'])
|
|
1121
|
+
const mapped = indexArray(list, (item, index) => `${item()}-${index}`)
|
|
1122
|
+
expect(mapped()).toEqual(['x-0', 'y-1', 'z-2'])
|
|
1123
|
+
dispose()
|
|
1124
|
+
})
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it('indexArray updates when source changes', () => {
|
|
1128
|
+
createRoot((dispose) => {
|
|
1129
|
+
const [list, setList] = createSignal([10, 20])
|
|
1130
|
+
const doubled = indexArray(list, (item) => item() * 2)
|
|
1131
|
+
expect(doubled()).toEqual([20, 40])
|
|
1132
|
+
setList([30])
|
|
1133
|
+
expect(doubled()).toEqual([60])
|
|
1134
|
+
dispose()
|
|
1135
|
+
})
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
// ─── Type exports ─────────────────────────────────────────────────────
|
|
1139
|
+
|
|
1140
|
+
it('type exports are usable', () => {
|
|
1141
|
+
// These are compile-time checks — just verify they don't cause runtime errors
|
|
1142
|
+
const _accessor: Accessor<number> = () => 42
|
|
1143
|
+
const _setter: Setter<number> = () => {}
|
|
1144
|
+
const _signal: Signal<number> = [_accessor, _setter]
|
|
1145
|
+
const _component: Component<{ name: string }> = () => null
|
|
1146
|
+
const _parent: ParentComponent<{ title: string }> = () => null
|
|
1147
|
+
expect(_signal).toHaveLength(2)
|
|
1148
|
+
expect(typeof _component).toBe('function')
|
|
1149
|
+
expect(typeof _parent).toBe('function')
|
|
1150
|
+
})
|
|
1151
|
+
|
|
786
1152
|
// ─── JSX runtime ───────────────────────────────────────────────────────
|
|
787
1153
|
|
|
788
1154
|
it('jsx-runtime exports are available', async () => {
|