This commit is contained in:
2026-09-05 04:33:55 +05:30
commit 02a4264bc3
121 changed files with 7913 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "bun:test";
import { renderHook, act } from "@testing-library/react";
import { useSWR } from "../src/index";
const fetcher = vi.fn((key: string) => Promise.resolve(`data-${key}`));
describe("useSWR (React)", () => {
beforeEach(() => {
vi.useFakeTimers();
fetcher.mockClear();
});
it("fetches data on mount", async () => {
const { result } = renderHook(() => useSWR("test-key", fetcher));
expect(result.current.isLoading).toBe(true);
await act(async () => {
await vi.runAllTimers();
});
expect(result.current.data).toBe("data-test-key");
expect(result.current.isLoading).toBe(false);
});
it("deduplicates requests", async () => {
const { result: r1 } = renderHook(() => useSWR("dedupe-key", fetcher));
const { result: r2 } = renderHook(() => useSWR("dedupe-key", fetcher));
await act(async () => {
await vi.runAllTimers();
});
expect(fetcher).toHaveBeenCalledTimes(1);
expect(r1.current.data).toBe("data-dedupe-key");
expect(r2.current.data).toBe("data-dedupe-key");
});
it("mutates data manually", async () => {
const { result } = renderHook(() => useSWR("mutate-key", fetcher));
await act(async () => {
await vi.runAllTimers();
});
expect(result.current.data).toBe("data-mutate-key");
await act(async () => {
await result.current.mutate("new-data");
await vi.runAllTimers();
});
expect(result.current.data).toBe("new-data");
});
// it("handles errors", async () => {
// const error = new Error("failed");
// fetcher.mockImplementationOnce(() => Promise.reject(error));
// const { result } = renderHook(() => useSWR("error-key", fetcher));
// await act(async () => {
// await vi.runAllTimers();
// });
// expect(result.current.error).toBe(error);
// expect(result.current.isLoading).toBe(false);
// });
});
+2
View File
@@ -0,0 +1,2 @@
[test]
preload = ["./happydom.ts", "./testing-library.ts"]
+3
View File
@@ -0,0 +1,3 @@
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register();
+7
View File
@@ -0,0 +1,7 @@
import { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
import { Matchers, AsymmetricMatchers } from "bun:test";
declare module "bun:test" {
interface Matchers<T> extends TestingLibraryMatchers<typeof expect.stringContaining, void> {}
interface AsymmetricMatchers extends TestingLibraryMatchers<any, any> {}
}
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@supwr/react",
"version": "0.0.1",
"type": "module",
"files": [
"dist"
],
"module": "./dist/index.js",
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": "./package.json"
},
"scripts": {
"build": "bunup"
},
"devDependencies": {
"@types/bun": "^1.4.0",
"@types/react": "^19.2.18",
"@happy-dom/global-registrator": "^20.12.0",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"bunup": "^0.16.32"
},
"dependencies": {
"@supwr/core": "workspace:*"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
// Main exports
// React SWR hooks
export { default as useSWR, SWRConfig, useSWRMutate, useSWRInstance, useSWRPrefetch } from './use-swr'
export { default as useSWRInfinite } from './use-swr-infinite'
export type { SWRResponse, SWRHook } from './types'
+5
View File
@@ -0,0 +1,5 @@
import { BareFetcher, Key, SWRConfig } from "@supwr/core";
import { SWRHook, SWRResponse } from "./types";
export type Middleware = (useSWRNext: SWRHook) => <Data = any, Error = any>(key: Key, fetcher: BareFetcher<Data> | null, config: SWRConfig<Key, Error>) => SWRResponse<Data, Error>;
+53
View File
@@ -0,0 +1,53 @@
import type { SWRConfig as CoreSWRConfig, MutatorOptions} from '@supwr/core'
import { Middleware } from './middleware';
export interface SWRConfig<Data = unknown, Error = unknown> extends CoreSWRConfig<Data, Error> {
// React-specific config
suspense?: boolean
fallbackData?: Data
revalidateOnMount?: boolean
use?: Middleware[]
revalidateOnFocus?: boolean
revalidateOnReconnect?: boolean
refreshWhenHidden?: boolean
refreshWhenOffline?: boolean
compare?: (a: Data | undefined, b: Data | undefined) => boolean
}
export interface SWRResponse<Data = unknown, Error = unknown> {
data: Data | undefined
error: Error | undefined
isLoading: boolean
isValidating: boolean
mutate: (data?: Data | Promise<Data> | ((currentData?: Data) => Data | Promise<Data>), options?: MutatorOptions<Data>) => Promise<Data | undefined>
}
export interface SWRHook<Data = unknown, Error = unknown> extends SWRResponse<Data, Error> {
// Additional hook-specific properties can be added here
}
// Infinite loading types
export interface SWRInfiniteKeyLoader<Data = any, Args extends any[] = any[]> {
(index: number, previousPageData: Data | null): Args | null | undefined
}
export interface SWRInfiniteConfig<Data = unknown, Error = unknown> extends Omit<SWRConfig<Data[], Error>, 'compare'> {
initialSize?: number
revalidateAll?: boolean
persistSize?: boolean
revalidateFirstPage?: boolean
parallel?: boolean
compare?: (a: Data[] | undefined, b: Data[] | undefined) => boolean
}
export interface SWRInfiniteResponse<Data = unknown, Error = unknown> {
data: Data[] | undefined
error: Error | undefined
isLoading: boolean
isValidating: boolean
loading: boolean
validating: boolean
size: number
setSize: (size: number | ((size: number) => number)) => Promise<Data[][]>
mutate: (data?: Data[] | Promise<Data[]> | ((currentData?: Data[]) => Data[] | Promise<Data[]>), options?: MutatorOptions<Data[]>) => Promise<Data[] | undefined>
}
+224
View File
@@ -0,0 +1,224 @@
import { useCallback, useDebugValue, useMemo, useState, useSyncExternalStore } from 'react'
import { createSWR } from '@supwr/core'
import type {
SWRInfiniteKeyLoader,
SWRInfiniteConfig,
SWRInfiniteResponse,
} from './types'
import {
Key,
Fetcher,
SWRFetcher,
BareFetcher
} from "@supwr/core"
// Global SWR instance for infinite loading
const globalSWR = createSWR()
/**
* useSWRInfinite hook for infinite loading
*/
export function useSWRInfinite<Data = any, Error = any>(
getKey: SWRInfiniteKeyLoader<Data> | null,
fetcher?: SWRFetcher<Data> | null,
config?: Partial<SWRInfiniteConfig<Data, Error>>
): SWRInfiniteResponse<Data, Error> {
const [size, setSize] = useState(config?.initialSize || 1)
// Generate all keys for current size
const keys = useMemo(() => {
if (!getKey) return []
const keys: Key[] = []
for (let i = 0; i < size; i++) {
const previousPageData = i === 0 ? null : (globalSWR.get(keys[i - 1])?.data as Data) || null
const keyArgs = getKey(i, previousPageData)
if (keyArgs === null || keyArgs === undefined) {
break
}
keys.push(keyArgs)
}
return keys
}, [getKey, size])
// Normalize fetcher
const normalizedFetcher = useMemo(() => {
if (!fetcher) return null
if (fetcher.length === 2) return fetcher as Fetcher<Data>
return (key: Key, options: any) => {
if (Array.isArray(key)) {
return (fetcher as BareFetcher<Data>)(...key)
}
return (fetcher as BareFetcher<Data>)(key)
}
}, [fetcher])
// Get data for all pages
const data = useMemo(() => {
if (keys.length === 0) return undefined
const pages: Data[] = []
for (const key of keys) {
const entry = globalSWR.get(key)
if (entry?.data !== undefined) {
pages.push(entry.data as Data)
} else {
// If any page doesn't have data, return undefined
return undefined
}
}
return pages
}, [keys])
// Check loading states
const isLoading = useMemo(() => {
if (keys.length === 0) return false
// Check if first page is loading
const firstEntry = globalSWR.get(keys[0])
return firstEntry?.isLoading || false
}, [keys])
const isValidating = useMemo(() => {
if (keys.length === 0) return false
// Check if any page is validating
for (const key of keys) {
const entry = globalSWR.get(key)
if (entry?.isValidating) return true
}
return false
}, [keys])
// Get error from any page
const error = useMemo(() => {
if (keys.length === 0) return undefined
for (const key of keys) {
const entry = globalSWR.get(key)
if (entry?.error) return entry.error
}
return undefined
}, [keys])
// Subscribe to all key changes
const subscribe = useCallback((callback: () => void) => {
if (keys.length === 0) return () => { }
const unsubscribes = keys.map(key =>
globalSWR.subscribe(key, callback)
)
return () => {
unsubscribes.forEach(unsubscribe => unsubscribe())
}
}, [keys])
const getSnapshot = useCallback(() => ({
data,
error,
isLoading,
isValidating
}), [data, error, isLoading, isValidating])
// Use sync external store
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
// Create mutate function for infinite data
const mutate = useCallback(
async (mutateData?: any, options?: any) => {
if (keys.length === 0) return undefined
if (mutateData === undefined) {
// Revalidate all pages
const promises = keys.map(key => globalSWR.revalidate(key, options))
return Promise.all(promises) as any
}
// Mutate specific page or all pages
if (typeof mutateData === 'function') {
// Apply function to all pages
const currentData = data || []
const newData = await Promise.all(currentData.map((page, index) =>
(mutateData as any)(page, index)
))
// Update each page
const promises = keys.map((key, index) =>
globalSWR.mutate(key, newData[index], options)
)
const results = await Promise.all(promises)
return results[0] as Promise<Data>
} else if (Array.isArray(mutateData)) {
// Update specific pages
const promises = mutateData.map((pageData, index) =>
keys[index] ? globalSWR.mutate(keys[index], pageData, options) : Promise.resolve(undefined)
)
const results = await Promise.all(promises)
return results[0] as Promise<Data>
} else {
// Update first page
return globalSWR.mutate(keys[0], mutateData, options)
}
},
[keys, data]
)
// Create setSize function
const setSizeFunction = useCallback(
(newSize: number | ((size: number) => number)) => {
const targetSize = typeof newSize === 'function' ? newSize(size) : newSize
if (targetSize !== size) {
setSize(targetSize)
}
// Return promise that resolves when new pages are loaded
return new Promise<Data[][]>((resolve) => {
// This will resolve when the data is updated
setTimeout(() => {
if (!getKey) {
resolve([])
return
}
const pages: Data[] = []
for (let i = 0; i < targetSize; i++) {
const previousPageData = i === 0 ? null : (globalSWR.get(keys[i - 1])?.data as Data) || null
const keyArgs = getKey(i, previousPageData)
if (keyArgs === null || keyArgs === undefined) break
const entry = globalSWR.get(keyArgs)
if (entry?.data) {
pages.push(entry.data as Data)
}
}
resolve([pages])
}, 0)
})
},
[size, getKey, keys]
)
// Debug value
useDebugValue(data ? `${data.length} pages` : 'loading')
return {
...state,
loading: state.isLoading,
validating: state.isValidating,
error: state.error as Error,
size,
setSize: setSizeFunction,
mutate
}
}
export default useSWRInfinite
+201
View File
@@ -0,0 +1,201 @@
import React, { useCallback, useRef, useDebugValue, useMemo, useEffect, useContext, createContext, ReactNode, useSyncExternalStore } from 'react'
import { createSWR } from '@supwr/core'
import type { Key, Fetcher, SWRFetcher, BareFetcher, SWRCore } from '@supwr/core'
import { SWRResponse, SWRConfig as SWRConfiguration } from './types';
const globalSWR = createSWR()
const ConfigContext = createContext<Partial<SWRConfiguration<any, any>>>({})
export function useSWR<Data = any, Error = any>(
key: Key | null,
fetcher?: SWRFetcher<Data> | null,
config?: Partial<SWRConfiguration<Data, Error>>
): SWRResponse<Data, Error> {
const contextConfig = useContext(ConfigContext)
const mergedConfig = useMemo(() => ({
...contextConfig,
...config
}), [contextConfig, config])
const refreshInterval = mergedConfig?.refreshInterval
const swrRef = useRef(globalSWR)
useEffect(() => {
if (mergedConfig) {
const { refreshInterval: _, ...configWithoutRefresh } = mergedConfig
swrRef.current.updateConfig(configWithoutRefresh as SWRConfig)
}
}, [mergedConfig])
useEffect(() => {
if (!key || !refreshInterval || refreshInterval <= 0) return
const intervalId = setInterval(() => {
swrRef.current.revalidate(key, {
fetcher: fetcherRef.current as any,
dedupe: true
})
}, refreshInterval)
return () => clearInterval(intervalId)
}, [key, refreshInterval])
const normalizedFetcher = useMemo(() => {
if (!fetcher) return null
if (fetcher.length === 2) return fetcher as Fetcher<Data>
return (key: Key, options: any) => {
if (Array.isArray(key)) {
return (fetcher as BareFetcher<Data>)(...key)
}
return (fetcher as BareFetcher<Data>)(key)
}
}, [fetcher])
const fetcherRef = useRef(normalizedFetcher)
useEffect(() => {
fetcherRef.current = normalizedFetcher
}, [normalizedFetcher])
const subscribe = useCallback((callback: () => void) => {
if (!key) return () => { }
return swrRef.current.addEventListener(key, () => {
callback()
})
}, [key])
const lastStateRef = useRef<any>(null)
const getSnapshot = useCallback(() => {
let nextState: any;
if (!key || !fetcherRef.current) {
nextState = {
data: mergedConfig?.fallbackData,
error: undefined,
isValidating: false,
isLoading: false
}
} else {
const entry = swrRef.current.get(key)
const data = entry?.data ?? mergedConfig?.fallbackData
nextState = {
data,
error: entry?.error,
isValidating: entry?.isValidating || false,
isLoading: entry?.isLoading || false
}
}
if (
lastStateRef.current &&
lastStateRef.current.data === nextState.data &&
lastStateRef.current.error === nextState.error &&
lastStateRef.current.isValidating === nextState.isValidating &&
lastStateRef.current.isLoading === nextState.isLoading
) {
return lastStateRef.current
}
lastStateRef.current = nextState
return nextState
}, [key, mergedConfig?.fallbackData])
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
const mutate = useCallback(
(data?: any, options?: any) => {
if (!key) return Promise.resolve(undefined)
return swrRef.current.mutate(key, data, options)
},
[key]
)
const suspense = mergedConfig?.suspense
const hasData = state.data !== undefined
const hasError = state.error !== undefined
useEffect(() => {
if (!key || !fetcherRef.current || suspense) return
const entry = swrRef.current.get(key)
const revalidateOnMount = mergedConfig?.revalidateOnMount !== false
if (!entry && revalidateOnMount) {
swrRef.current.setLoading(key)
swrRef.current.revalidate(key, {
fetcher: fetcherRef.current as any,
dedupe: true
})
} else if (entry && revalidateOnMount && !entry.data) {
swrRef.current.revalidate(key, {
fetcher: fetcherRef.current as any,
dedupe: true
})
}
}, [key, suspense, mergedConfig?.revalidateOnMount])
if (suspense) {
if (hasError) {
throw state.error
}
if (!hasData) {
const promise = swrRef.current.revalidate(key, {
fetcher: fetcherRef.current as any,
dedupe: true
}).then(() => true)
;(promise as any).catch(() => {})
throw promise
}
}
useDebugValue(state.data ? `data: ${JSON.stringify(state.data)}` : 'loading')
return {
...state,
loading: state.isLoading,
validating: state.isValidating,
mutate
}
}
export function SWRConfig<Data = any, Error = any>({
children,
value
}: {
children: ReactNode
value: Partial<SWRConfiguration<Data, Error>>
}): React.ReactNode {
return (
<ConfigContext.Provider value={value}>
{children}
</ConfigContext.Provider>
)
}
export type SWRConfig<Data = any, Error = any> = SWRConfiguration<Data, Error>
export function useSWRMutate<Data = any, Error = any>() {
return useCallback(
(key: Key, data?: any, options?: any) => {
return globalSWR.mutate(key, data, options)
},
[]
)
}
export function useSWRInstance(): SWRCore<unknown, unknown> {
return globalSWR
}
export function useSWRPrefetch<Data = any, Error = any>() {
return useCallback(
(key: Key, fetcher: SWRFetcher<Data>) => globalSWR.revalidate(key, { dedupe: true }), []
)
}
export default useSWR
+10
View File
@@ -0,0 +1,10 @@
import { afterEach, expect } from "bun:test";
import { cleanup } from "@testing-library/react";
import * as matchers from "@testing-library/jest-dom/matchers";
expect.extend(matchers);
// Optional: cleans up `render` after each test
afterEach(() => {
cleanup();
});
+39
View File
@@ -0,0 +1,39 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": [
"DOM",
"ES2022"
],
"target": "ES2018",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": [
"bun"
],
// Bundler mode
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}