diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9052950 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [main, next] + pull_request: + branches: [main, next] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bunx tsc --noEmit + + - name: Lint and format + run: bunx biome check . + + - name: Unit tests + run: bun run test + + - name: Bundle size regression + run: bun test test/regression-bundle.test.ts + + - name: Build + run: bun run build + + performance: + runs-on: ubuntu-latest + continue-on-error: true # informational only — shared runners are too noisy for timing assertions + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Performance regression (informational) + run: bun test test/regression-performance.test.ts diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 7e79784..91bdf9c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -22,6 +22,9 @@ jobs: - name: Install all dependencies (including devDependencies for build) run: bun install + - name: Typecheck, lint, test, and bundle regression + run: bun run check + - name: Build package run: bun run build diff --git a/.zed/settings.json b/.zed/settings.json index 1b9d380..c7c2815 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -4,22 +4,22 @@ "language_servers": ["!eslint", "..."], "code_actions_on_format": { "source.fixAll.biome": true, - "source.organizeImports.biome": true, - }, + "source.organizeImports.biome": true + } }, "JavaScript": { "language_servers": ["!eslint", "..."], "code_actions_on_format": { "source.fixAll.biome": true, - "source.organizeImports.biome": true, - }, + "source.organizeImports.biome": true + } }, "TSX": { "language_servers": ["!eslint", "..."], "code_actions_on_format": { "source.fixAll.biome": true, - "source.organizeImports.biome": true, - }, - }, - }, + "source.organizeImports.biome": true + } + } + } } diff --git a/bench/reactivity.bench.ts b/bench/reactivity.bench.ts index 837a0ad..7cae98a 100644 --- a/bench/reactivity.bench.ts +++ b/bench/reactivity.bench.ts @@ -648,8 +648,7 @@ group('Collection: chain 5 derivations (100 items)', () => { bench('cause-effect', () => { const list = createList(Array.from({ length: 100 }, (_, i) => i + 1)) let col = list.deriveCollection((v: number) => v * 2) - for (let i = 1; i < 5; i++) - col = col.deriveCollection((v: number) => v + 1) + for (let i = 1; i < 5; i++) col = col.deriveCollection((v: number) => v + 1) col.get() }) }) diff --git a/biome.json b/biome.json index 88f22a6..f8c0fd6 100644 --- a/biome.json +++ b/biome.json @@ -21,7 +21,9 @@ }, "javascript": { "formatter": { - "quoteStyle": "double" + "quoteStyle": "single", + "semicolons": "asNeeded", + "arrowParentheses": "asNeeded" } }, "assist": { diff --git a/package.json b/package.json index faa1f55..104d168 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "bench": "bun run bench/reactivity.bench.ts", "test": "bun test --path-ignore-patterns=**/regression*", "regression": "bun test test/regression-performance.test.ts && bun test test/regression-bundle.test.ts", - "lint": "bunx biome lint --write" + "check": "bunx tsc --noEmit && bunx biome check . && bun run test && bun test test/regression-bundle.test.ts", + "lint": "bunx biome check --write" } } diff --git a/src/errors.ts b/src/errors.ts index a8c95e0..659a0d2 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -173,10 +173,7 @@ class InvalidStoreMutationError extends TypeError { * @param prop - The property name that was directly mutated. * @param action - The kind of mutation attempted (`'assign to'`, `'delete'`, or `'define'`). */ - constructor( - prop: string, - action: 'assign to' | 'delete' | 'define', - ) { + constructor(prop: string, action: 'assign to' | 'delete' | 'define') { const guidance = action === 'delete' ? `use store.remove(${JSON.stringify(prop)})` diff --git a/src/graph.ts b/src/graph.ts index 64da080..12f2b25 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -236,12 +236,7 @@ const deepEqualInner = ( ): boolean => { if (Object.is(a, b)) return true if (typeof a !== typeof b) return false - if ( - a == null || - typeof a !== 'object' || - b == null || - typeof b !== 'object' - ) + if (a == null || typeof a !== 'object' || b == null || typeof b !== 'object') return false // Cycle guard: if `a` is already on the current recursion path, treat this @@ -406,8 +401,7 @@ function propagate(node: SinkNode, newFlag = FLAG_DIRTY): void { } // Propagate Check to sinks - for (let e = node.sinks; e; e = e.nextSink) - propagate(e.sink, FLAG_CHECK) + for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink, FLAG_CHECK) } else { if ((flags & (FLAG_DIRTY | FLAG_CHECK)) >= newFlag) return @@ -520,8 +514,7 @@ function recomputeTask(node: TaskNode): void { if (node.error || !node.equals(next, node.value)) { node.value = next node.error = undefined - for (let e = node.sinks; e; e = e.nextSink) - propagate(e.sink) + for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink) } setState(node.pendingNode, false) }) @@ -539,8 +532,7 @@ function recomputeTask(node: TaskNode): void { ) { // We don't clear old value on errors node.error = error - for (let e = node.sinks; e; e = e.nextSink) - propagate(e.sink) + for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink) } setState(node.pendingNode, false) }) @@ -580,11 +572,7 @@ function refresh(node: SinkNode): void { if (node.flags & FLAG_RUNNING) { throw new CircularDependencyError( - 'controller' in node - ? TYPE_TASK - : 'value' in node - ? TYPE_MEMO - : 'Effect', + 'controller' in node ? TYPE_TASK : 'value' in node ? TYPE_MEMO : 'Effect', ) } diff --git a/src/nodes/collection.ts b/src/nodes/collection.ts index c364b26..00cc4f3 100644 --- a/src/nodes/collection.ts +++ b/src/nodes/collection.ts @@ -61,9 +61,7 @@ type Collection = Signal> = { byKey(key: string): S | undefined keyAt(index: number): string | undefined indexOfKey(key: string): number - deriveCollection( - callback: (sourceValue: T) => R, - ): Collection + deriveCollection(callback: (sourceValue: T) => R): Collection deriveCollection( callback: (sourceValue: T, abort: AbortSignal) => Promise, ): Collection @@ -154,10 +152,7 @@ function deriveCollection( const sourceValue = itemSignal.get() as U if (sourceValue == null) return prev as T return ( - callback as ( - sourceValue: U, - abort: AbortSignal, - ) => Promise + callback as (sourceValue: U, abort: AbortSignal) => Promise )(sourceValue, abort) }) : createMemo(() => { diff --git a/src/nodes/effect.ts b/src/nodes/effect.ts index 368811c..028179a 100644 --- a/src/nodes/effect.ts +++ b/src/nodes/effect.ts @@ -14,8 +14,8 @@ import { registerCleanup, runCleanup, runEffect, - scheduleEffect, type Signal, + scheduleEffect, trimSources, } from '../graph' import { isTask } from './task' @@ -32,9 +32,11 @@ type MaybePromise = T | Promise */ type MatchHandlers[]> = { /** Called when all signals have a value. Receives a tuple of resolved values. */ - ok: (values: { - [K in keyof T]: T[K] extends Signal ? V : never - }) => MaybePromise + ok: ( + values: { + [K in keyof T]: T[K] extends Signal ? V : never + }, + ) => MaybePromise /** Called when one or more signals hold an error. Defaults to `console.error`. */ err?: (errors: readonly Error[]) => MaybePromise /** Called when one or more signals are unset (pending). */ @@ -221,12 +223,14 @@ function match( const owner = activeOwner const controller = new AbortController() registerCleanup(owner, () => controller.abort()) - out.then(cleanup => { - if (!controller.signal.aborted && typeof cleanup === 'function') - registerCleanup(owner, cleanup) - }).catch(e => { - err([e instanceof Error ? e : new Error(String(e))]) - }) + out + .then(cleanup => { + if (!controller.signal.aborted && typeof cleanup === 'function') + registerCleanup(owner, cleanup) + }) + .catch(e => { + err([e instanceof Error ? e : new Error(String(e))]) + }) } } diff --git a/src/nodes/list.ts b/src/nodes/list.ts index 08a0ed4..68bc72b 100644 --- a/src/nodes/list.ts +++ b/src/nodes/list.ts @@ -99,9 +99,7 @@ type List = MutableSignal> = { replace(key: string, value: T): void sort(compareFn?: (a: T, b: T) => number): void splice(start: number, deleteCount?: number, ...items: T[]): T[] - deriveCollection( - callback: (sourceValue: T) => R, - ): Collection + deriveCollection(callback: (sourceValue: T) => R): Collection deriveCollection( callback: (sourceValue: T, abort: AbortSignal) => Promise, ): Collection @@ -281,9 +279,7 @@ function createList< const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig) const itemEquals = options?.itemEquals ?? DEEP_EQUALITY const itemFactory = (options?.createItem ?? - ((item: T) => createState(item, { equals: itemEquals }))) as ( - value: T, - ) => S + ((item: T) => createState(item, { equals: itemEquals }))) as (value: T) => S // --- Internal helpers --- @@ -334,10 +330,7 @@ function createList< batch(() => { for (const key in changes.change) { const val = changes.change[key] - validateSignalValue( - `${TYPE_LIST} item for key "${key}"`, - val, - ) + validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val) const signal = signals.get(key) if (signal) signal.set(val as T) } @@ -362,8 +355,7 @@ function createList< // --- Initialize --- for (let i = 0; i < value.length; i++) { const val = value[i] - if (val == null) - throw new NullishSignalValueError(`${TYPE_LIST} item ${i}`) + if (val == null) throw new NullishSignalValueError(`${TYPE_LIST} item ${i}`) let key = keys[i] if (!key) { key = generateKey(val) @@ -425,8 +417,7 @@ function createList< // Use cached value if clean, recompute if dirty. untrack prevents // buildValue's child .get() calls from leaking edges into whatever // effect is currently active (which would cause over-broad re-runs). - const prev = - node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value + const prev = node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value const changes = diffArrays( prev, next, @@ -476,8 +467,7 @@ function createList< add(value: T) { const key = generateKey(value) - if (signals.has(key)) - throw new DuplicateKeyError(TYPE_LIST, key, value) + if (signals.has(key)) throw new DuplicateKeyError(TYPE_LIST, key, value) keys.push(key) validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value) signals.set(key, itemFactory(value)) @@ -488,15 +478,12 @@ function createList< }, remove(keyOrIndex: string | number) { - const key = - typeof keyOrIndex === 'number' ? keys[keyOrIndex] : keyOrIndex + const key = typeof keyOrIndex === 'number' ? keys[keyOrIndex] : keyOrIndex if (key === undefined) return const ok = signals.delete(key) if (ok) { const index = - typeof keyOrIndex === 'number' - ? keyOrIndex - : keys.indexOf(key) + typeof keyOrIndex === 'number' ? keyOrIndex : keys.indexOf(key) if (index >= 0) keys.splice(index, 1) node.flags |= FLAG_DIRTY | FLAG_RELINK for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink) @@ -554,14 +541,11 @@ function createList< splice(start: number, deleteCount?: number, ...items: T[]) { const length = keys.length const actualStart = - start < 0 - ? Math.max(0, length + start) - : Math.min(start, length) + start < 0 ? Math.max(0, length + start) : Math.min(start, length) const actualDeleteCount = Math.max( 0, Math.min( - deleteCount ?? - Math.max(0, length - Math.max(0, actualStart)), + deleteCount ?? Math.max(0, length - Math.max(0, actualStart)), length - actualStart, ), ) diff --git a/src/nodes/slot.ts b/src/nodes/slot.ts index 9df9ae5..8709c37 100644 --- a/src/nodes/slot.ts +++ b/src/nodes/slot.ts @@ -147,9 +147,7 @@ function createSlot( } } - const replace = ( - next: Signal | SlotDescriptor, - ): void => { + const replace = (next: Signal | SlotDescriptor): void => { validateSignalValue(TYPE_SLOT, next, isSignalOrDescriptor) delegated = next diff --git a/src/nodes/store.ts b/src/nodes/store.ts index 1b6f590..548a4ea 100644 --- a/src/nodes/store.ts +++ b/src/nodes/store.ts @@ -44,10 +44,7 @@ type BaseStore = { readonly [Symbol.toStringTag]: 'Store' readonly [Symbol.isConcatSpreadable]: false [Symbol.iterator](): IterableIterator< - [ - string, - State | Store | List, - ] + [string, State | Store | List] > keys(): IterableIterator byKey( @@ -99,10 +96,7 @@ function diffRecords(prev: T, next: T): DiffResult { for (const key of nextKeys) { if (key in prev) { if ( - !DEEP_EQUALITY( - prev[key] as unknown & {}, - next[key] as unknown & {}, - ) + !DEEP_EQUALITY(prev[key] as unknown & {}, next[key] as unknown & {}) ) { change[key] = next[key] changed = true @@ -274,11 +268,7 @@ function createStore( for (const [key, signal] of signals) { yield [key, signal] as [ string, - ( - | State - | Store - | List - ), + State | Store | List, ] } }, @@ -289,8 +279,7 @@ function createStore( }, byKey(key: K) { - return signals.get(key) as T[K] extends readonly (infer U extends - {})[] + return signals.get(key) as T[K] extends readonly (infer U extends {})[] ? List : T[K] extends UnknownRecord ? Store @@ -331,8 +320,7 @@ function createStore( // Use cached value if clean, recompute if dirty. untrack prevents // buildValue's child .get() calls from leaking edges into whatever // effect is currently active (which would cause over-broad re-runs). - const prev = - node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value + const prev = node.flags & FLAG_DIRTY ? untrack(buildValue) : node.value const changes = diffRecords(prev, next) if (applyChanges(changes)) { @@ -347,8 +335,7 @@ function createStore( }, add(key: K, value: T[K]) { - if (signals.has(key)) - throw new DuplicateKeyError(TYPE_STORE, key, value) + if (signals.has(key)) throw new DuplicateKeyError(TYPE_STORE, key, value) addSignal(key, value) node.flags |= FLAG_DIRTY | FLAG_RELINK for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink) @@ -390,8 +377,7 @@ function createStore( return Array.from(target.keys()) }, getOwnPropertyDescriptor(target, prop) { - if (prop in target) - return Reflect.getOwnPropertyDescriptor(target, prop) + if (prop in target) return Reflect.getOwnPropertyDescriptor(target, prop) if (typeof prop === 'symbol') return undefined const signal = target.byKey(String(prop) as keyof T & string) return signal diff --git a/src/signal.ts b/src/signal.ts index 0d7a37e..21607e0 100644 --- a/src/signal.ts +++ b/src/signal.ts @@ -89,8 +89,7 @@ function createSignal(value: unknown): unknown { if (value == null) throw new InvalidSignalValueError('createSignal', value) if (isAsyncFunction(value)) return createTask(value as TaskCallback) - if (isFunction(value)) - return createMemo(value as MemoCallback) + if (isFunction(value)) return createMemo(value as MemoCallback) if (Array.isArray(value) && value.every(item => item != null)) return createList(value as (unknown & {})[]) if (isRecord(value)) return createStore(value) diff --git a/test/benchmark.test.ts b/test/benchmark.test.ts index 5a16f39..582d6f5 100644 --- a/test/benchmark.test.ts +++ b/test/benchmark.test.ts @@ -291,9 +291,7 @@ for (const framework of [v18]) { }) test('mux', async () => { - const heads = new Array(100) - .fill(null) - .map(_ => framework.signal(0)) + const heads = new Array(100).fill(null).map(_ => framework.signal(0)) const mux = framework.computed(() => Object.fromEntries(heads.map(h => h.read()).entries()), ) @@ -451,9 +449,7 @@ for (const framework of [v18]) { framework.withBuild(() => { const A = framework.signal(0) const B = framework.signal(0) - const C = framework.computed( - () => (A.read() % 2) + (B.read() % 2), - ) + const C = framework.computed(() => (A.read() % 2) + (B.read() % 2)) const D = framework.computed(() => numbers.map(i => ({ x: i + (A.read() % 2) - (B.read() % 2), @@ -547,12 +543,8 @@ for (const framework of [v18]) { const m: CellxLayer = layer const s = { prop1: framework.computed(() => m.prop2.read()), - prop2: framework.computed( - () => m.prop1.read() - m.prop3.read(), - ), - prop3: framework.computed( - () => m.prop2.read() + m.prop4.read(), - ), + prop2: framework.computed(() => m.prop1.read() - m.prop3.read()), + prop3: framework.computed(() => m.prop2.read() + m.prop4.read()), prop4: framework.computed(() => m.prop3.read()), } diff --git a/test/collection.test.ts b/test/collection.test.ts index 440ad67..7101eb3 100644 --- a/test/collection.test.ts +++ b/test/collection.test.ts @@ -113,9 +113,9 @@ describe('Collection', () => { expect(isCollection(42)).toBe(false) expect(isCollection(null)).toBe(false) expect(isCollection({})).toBe(false) - expect( - isList(createCollection(() => () => {}, { value: [1] })), - ).toBe(false) + expect(isList(createCollection(() => () => {}, { value: [1] }))).toBe( + false, + ) }) }) @@ -175,9 +175,7 @@ describe('Collection', () => { describe('applyChanges', () => { test('should add items', () => { - let apply: - | ((changes: CollectionChanges) => void) - | undefined + let apply: ((changes: CollectionChanges) => void) | undefined const col = createCollection(applyChanges => { apply = applyChanges return () => {} @@ -204,9 +202,7 @@ describe('Collection', () => { test('should change item values', () => { let apply: - | (( - changes: CollectionChanges<{ id: string; val: number }>, - ) => void) + | ((changes: CollectionChanges<{ id: string; val: number }>) => void) | undefined const col = createCollection( applyChanges => { @@ -239,9 +235,7 @@ describe('Collection', () => { test('should remove items', () => { let apply: - | (( - changes: CollectionChanges<{ id: string; v: number }>, - ) => void) + | ((changes: CollectionChanges<{ id: string; v: number }>) => void) | undefined const col = createCollection( applyChanges => { @@ -286,9 +280,7 @@ describe('Collection', () => { test('should handle mixed add/change/remove', () => { let apply: - | (( - changes: CollectionChanges<{ id: string; v: number }>, - ) => void) + | ((changes: CollectionChanges<{ id: string; v: number }>) => void) | undefined const col = createCollection( applyChanges => { @@ -328,9 +320,7 @@ describe('Collection', () => { }) test('should skip when no changes provided', () => { - let apply: - | ((changes: CollectionChanges) => void) - | undefined + let apply: ((changes: CollectionChanges) => void) | undefined const col = createCollection( applyChanges => { apply = applyChanges @@ -358,9 +348,7 @@ describe('Collection', () => { }) test('should trigger effects on structural changes', () => { - let apply: - | ((changes: CollectionChanges) => void) - | undefined + let apply: ((changes: CollectionChanges) => void) | undefined const col = createCollection(applyChanges => { apply = applyChanges return () => {} @@ -386,9 +374,7 @@ describe('Collection', () => { }) test('should batch multiple calls', () => { - let apply: - | ((changes: CollectionChanges) => void) - | undefined + let apply: ((changes: CollectionChanges) => void) | undefined const col = createCollection(applyChanges => { apply = applyChanges return () => {} @@ -622,9 +608,7 @@ describe('Collection', () => { // once during setup (transient leak, mode b). test('applyChanges({ change }) inside an effect does not transiently re-run', () => { type Item = { id: string; v: number } - let apply: - | ((changes: CollectionChanges) => void) - | undefined + let apply: ((changes: CollectionChanges) => void) | undefined const col = createCollection( applyChanges => { apply = applyChanges @@ -1030,9 +1014,6 @@ test('Type Inference for custom createItem', () => { ? true : false type _Test = Expect< - Equal< - typeof byKey, - ReturnType> | undefined - > + Equal> | undefined> > }) diff --git a/test/effect.test.ts b/test/effect.test.ts index 1759f63..f915a45 100644 --- a/test/effect.test.ts +++ b/test/effect.test.ts @@ -536,9 +536,7 @@ describe('createEffect', () => { '[Effect] Callback null is invalid', ) // @ts-expect-error - Testing invalid input - expect(() => createEffect(42)).toThrow( - '[Effect] Callback 42 is invalid', - ) + expect(() => createEffect(42)).toThrow('[Effect] Callback 42 is invalid') }) }) }) @@ -1277,10 +1275,7 @@ describe('match', () => { ok: () => new Promise((_resolve, reject) => { // Reject asynchronously, after disposal. - setTimeout( - () => reject(new Error('late reject')), - 10, - ) + setTimeout(() => reject(new Error('late reject')), 10) }), err: e => { errCount++ diff --git a/test/equality.test.ts b/test/equality.test.ts index bf9543f..ac57b21 100644 --- a/test/equality.test.ts +++ b/test/equality.test.ts @@ -131,9 +131,9 @@ describe('DEEP_EQUALITY', () => { }) test('differ in length', () => { - expect( - DEEP_EQUALITY([1, 2] as number[], [1, 2, 3] as number[]), - ).toBe(false) + expect(DEEP_EQUALITY([1, 2] as number[], [1, 2, 3] as number[])).toBe( + false, + ) }) test('differ in one element', () => { @@ -149,15 +149,15 @@ describe('DEEP_EQUALITY', () => { }) test('array of equal objects', () => { - expect( - DEEP_EQUALITY([{ x: 1 }, { x: 2 }], [{ x: 1 }, { x: 2 }]), - ).toBe(true) + expect(DEEP_EQUALITY([{ x: 1 }, { x: 2 }], [{ x: 1 }, { x: 2 }])).toBe( + true, + ) }) test('array of objects differing in one element', () => { - expect( - DEEP_EQUALITY([{ x: 1 }, { x: 2 }], [{ x: 1 }, { x: 9 }]), - ).toBe(false) + expect(DEEP_EQUALITY([{ x: 1 }, { x: 2 }], [{ x: 1 }, { x: 9 }])).toBe( + false, + ) }) }) diff --git a/test/list.test.ts b/test/list.test.ts index 9741630..b5627a8 100644 --- a/test/list.test.ts +++ b/test/list.test.ts @@ -199,9 +199,7 @@ describe('List', () => { const list = createList([{ id: 'a', val: 1 }], { keyConfig: item => item.id, }) - expect(() => list.add({ id: 'a', val: 2 })).toThrow( - 'already exists', - ) + expect(() => list.add({ id: 'a', val: 2 })).toThrow('already exists') }) test('DuplicateKeyError message includes [List] prefix and key', () => { @@ -603,9 +601,7 @@ describe('List', () => { test('computed signals should react to list changes', () => { const list = createList([1, 2, 3]) - const sum = createMemo(() => - list.get().reduce((acc, n) => acc + n, 0), - ) + const sum = createMemo(() => list.get().reduce((acc, n) => acc + n, 0)) expect(sum.get()).toBe(6) list.add(4) @@ -1141,9 +1137,6 @@ test('Type Inference for custom createItem', () => { ? true : false type _Test = Expect< - Equal< - typeof byKey, - ReturnType> | undefined - > + Equal> | undefined> > }) diff --git a/test/memo.test.ts b/test/memo.test.ts index 05bfb37..54fb34d 100644 --- a/test/memo.test.ts +++ b/test/memo.test.ts @@ -362,13 +362,9 @@ describe('Memo', () => { describe('Input Validation', () => { test('should throw InvalidCallbackError for non-function callback', () => { // @ts-expect-error - Testing invalid input - expect(() => createMemo(null)).toThrow( - '[Memo] Callback null is invalid', - ) + expect(() => createMemo(null)).toThrow('[Memo] Callback null is invalid') // @ts-expect-error - Testing invalid input - expect(() => createMemo(42)).toThrow( - '[Memo] Callback 42 is invalid', - ) + expect(() => createMemo(42)).toThrow('[Memo] Callback 42 is invalid') // @ts-expect-error - Testing invalid input expect(() => createMemo('str')).toThrow( '[Memo] Callback "str" is invalid', diff --git a/test/recipes.test.ts b/test/recipes.test.ts index 30fb136..ebafff1 100644 --- a/test/recipes.test.ts +++ b/test/recipes.test.ts @@ -46,8 +46,7 @@ describe('Recipes', () => { const wizardState = createMemo(() => { const step = currentStep.get() - const canProceed = - step === 1 ? isStep1Valid.get() : isStep2Valid.get() + const canProceed = step === 1 ? isStep1Valid.get() : isStep2Valid.get() const isComplete = step === totalSteps && canProceed return { @@ -59,10 +58,7 @@ describe('Recipes', () => { }) function nextStep() { - if ( - wizardState.get().canProceed && - currentStep.get() < totalSteps - ) { + if (wizardState.get().canProceed && currentStep.get() < totalSteps) { currentStep.update(s => s + 1) } } @@ -163,9 +159,7 @@ describe('Recipes', () => { applyComplexServerSync({ removed: ['w2'], modified: [{ id: 'w1', newMembers: ['Alice', 'Bob', 'Dave'] }], - added: [ - { id: 'w3', name: 'Marketing', members: ['Eve'], active: true }, - ], + added: [{ id: 'w3', name: 'Marketing', members: ['Eve'], active: true }], }) expect(totalCount.get()).toBe(4) diff --git a/test/regression-performance.test.ts b/test/regression-performance.test.ts index 3ccb043..040167d 100644 --- a/test/regression-performance.test.ts +++ b/test/regression-performance.test.ts @@ -109,9 +109,7 @@ describe('Performance — primitive nodes', () => { const branches = Array.from({ length: 5 }, () => f.createMemo(() => head.get() + 1), ) - const sum = f.createMemo(() => - branches.reduce((a, b) => a + b.get(), 0), - ) + const sum = f.createMemo(() => branches.reduce((a, b) => a + b.get(), 0)) f.createEffect(() => { sum.get() }) @@ -233,9 +231,7 @@ describe('Performance — composite nodes', () => { test('derived collection item update (2000 iterations)', () => { const setup = (f: typeof current) => () => { - const list = f.createList( - Array.from({ length: 5 }, (_, i) => i), - ) + const list = f.createList(Array.from({ length: 5 }, (_, i) => i)) const derived = list.deriveCollection((v: number) => v * 2) // biome-ignore lint/style/noNonNullAssertion: list is pre-populated const firstKey = list.keyAt(0)! diff --git a/test/slot.test.ts b/test/slot.test.ts index c57f665..3b074e3 100644 --- a/test/slot.test.ts +++ b/test/slot.test.ts @@ -254,20 +254,20 @@ describe('Slot', () => { get: () => state.get() * 2, set: (val: number) => state.set(val / 2), }) - + expect(slot.get()).toBe(2) - + let runs = 0 createEffect(() => { slot.get() runs++ }) expect(runs).toBe(1) - + state.set(5) expect(runs).toBe(2) expect(slot.get()).toBe(10) - + slot.set(100) expect(state.get()).toBe(50) expect(runs).toBe(3) @@ -326,7 +326,7 @@ describe('Slot', () => { const slot = createSlot(state, { // Only compare by x — changes to y should not propagate. // First call receives undefined prev, so guard against that. - equals: (a, b) => b == null ? false : a.x === b.x, + equals: (a, b) => (b == null ? false : a.x === b.x), }) let runs = 0 let seenX = 0 diff --git a/test/state.test.ts b/test/state.test.ts index 9529733..d23d866 100644 --- a/test/state.test.ts +++ b/test/state.test.ts @@ -97,10 +97,7 @@ describe('State', () => { describe('options.equals', () => { test('should use custom equality function to skip updates', () => { - const state = createState( - { x: 1 }, - { equals: (a, b) => a.x === b.x }, - ) + const state = createState({ x: 1 }, { equals: (a, b) => a.x === b.x }) let effectCount = 0 createEffect(() => { state.get() @@ -142,9 +139,7 @@ describe('State', () => { const state = createState(1, { guard: (v): v is number => typeof v === 'number' && v > 0, }) - expect(() => state.set(0)).toThrow( - '[State] Signal value 0 is invalid', - ) + expect(() => state.set(0)).toThrow('[State] Signal value 0 is invalid') expect(state.get()).toBe(1) // unchanged }) diff --git a/test/store.test.ts b/test/store.test.ts index 0f6fc72..7bed099 100644 --- a/test/store.test.ts +++ b/test/store.test.ts @@ -385,13 +385,11 @@ describe('Store', () => { test('should maintain property key ordering', () => { const config = createStore({ alpha: 1, beta: 2, gamma: 3 }) const entries = [...config] - expect(entries.map(([key, signal]) => [key, signal.get()])).toEqual( - [ - ['alpha', 1], - ['beta', 2], - ['gamma', 3], - ], - ) + expect(entries.map(([key, signal]) => [key, signal.get()])).toEqual([ + ['alpha', 1], + ['beta', 2], + ['gamma', 3], + ]) }) }) @@ -750,9 +748,9 @@ describe('Store', () => { const store = createStore<{ name: string; x?: number }>({ name: 'Alice', }) - expect(() => - Object.defineProperty(store, 'x', { value: 1 }), - ).toThrow(InvalidStoreMutationError) + expect(() => Object.defineProperty(store, 'x', { value: 1 })).toThrow( + InvalidStoreMutationError, + ) expect(store.get()).toEqual({ name: 'Alice' }) }) diff --git a/test/task.test.ts b/test/task.test.ts index 915167d..fc9ee95 100644 --- a/test/task.test.ts +++ b/test/task.test.ts @@ -462,13 +462,9 @@ describe('Task', () => { test('should throw InvalidCallbackError for non-function callback', () => { // @ts-expect-error - Testing invalid input - expect(() => createTask(null)).toThrow( - '[Task] Callback null is invalid', - ) + expect(() => createTask(null)).toThrow('[Task] Callback null is invalid') // @ts-expect-error - Testing invalid input - expect(() => createTask(42)).toThrow( - '[Task] Callback 42 is invalid', - ) + expect(() => createTask(42)).toThrow('[Task] Callback 42 is invalid') }) test('should throw NullishSignalValueError for null initial value', () => { @@ -519,9 +515,7 @@ describe('Task', () => { } expect(caught).toBeInstanceOf(Error) expect((caught as Error).message).toBe('sync boom') - expect((caught as Error).message).not.toContain( - 'Circular dependency', - ) + expect((caught as Error).message).not.toContain('Circular dependency') }) }) diff --git a/test/unown.test.ts b/test/unown.test.ts index 6c408f6..6315ec7 100644 --- a/test/unown.test.ts +++ b/test/unown.test.ts @@ -1,19 +1,18 @@ import { describe, expect, test } from 'bun:test' -import { - createEffect, - createScope, - createState, - unown, -} from '../index.ts' +import { createEffect, createScope, createState, unown } from '../index.ts' describe('createScope with root option', () => { - test('root scope is not registered on enclosing scope', () => { let rootCleanupRan = false const outerDispose = createScope(() => { - createScope(() => { - return () => { rootCleanupRan = true } - }, { root: true }) + createScope( + () => { + return () => { + rootCleanupRan = true + } + }, + { root: true }, + ) }) outerDispose() expect(rootCleanupRan).toBe(false) @@ -26,9 +25,14 @@ describe('createScope with root option', () => { const outerDispose = createScope(() => { createEffect((): undefined => { trigger.get() - createScope(() => { - return () => { rootCleanupRuns++ } - }, { root: true }) + createScope( + () => { + return () => { + rootCleanupRuns++ + } + }, + { root: true }, + ) }) }) @@ -47,12 +51,17 @@ describe('createScope with root option', () => { let componentCleanupRuns = 0 const connectComponent = () => - createScope(() => { - createEffect((): undefined => { - componentEffectRuns++ - }) - return () => { componentCleanupRuns++ } - }, { root: true }) + createScope( + () => { + createEffect((): undefined => { + componentEffectRuns++ + }) + return () => { + componentCleanupRuns++ + } + }, + { root: true }, + ) const outerDispose = createScope(() => { createEffect((): undefined => { @@ -73,9 +82,14 @@ describe('createScope with root option', () => { test('dispose returned from root scope still works', () => { let cleanupRan = false - const dispose = createScope(() => { - return () => { cleanupRan = true } - }, { root: true }) + const dispose = createScope( + () => { + return () => { + cleanupRan = true + } + }, + { root: true }, + ) expect(cleanupRan).toBe(false) dispose() expect(cleanupRan).toBe(true) @@ -85,12 +99,15 @@ describe('createScope with root option', () => { const source = createState('a') let effectRuns = 0 - const dispose = createScope(() => { - createEffect((): undefined => { - source.get() - effectRuns++ - }) - }, { root: true }) + const dispose = createScope( + () => { + createEffect((): undefined => { + source.get() + effectRuns++ + }) + }, + { root: true }, + ) expect(effectRuns).toBe(1) source.set('b') @@ -105,22 +122,27 @@ describe('createScope with root option', () => { let postCleanupRan = false const outerDispose = createScope(() => { try { - createScope(() => { throw new Error('boom') }, { root: true }) + createScope( + () => { + throw new Error('boom') + }, + { root: true }, + ) } catch { // swallow } createScope(() => { - return () => { postCleanupRan = true } + return () => { + postCleanupRan = true + } }) }) outerDispose() expect(postCleanupRan).toBe(true) }) - }) describe('unown', () => { - test('should return the value of the callback', () => { const result = unown(() => 42) expect(result).toBe(42) @@ -128,7 +150,9 @@ describe('unown', () => { test('should run the callback immediately and synchronously', () => { let ran = false - unown(() => { ran = true }) + unown(() => { + ran = true + }) expect(ran).toBe(true) }) @@ -137,7 +161,9 @@ describe('unown', () => { const outerDispose = createScope(() => { unown(() => { createScope(() => { - return () => { innerCleanupRan = true } + return () => { + innerCleanupRan = true + } }) }) }) @@ -154,7 +180,9 @@ describe('unown', () => { trigger.get() unown(() => { createScope(() => { - return () => { innerCleanupRuns++ } + return () => { + innerCleanupRuns++ + } }) }) }) @@ -174,14 +202,17 @@ describe('unown', () => { let componentEffectRuns = 0 let componentCleanupRuns = 0 - const connectComponent = () => unown(() => - createScope(() => { - createEffect((): undefined => { - componentEffectRuns++ - }) - return () => { componentCleanupRuns++ } - }) - ) + const connectComponent = () => + unown(() => + createScope(() => { + createEffect((): undefined => { + componentEffectRuns++ + }) + return () => { + componentCleanupRuns++ + } + }), + ) const outerDispose = createScope(() => { createEffect((): undefined => { @@ -210,7 +241,7 @@ describe('unown', () => { source.get() effectRuns++ }) - }) + }), ) expect(effectRuns).toBe(1) @@ -226,8 +257,10 @@ describe('unown', () => { let cleanupRan = false const dispose = unown(() => createScope(() => { - return () => { cleanupRan = true } - }) + return () => { + cleanupRan = true + } + }), ) expect(cleanupRan).toBe(false) dispose() @@ -240,7 +273,9 @@ describe('unown', () => { unown(() => { unown(() => { createScope(() => { - return () => { innerCleanupRan = true } + return () => { + innerCleanupRan = true + } }) }) }) @@ -252,9 +287,13 @@ describe('unown', () => { test('restores the active owner after the callback completes', () => { let postCleanupRan = false const outerDispose = createScope(() => { - unown(() => { /* some unowned work */ }) + unown(() => { + /* some unowned work */ + }) createScope(() => { - return () => { postCleanupRan = true } + return () => { + postCleanupRan = true + } }) }) outerDispose() @@ -265,12 +304,16 @@ describe('unown', () => { let postCleanupRan = false const outerDispose = createScope(() => { try { - unown(() => { throw new Error('boom') }) + unown(() => { + throw new Error('boom') + }) } catch { // swallow } createScope(() => { - return () => { postCleanupRan = true } + return () => { + postCleanupRan = true + } }) }) outerDispose() @@ -288,5 +331,4 @@ describe('unown', () => { expect(ran).toBe(true) expect(typeof dispose).toBe('function') }) - }) diff --git a/test/util/core-entry.ts b/test/util/core-entry.ts index a5c032d..9b93d87 100644 --- a/test/util/core-entry.ts +++ b/test/util/core-entry.ts @@ -4,7 +4,12 @@ * path a real consumer uses — and actually wires the four core signal types * together so the bundler can't eliminate code a real usage would retain. */ -import { createEffect, createMemo, createState, createTask } from '../../index.ts' +import { + createEffect, + createMemo, + createState, + createTask, +} from '../../index.ts' const count = createState(0) const doubled = createMemo(() => count.get() * 2) diff --git a/test/util/dependency-graph.ts b/test/util/dependency-graph.ts index 4c42a0a..93068a7 100644 --- a/test/util/dependency-graph.ts +++ b/test/util/dependency-graph.ts @@ -24,9 +24,7 @@ export function makeGraph( const { width, totalLayers, staticFraction, nSources } = config return framework.withBuild(() => { - const sources = new Array(width) - .fill(0) - .map((_, i) => framework.signal(i)) + const sources = new Array(width).fill(0).map((_, i) => framework.signal(i)) const rows = makeDependentRows( sources, totalLayers - 1, diff --git a/tsconfig.build.json b/tsconfig.build.json index 55b4f8b..7f0ab99 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,8 +4,8 @@ "noEmit": false, "declaration": true, "declarationDir": "./types", - "emitDeclarationOnly": true, + "emitDeclarationOnly": true }, "include": ["./index.ts", "./src/**/*.ts"], - "exclude": ["node_modules", "types", "test", "archive"], + "exclude": ["node_modules", "types", "test", "archive"] } diff --git a/tsconfig.json b/tsconfig.json index 03f3826..81a6c8d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,8 +37,8 @@ /* Performance */ "incremental": true, - "tsBuildInfoFile": "./.tsbuildinfo", + "tsBuildInfoFile": "./.tsbuildinfo" }, "include": ["./**/*.ts"], - "exclude": ["node_modules", "types", "index.js"], + "exclude": ["node_modules", "types", "index.js"] }