From 8d6abe0c17878f06e89d3c5d3581cd924ec34272 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Mon, 1 Sep 2025 14:31:21 +0200 Subject: [PATCH 1/9] draft: zero 0.23 --- package.json | 2 ++ pnpm-workspace.yaml | 3 ++ src/create-use-zero.ts | 11 +++++++ src/create-zero.ts | 24 +++++++++++++++ src/query.test.ts | 66 ++++++++++++++++++++++++++++++++++++++---- src/query.ts | 9 +++++- tsconfig.json | 3 +- vitest.config.ts | 1 + 8 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 src/create-use-zero.ts create mode 100644 src/create-zero.ts diff --git a/package.json b/package.json index f306cae..8dee34a 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,12 @@ "devDependencies": { "@antfu/eslint-config": "latest", "@rocicorp/resolver": "1.0.2", + "@testing-library/vue": "^8.1.0", "@vitest/coverage-v8": "latest", "bumpp": "latest", "changelogithub": "13.16.0", "eslint": "latest", + "happy-dom": "^18.0.1", "installed-check": "latest", "knip": "latest", "lint-staged": "latest", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1dc752c..8ed1558 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,9 +1,12 @@ packages: - playground + ignoredBuiltDependencies: - esbuild - protobufjs - unrs-resolver + onlyBuiltDependencies: - '@rocicorp/zero-sqlite3' + - oxc-resolver - simple-git-hooks diff --git a/src/create-use-zero.ts b/src/create-use-zero.ts new file mode 100644 index 0000000..dabb470 --- /dev/null +++ b/src/create-use-zero.ts @@ -0,0 +1,11 @@ +import type { CustomMutatorDefs, Schema, Zero } from '@rocicorp/zero' +import type { ShallowRef } from 'vue' +import { inject } from 'vue' +import { zeroSymbol } from './create-zero' + +export function createUseZero< + S extends Schema, + MD extends CustomMutatorDefs | undefined = undefined, +>() { + return () => inject(zeroSymbol) as ShallowRef> +} diff --git a/src/create-zero.ts b/src/create-zero.ts new file mode 100644 index 0000000..3bbc649 --- /dev/null +++ b/src/create-zero.ts @@ -0,0 +1,24 @@ +import type { Schema, ZeroOptions } from '@rocicorp/zero' +import type { App, InjectionKey, MaybeRefOrGetter, ShallowRef } from 'vue' +import { Zero } from '@rocicorp/zero' +import { shallowRef, toValue, watch } from 'vue' + +export const zeroSymbol = Symbol('zero') as InjectionKey>> + +export function createZero(opts: MaybeRefOrGetter>) { + const z = shallowRef() as ShallowRef> + + watch(() => toValue(opts), async (opts) => { + // await z.value?.close() + z.value = new Zero(opts) + }, { deep: true }) + + return { + install: (app: App) => { + z.value = new Zero(toValue(opts)) + + // @ts-expect-error - TODO: type properly + app.provide(zeroSymbol, z) + }, + } +} diff --git a/src/query.test.ts b/src/query.test.ts index 1f7a747..f82aad5 100644 --- a/src/query.test.ts +++ b/src/query.test.ts @@ -1,10 +1,26 @@ import type { TTL } from '@rocicorp/zero' -import { createSchema, number, string, table, Zero } from '@rocicorp/zero' +import { createBuilder, createSchema, number, string, syncedQuery, table, Zero } from '@rocicorp/zero' import { describe, expect, it, vi } from 'vitest' -import { ref, watchEffect } from 'vue' +import { createApp, nextTick, onMounted, ref, shallowRef, watchEffect } from 'vue' +import { createUseZero } from './create-use-zero' +import { createZero } from './create-zero' import { useQuery } from './query' import { VueView, vueViewFactory } from './view' +export function withSetup(composable: () => T) { + const result = shallowRef() + const app = createApp({ + setup() { + onMounted(() => { + result.value = composable() + }) + + return () => {} + }, + }) + return [result, app] as const +} + async function setupTestEnvironment() { const schema = createSchema({ tables: [ @@ -16,22 +32,41 @@ async function setupTestEnvironment() { .primaryKey('a'), ], }) + const builder = createBuilder(schema) - const z = new Zero({ + const useZero = createUseZero() + const [zero, app] = withSetup(useZero) + app.use(createZero({ userID: 'asdf', server: null, schema, // This is often easier to develop with if you're frequently changing // the schema. Switch to 'idb' for local-persistence. kvStore: 'mem', - }) + })) + app.mount(document.createElement('div')) + + const z = zero.value!.value await z.mutate.table.insert({ a: 1, b: 'a' }) await z.mutate.table.insert({ a: 2, b: 'b' }) + const byIdQuery = syncedQuery( + 'byId', + ([id]) => { + if (typeof id !== 'number') { + throw new TypeError('id must be a number') + } + return [id] as const + }, + (id: number) => { + return builder.table.where('a', id) + }, + ) + const tableQuery = z.query.table - return { z, tableQuery } + return { z, tableQuery, byIdQuery } } describe('useQuery', () => { @@ -112,7 +147,7 @@ describe('useQuery', () => { z.close() }) - it('useQuery with ttl (zero@0.19)', async () => { + it.only('useQuery with ttl (zero@0.19)', async () => { const { z, tableQuery } = await setupTestEnvironment() if ('updateTTL' in tableQuery) { // 0.19 removed updateTTL from the query @@ -258,4 +293,23 @@ describe('useQuery', () => { z.close() }) + + it.skip('useQuery with syncedQuery', async () => { + const { z, byIdQuery } = await setupTestEnvironment() + + const a = ref(1) + const { data: rows, status } = useQuery(() => byIdQuery(a.value)) + + expect(rows.value).toMatchInlineSnapshot(` +[ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, +]`) + expect(status.value).toEqual('unknown') + + z.close() + }) }) diff --git a/src/query.ts b/src/query.ts index eec9100..775808e 100644 --- a/src/query.ts +++ b/src/query.ts @@ -7,11 +7,13 @@ import type { VueView } from './view' import { computed, getCurrentInstance, + inject, onUnmounted, shallowRef, toValue, watch, } from 'vue' +import { zeroSymbol } from './create-zero' import { vueViewFactory } from './view' const DEFAULT_TTL_MS = 1_000 * 60 * 5 @@ -38,11 +40,16 @@ export function useQuery< }) const view = shallowRef> | null>(null) + const z = inject(zeroSymbol) + if (!z) { + throw new Error('Zero not found. Did you forget to call app.use(createZero())?') + } + watch( () => toValue(query), (q) => { view.value?.destroy() - view.value = q.materialize(vueViewFactory, ttl.value) + view.value = z.value.materialize(q, vueViewFactory, { ttl: ttl.value }) }, { immediate: true }, ) diff --git a/tsconfig.json b/tsconfig.json index 812e9a9..a2b2399 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "target": "es2022", "lib": [ - "es2022" + "es2022", + "dom" ], "moduleDetection": "force", "module": "preserve", diff --git a/vitest.config.ts b/vitest.config.ts index ed149b9..1e0f4a7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ }, }, test: { + environment: 'happy-dom', coverage: { include: ['src'], reporter: ['text', 'json', 'html'], From 354e5ba6a115c11724f670512892133d63d77148 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Tue, 9 Sep 2025 13:15:31 +0200 Subject: [PATCH 2/9] fix --- README.md | 20 +++ src/create-zero.test.ts | 109 +++++++++++++ src/create-zero.ts | 24 ++- src/index.ts | 2 + src/query.test.ts | 343 +++++++++++++++++++++++----------------- src/query.ts | 17 +- 6 files changed, 359 insertions(+), 156 deletions(-) create mode 100644 src/create-zero.test.ts diff --git a/README.md b/README.md index 4bcf86f..e306aca 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,17 @@ npm install zero-vue pnpm install zero-vue ``` +Register plugin: + +```js +import { createApp } from 'vue' +import { createZero } from 'zero-vue' + +const app = createApp(App) +app.use(createZero()) +``` + +Use `useQuery`: ```js import { Zero } from '@rocicorp/zero' import { useQuery } from 'zero-vue' @@ -34,6 +45,15 @@ const z = new Zero({ const { data: users } = useQuery(z.query.user) ``` +Optional: typing `useZero`: +```ts +import { createUseZero } from 'zero-vue'; +import type { Schema } from './schema.ts'; +import type { Mutators } from './mutators.ts'; +export const useZero = createUseZero(); +const z = useZero(); // z is typed with your own schema and mutators +``` + > [!TIP] > See [the playground](./playground) for a full working example based on [rocicorp/hello-zero](https://github.com/rocicorp/hello-zero), or check out [danielroe/hello-zero-nuxt](https://github.com/danielroe/hello-zero-nuxt) to see how to set things up with [Nuxt](https://nuxt.com/). diff --git a/src/create-zero.test.ts b/src/create-zero.test.ts new file mode 100644 index 0000000..4300b06 --- /dev/null +++ b/src/create-zero.test.ts @@ -0,0 +1,109 @@ +import { createSchema, number, string, table, Zero } from '@rocicorp/zero' +import { assert, describe, expect, it } from 'vitest' +import { computed, createApp, inject, ref } from 'vue' +import { createZero, zeroSymbol } from './create-zero' + +const testSchema = createSchema({ + tables: [ + table('test') + .columns({ + id: number(), + name: string(), + }) + .primaryKey('id'), + ], +}) + +describe('createZero', () => { + it('installs and provides zero instance to Vue app', () => { + const app = createApp({}) + app.use(createZero({ + userID: 'test-user', + server: null, + schema: testSchema, + kvStore: 'mem' as const, + })) + + app.runWithContext(() => { + const zero = inject(zeroSymbol) + assert(zero?.value) + expect(zero?.value.userID).toEqual('test-user') + }) + }) + + it('accepts Zero instance instead of options', () => { + const app = createApp({}) + const zero = new Zero({ + userID: 'test-user', + server: null, + schema: testSchema, + kvStore: 'mem' as const, + }) + app.use(createZero({ zero })) + + app.runWithContext(() => { + const injectedZero = inject(zeroSymbol) + assert(injectedZero?.value) + expect(injectedZero.value).toEqual(zero) + }) + }) + + it('updates when options change', async () => { + const app = createApp({}) + const userID = ref('test-user') + const zeroOptions = computed(() => ({ + userID: userID.value, + server: null, + schema: testSchema, + kvStore: 'mem' as const, + })) + + app.use(createZero(zeroOptions)) + + await app.runWithContext(async () => { + const injectedZero = inject(zeroSymbol) + assert(injectedZero?.value) + + expect(injectedZero.value.userID).toEqual('test-user') + + const oldZero = injectedZero.value + + userID.value = 'test-user-2' + await 1 + + expect(injectedZero.value.userID).toEqual('test-user-2') + expect(injectedZero.value.closed).toBe(false) + expect(oldZero.closed).toBe(true) + }) + }) + + it('updates when Zero instance changes', async () => { + const app = createApp({}) + const userID = ref('test-user') + + const zero = computed(() => ({ zero: new Zero({ + userID: userID.value, + server: null, + schema: testSchema, + kvStore: 'mem' as const, + }) })) + + app.use(createZero(zero)) + + await app.runWithContext(async () => { + const injectedZero = inject(zeroSymbol) + assert(injectedZero?.value) + + expect(injectedZero.value.userID).toEqual('test-user') + + const oldZero = injectedZero.value + + userID.value = 'test-user-2' + await 1 + + expect(injectedZero.value.userID).toEqual('test-user-2') + expect(injectedZero.value.closed).toBe(false) + expect(oldZero.closed).toBe(true) + }) + }) +}) diff --git a/src/create-zero.ts b/src/create-zero.ts index 3bbc649..b0ae2f7 100644 --- a/src/create-zero.ts +++ b/src/create-zero.ts @@ -1,22 +1,30 @@ -import type { Schema, ZeroOptions } from '@rocicorp/zero' +import type { CustomMutatorDefs, Schema, ZeroOptions } from '@rocicorp/zero' import type { App, InjectionKey, MaybeRefOrGetter, ShallowRef } from 'vue' import { Zero } from '@rocicorp/zero' import { shallowRef, toValue, watch } from 'vue' export const zeroSymbol = Symbol('zero') as InjectionKey>> -export function createZero(opts: MaybeRefOrGetter>) { - const z = shallowRef() as ShallowRef> +const oldZeroCleanups = new Set() - watch(() => toValue(opts), async (opts) => { - // await z.value?.close() - z.value = new Zero(opts) +export function createZero(optsOrZero: MaybeRefOrGetter | { zero: Zero }>) { + const z = shallowRef() as ShallowRef> + + const opts = toValue(optsOrZero) + z.value = 'zero' in opts ? opts.zero : new Zero(opts) + + watch(() => toValue(optsOrZero), (opts) => { + const cleanupZeroPromise = z.value.close() + oldZeroCleanups.add(cleanupZeroPromise) + cleanupZeroPromise.finally(() => { + oldZeroCleanups.delete(cleanupZeroPromise) + }) + + z.value = 'zero' in opts ? opts.zero : new Zero(opts) }, { deep: true }) return { install: (app: App) => { - z.value = new Zero(toValue(opts)) - // @ts-expect-error - TODO: type properly app.provide(zeroSymbol, z) }, diff --git a/src/index.ts b/src/index.ts index 4ae50f5..004dcde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,3 @@ +export { createUseZero } from './create-use-zero' +export { createZero } from './create-zero' export { useQuery, type UseQueryOptions } from './query' diff --git a/src/query.test.ts b/src/query.test.ts index f82aad5..eb1d15a 100644 --- a/src/query.test.ts +++ b/src/query.test.ts @@ -1,7 +1,8 @@ import type { TTL } from '@rocicorp/zero' -import { createBuilder, createSchema, number, string, syncedQuery, table, Zero } from '@rocicorp/zero' +import type { MockInstance } from 'vitest' +import { createBuilder, createSchema, number, string, syncedQuery, table } from '@rocicorp/zero' import { describe, expect, it, vi } from 'vitest' -import { createApp, nextTick, onMounted, ref, shallowRef, watchEffect } from 'vue' +import { computed, createApp, nextTick, onMounted, ref, shallowRef, watchEffect } from 'vue' import { createUseZero } from './create-use-zero' import { createZero } from './create-zero' import { useQuery } from './query' @@ -32,49 +33,50 @@ async function setupTestEnvironment() { .primaryKey('a'), ], }) - const builder = createBuilder(schema) const useZero = createUseZero() const [zero, app] = withSetup(useZero) - app.use(createZero({ - userID: 'asdf', + const userID = ref('asdf') + app.use(createZero(() => ({ + userID: userID.value, server: null, schema, - // This is often easier to develop with if you're frequently changing - // the schema. Switch to 'idb' for local-persistence. kvStore: 'mem', - })) + }))) app.mount(document.createElement('div')) - const z = zero.value!.value - - await z.mutate.table.insert({ a: 1, b: 'a' }) - await z.mutate.table.insert({ a: 2, b: 'b' }) - - const byIdQuery = syncedQuery( - 'byId', - ([id]) => { - if (typeof id !== 'number') { - throw new TypeError('id must be a number') - } - return [id] as const - }, - (id: number) => { - return builder.table.where('a', id) - }, - ) + const z = computed(() => zero.value!.value) - const tableQuery = z.query.table + await z!.value.mutate.table.insert({ a: 1, b: 'a' }) + await z!.value.mutate.table.insert({ a: 2, b: 'b' }) - return { z, tableQuery, byIdQuery } + const builder = createBuilder(schema) + const byIdQuery = syncedQuery + ? syncedQuery( + 'byId', + ([id]) => { + if (typeof id !== 'number') { + throw new TypeError('id must be a number') + } + return [id] as const + }, + (id: number) => { + return builder.table.where('a', id) + }, + ) + : undefined + + const tableQuery = z!.value.query.table + + return { z, tableQuery, byIdQuery, app, userID } } describe('useQuery', () => { it('useQuery', async () => { - const { z, tableQuery } = await setupTestEnvironment() - - const { data: rows, status } = useQuery(() => tableQuery) - expect(rows.value).toMatchInlineSnapshot(`[ + const { z, tableQuery, app } = await setupTestEnvironment() + await app.runWithContext(async () => { + const { data: rows, status } = useQuery(() => tableQuery) + expect(rows.value).toMatchInlineSnapshot(`[ { "a": 1, "b": "a", @@ -86,12 +88,12 @@ describe('useQuery', () => { Symbol(rc): 1, }, ]`) - expect(status.value).toEqual('unknown') + expect(status.value).toEqual('unknown') - await z.mutate.table.insert({ a: 3, b: 'c' }) - await 1 + await z.value.mutate.table.insert({ a: 3, b: 'c' }) + await 1 - expect(rows.value).toMatchInlineSnapshot(`[ + expect(rows.value).toMatchInlineSnapshot(`[ { "a": 1, "b": "a", @@ -109,103 +111,129 @@ describe('useQuery', () => { }, ]`) - // TODO: this is not working at the moment, possibly because we don't have a server connection in test - // expect(resultType.value).toEqual("complete"); + // TODO: this is not working at the moment, possibly because we don't have a server connection in test + // expect(resultType.value).toEqual("complete"); - z.close() + z.value.close() + }) }) it('useQuery with ttl (zero@0.18)', async () => { - const { z, tableQuery } = await setupTestEnvironment() + const { z, tableQuery, app } = await setupTestEnvironment() if (!('updateTTL' in tableQuery)) { // 0.19 removed updateTTL from the query return } - const ttl = ref('1m') - const materializeSpy = vi.spyOn(tableQuery, 'materialize') - // @ts-expect-error missing from v0.19+ - const updateTTLSpy = vi.spyOn(tableQuery, 'updateTTL') - const queryGetter = vi.fn(() => tableQuery) + await app.runWithContext(async () => { + const ttl = ref('1m') + + const materializeSpy = vi.spyOn(tableQuery, 'materialize') + // @ts-expect-error missing from v0.19+ + const updateTTLSpy = vi.spyOn(tableQuery, 'updateTTL') + const queryGetter = vi.fn(() => tableQuery) - useQuery(queryGetter, () => ({ ttl: ttl.value })) + useQuery(queryGetter, () => ({ ttl: ttl.value })) - expect(queryGetter).toHaveBeenCalledTimes(1) - expect(updateTTLSpy).toHaveBeenCalledTimes(0) - expect(materializeSpy).toHaveBeenCalledExactlyOnceWith( - vueViewFactory, - '1m', - ) - materializeSpy.mockClear() + expect(queryGetter).toHaveBeenCalledTimes(1) + expect(updateTTLSpy).toHaveBeenCalledTimes(0) + expect(materializeSpy).toHaveBeenCalledExactlyOnceWith( + vueViewFactory, + '1m', + ) + materializeSpy.mockClear() - ttl.value = '10m' - await 1 + ttl.value = '10m' + await 1 - expect(materializeSpy).toHaveBeenCalledTimes(0) - expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m') + expect(materializeSpy).toHaveBeenCalledTimes(0) + expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m') - z.close() + z.value.close() + }) }) - it.only('useQuery with ttl (zero@0.19)', async () => { - const { z, tableQuery } = await setupTestEnvironment() + it('useQuery with ttl (zero@0.19)', async () => { + const { z, tableQuery, app } = await setupTestEnvironment() if ('updateTTL' in tableQuery) { // 0.19 removed updateTTL from the query return } - const ttl = ref('1m') + await app.runWithContext(async () => { + const ttl = ref('1m') + + let materializeSpy: MockInstance + // @ts-expect-error only present in v0.23+ + if (z.value.materialize) { + materializeSpy = vi.spyOn(z.value, 'materialize') + } + else { + materializeSpy = vi.spyOn(tableQuery, 'materialize') + } + + const queryGetter = vi.fn(() => tableQuery) - const materializeSpy = vi.spyOn(tableQuery, 'materialize') + useQuery(queryGetter, () => ({ ttl: ttl.value })) + expect(queryGetter).toHaveBeenCalledTimes(1) - const queryGetter = vi.fn(() => tableQuery) + expect(materializeSpy).toHaveLastReturnedWith(expect.any(VueView)) + // @ts-expect-error only present in v0.23+ + if (z.value.materialize) { + expect(materializeSpy).toHaveBeenCalledExactlyOnceWith( + tableQuery, + vueViewFactory, + { ttl: '1m' }, + ) + } + else { + expect(materializeSpy).toHaveBeenCalledExactlyOnceWith( + vueViewFactory, + '1m', + ) + } - useQuery(queryGetter, () => ({ ttl: ttl.value })) - expect(queryGetter).toHaveBeenCalledTimes(1) - expect(materializeSpy).toHaveBeenCalledExactlyOnceWith( - vueViewFactory, - '1m', - ) - expect(materializeSpy).toHaveLastReturnedWith(expect.any(VueView)) - const view: VueView = materializeSpy.mock.results[0]!.value - const updateTTLSpy = vi.spyOn(view, 'updateTTL') + const view: VueView = materializeSpy.mock.results[0]!.value + const updateTTLSpy = vi.spyOn(view, 'updateTTL') - materializeSpy.mockClear() + materializeSpy.mockClear() - ttl.value = '10m' - await 1 + ttl.value = '10m' + await 1 - expect(materializeSpy).toHaveBeenCalledTimes(0) - expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m') + expect(materializeSpy).toHaveBeenCalledTimes(0) + expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m') - z.close() + z.value.close() + }) }) it('useQuery deps change', async () => { - const { z, tableQuery } = await setupTestEnvironment() + const { z, tableQuery, app } = await setupTestEnvironment() - const a = ref(1) + await app.runWithContext(async () => { + const a = ref(1) - const { data: rows, status } = useQuery(() => - tableQuery.where('a', a.value), - ) + const { data: rows, status } = useQuery(() => + tableQuery.where('a', a.value), + ) - const rowLog: unknown[] = [] - const resultDetailsLog: unknown[] = [] - const resetLogs = () => { - rowLog.length = 0 - resultDetailsLog.length = 0 - } + const rowLog: unknown[] = [] + const resultDetailsLog: unknown[] = [] + const resetLogs = () => { + rowLog.length = 0 + resultDetailsLog.length = 0 + } - watchEffect(() => { - rowLog.push(rows.value) - }) + watchEffect(() => { + rowLog.push(rows.value) + }) - watchEffect(() => { - resultDetailsLog.push(status.value) - }) + watchEffect(() => { + resultDetailsLog.push(status.value) + }) - expect(rowLog).toMatchInlineSnapshot(`[ + expect(rowLog).toMatchInlineSnapshot(`[ [ { "a": 1, @@ -214,17 +242,17 @@ describe('useQuery', () => { }, ], ]`) - // expect(resultDetailsLog).toEqual(["unknown"]); - resetLogs() + // expect(resultDetailsLog).toEqual(["unknown"]); + resetLogs() - expect(rowLog).toEqual([]) - // expect(resultDetailsLog).toEqual(["complete"]); - resetLogs() + expect(rowLog).toEqual([]) + // expect(resultDetailsLog).toEqual(["complete"]); + resetLogs() - a.value = 2 - await 1 + a.value = 2 + await 1 - expect(rowLog).toMatchInlineSnapshot(`[ + expect(rowLog).toMatchInlineSnapshot(`[ [ { "a": 2, @@ -233,83 +261,112 @@ describe('useQuery', () => { }, ], ]`) - // expect(resultDetailsLog).toEqual(["unknown"]); - resetLogs() + // expect(resultDetailsLog).toEqual(["unknown"]); + resetLogs() - expect(rowLog).toEqual([]) - // expect(resultDetailsLog).toEqual(["complete"]); + expect(rowLog).toEqual([]) + // expect(resultDetailsLog).toEqual(["complete"]); - z.close() + z.value.close() + }) }) it('useQuery deps change watchEffect', async () => { - const { z, tableQuery } = await setupTestEnvironment() - const a = ref(1) - const { data: rows } = useQuery(() => tableQuery.where('a', a.value)) - - let run = 0 - - await new Promise((resolve) => { - watchEffect(() => { - if (run === 0) { - expect(rows.value).toMatchInlineSnapshot( - `[ + const { z, tableQuery, app } = await setupTestEnvironment() + await app.runWithContext(async () => { + const a = ref(1) + const { data: rows } = useQuery(() => tableQuery.where('a', a.value)) + + let run = 0 + + await new Promise((resolve) => { + watchEffect(() => { + if (run === 0) { + expect(rows.value).toMatchInlineSnapshot( + `[ { "a": 1, "b": "a", Symbol(rc): 1, }, ]`, - ) - z.mutate.table.update({ a: 1, b: 'a2' }) - } - else if (run === 1) { - expect(rows.value).toMatchInlineSnapshot( - `[ + ) + z.value.mutate.table.update({ a: 1, b: 'a2' }) + } + else if (run === 1) { + expect(rows.value).toMatchInlineSnapshot( + `[ { "a": 1, "b": "a2", Symbol(rc): 1, }, ]`, - ) - a.value = 2 - } - else if (run === 2) { - expect(rows.value).toMatchInlineSnapshot( - `[ + ) + a.value = 2 + } + else if (run === 2) { + expect(rows.value).toMatchInlineSnapshot( + `[ { "a": 2, "b": "b", Symbol(rc): 1, }, ]`, - ) - resolve(true) - } - run++ + ) + resolve(true) + } + run++ + }) }) - }) - z.close() + z.value.close() + }) }) - it.skip('useQuery with syncedQuery', async () => { - const { z, byIdQuery } = await setupTestEnvironment() + it('useQuery with syncedQuery', async () => { + const { z, byIdQuery, app } = await setupTestEnvironment() + if (!byIdQuery) { + return + } - const a = ref(1) - const { data: rows, status } = useQuery(() => byIdQuery(a.value)) + app.runWithContext(() => { + const a = ref(1) + const { data: rows, status } = useQuery(() => byIdQuery(a.value)) - expect(rows.value).toMatchInlineSnapshot(` + expect(rows.value).toMatchInlineSnapshot(` [ { "a": 1, "b": "a", Symbol(rc): 1, }, +]`) + expect(status.value).toEqual('unknown') + + z.value.close() + }) + }) + + it('useQuery can be used without plugin (dropped in future versions)', async () => { + const { z, tableQuery } = await setupTestEnvironment() + + const { data: rows, status } = useQuery(() => tableQuery) + expect(rows.value).toMatchInlineSnapshot(`[ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, ]`) expect(status.value).toEqual('unknown') - z.close() + z.value.close() }) }) diff --git a/src/query.ts b/src/query.ts index 775808e..d79f5af 100644 --- a/src/query.ts +++ b/src/query.ts @@ -40,16 +40,23 @@ export function useQuery< }) const view = shallowRef> | null>(null) - const z = inject(zeroSymbol) + const z = zeroSymbol ? inject(zeroSymbol) : null if (!z) { - throw new Error('Zero not found. Did you forget to call app.use(createZero())?') + console.warn('Zero-vue plugin not found, make sure to call app.use(createZero()). This is required in order to use Synced Queries, and not doing this will throw an error in future releases.') } watch( - () => toValue(query), - (q) => { + [() => toValue(query), () => z], + ([q, z]) => { view.value?.destroy() - view.value = z.value.materialize(q, vueViewFactory, { ttl: ttl.value }) + + // Only present in v0.23+ + if (z && z.value.materialize) { + view.value = z.value.materialize(q, vueViewFactory, { ttl: ttl.value }) + return + } + + view.value = q.materialize(vueViewFactory, ttl.value) }, { immediate: true }, ) From c6869cf3dd08cb927982e3d9e4f61639b1afb67f Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Tue, 9 Sep 2025 13:48:03 +0200 Subject: [PATCH 3/9] check injection context --- src/query.test.ts | 69 +++++++++++++++++++++++++++++++---------------- src/query.ts | 8 ++++-- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/src/query.test.ts b/src/query.test.ts index eb1d15a..cddb2ed 100644 --- a/src/query.test.ts +++ b/src/query.test.ts @@ -1,10 +1,11 @@ import type { TTL } from '@rocicorp/zero' import type { MockInstance } from 'vitest' -import { createBuilder, createSchema, number, string, syncedQuery, table } from '@rocicorp/zero' +import type { ShallowRef } from 'vue' +import { createBuilder, createSchema, number, string, syncedQuery, table, Zero } from '@rocicorp/zero' import { describe, expect, it, vi } from 'vitest' -import { computed, createApp, nextTick, onMounted, ref, shallowRef, watchEffect } from 'vue' +import { computed, createApp, inject, onMounted, ref, shallowRef, watchEffect } from 'vue' import { createUseZero } from './create-use-zero' -import { createZero } from './create-zero' +import { createZero, zeroSymbol } from './create-zero' import { useQuery } from './query' import { VueView, vueViewFactory } from './view' @@ -22,7 +23,7 @@ export function withSetup(composable: () => T) { return [result, app] as const } -async function setupTestEnvironment() { +async function setupTestEnvironment(registerPlugin = true) { const schema = createSchema({ tables: [ table('table') @@ -34,21 +35,37 @@ async function setupTestEnvironment() { ], }) - const useZero = createUseZero() - const [zero, app] = withSetup(useZero) + let zero: ShallowRef | undefined> | undefined | void> + const userID = ref('asdf') - app.use(createZero(() => ({ - userID: userID.value, - server: null, - schema, - kvStore: 'mem', - }))) + const useZero = createUseZero() + + const setupResult = withSetup(registerPlugin ? useZero : () => {}) + if (setupResult[0]) { + zero = setupResult[0] + } + const app = setupResult[1] + + if (registerPlugin) { + app.use(createZero(() => ({ + userID: userID.value, + server: null, + schema, + kvStore: 'mem', + }))) + } app.mount(document.createElement('div')) - const z = computed(() => zero.value!.value) + const z = computed(() => { + if (zero?.value) { + return zero.value!.value! + } + + return new Zero({ userID: 'asdf', server: null, schema, kvStore: 'mem' }) + }) - await z!.value.mutate.table.insert({ a: 1, b: 'a' }) - await z!.value.mutate.table.insert({ a: 2, b: 'b' }) + await z.value.mutate.table.insert({ a: 1, b: 'a' }) + await z.value.mutate.table.insert({ a: 2, b: 'b' }) const builder = createBuilder(schema) const byIdQuery = syncedQuery @@ -74,7 +91,7 @@ async function setupTestEnvironment() { describe('useQuery', () => { it('useQuery', async () => { const { z, tableQuery, app } = await setupTestEnvironment() - await app.runWithContext(async () => { + await app!.runWithContext(async () => { const { data: rows, status } = useQuery(() => tableQuery) expect(rows.value).toMatchInlineSnapshot(`[ { @@ -125,7 +142,7 @@ describe('useQuery', () => { return } - await app.runWithContext(async () => { + await app!.runWithContext(async () => { const ttl = ref('1m') const materializeSpy = vi.spyOn(tableQuery, 'materialize') @@ -160,7 +177,7 @@ describe('useQuery', () => { return } - await app.runWithContext(async () => { + await app!.runWithContext(async () => { const ttl = ref('1m') let materializeSpy: MockInstance @@ -211,7 +228,7 @@ describe('useQuery', () => { it('useQuery deps change', async () => { const { z, tableQuery, app } = await setupTestEnvironment() - await app.runWithContext(async () => { + await app!.runWithContext(async () => { const a = ref(1) const { data: rows, status } = useQuery(() => @@ -273,7 +290,7 @@ describe('useQuery', () => { it('useQuery deps change watchEffect', async () => { const { z, tableQuery, app } = await setupTestEnvironment() - await app.runWithContext(async () => { + await app!.runWithContext(async () => { const a = ref(1) const { data: rows } = useQuery(() => tableQuery.where('a', a.value)) @@ -331,7 +348,7 @@ describe('useQuery', () => { return } - app.runWithContext(() => { + app!.runWithContext(() => { const a = ref(1) const { data: rows, status } = useQuery(() => byIdQuery(a.value)) @@ -349,8 +366,8 @@ describe('useQuery', () => { }) }) - it('useQuery can be used without plugin (dropped in future versions)', async () => { - const { z, tableQuery } = await setupTestEnvironment() + it('useQuery can be used without plugin (will be dropped in future versions)', async () => { + const { z, tableQuery, app } = await setupTestEnvironment(false) const { data: rows, status } = useQuery(() => tableQuery) expect(rows.value).toMatchInlineSnapshot(`[ @@ -367,6 +384,12 @@ describe('useQuery', () => { ]`) expect(status.value).toEqual('unknown') + app.runWithContext(() => { + if (zeroSymbol) { + expect(inject(zeroSymbol, null)).toBeNull() + } + }) + z.value.close() }) }) diff --git a/src/query.ts b/src/query.ts index d79f5af..d774296 100644 --- a/src/query.ts +++ b/src/query.ts @@ -7,6 +7,7 @@ import type { VueView } from './view' import { computed, getCurrentInstance, + hasInjectionContext, inject, onUnmounted, shallowRef, @@ -40,8 +41,11 @@ export function useQuery< }) const view = shallowRef> | null>(null) - const z = zeroSymbol ? inject(zeroSymbol) : null - if (!z) { + const z = zeroSymbol && hasInjectionContext() ? inject(zeroSymbol) : null + if (!hasInjectionContext()) { + console.warn('Not currently in an injection context (we can\'t call `inject` here). In the future this will throw an error.') + } + else if (!z) { console.warn('Zero-vue plugin not found, make sure to call app.use(createZero()). This is required in order to use Synced Queries, and not doing this will throw an error in future releases.') } From ee3c55d6140b9684e9fad45fdf7bee60a3c53ab2 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Tue, 9 Sep 2025 13:56:07 +0200 Subject: [PATCH 4/9] update readme --- README.md | 54 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e306aca..8e68510 100644 --- a/README.md +++ b/README.md @@ -20,40 +20,58 @@ pnpm install zero-vue ``` Register plugin: - ```js import { createApp } from 'vue' import { createZero } from 'zero-vue' const app = createApp(App) -app.use(createZero()) -``` +// see docs for all options: https://zero.rocicorp.dev/docs/introduction +app.use(createZero({ + userID, + server: import.meta.env.VITE_PUBLIC_SERVER, + schema, + kvStore: 'mem', +})) -Use `useQuery`: -```js -import { Zero } from '@rocicorp/zero' -import { useQuery } from 'zero-vue' +// With computed options: +app.use(createZero(() => ({ + userID: userID.value, + server: import.meta.env.VITE_PUBLIC_SERVER, + schema, + kvStore: 'mem', +}))) -// see docs: https://zero.rocicorp.dev/docs/introduction -const z = new Zero({ +// Or with a Zero instance: +app.use(createZero(new Zero({ userID, server: import.meta.env.VITE_PUBLIC_SERVER, schema, kvStore: 'mem', -}) - -const { data: users } = useQuery(z.query.user) +}))) ``` -Optional: typing `useZero`: +Creating `useZero` composable: ```ts -import { createUseZero } from 'zero-vue'; -import type { Schema } from './schema.ts'; -import type { Mutators } from './mutators.ts'; -export const useZero = createUseZero(); -const z = useZero(); // z is typed with your own schema and mutators +// Typed: +import { createUseZero } from 'zero-vue' +import type { Schema } from './schema.ts' +import type { Mutators } from './mutators.ts' +export const useZero = createUseZero() + +// Untyped: +import { createUseZero } from 'zero-vue' +export const useZero = createUseZero() +``` + +To query data: +```js +import { useQuery } from 'zero-vue' +import { useZero } from './use-zero.ts' +const z = useZero() +const { data: users } = useQuery(z.value.query.user) ``` + > [!TIP] > See [the playground](./playground) for a full working example based on [rocicorp/hello-zero](https://github.com/rocicorp/hello-zero), or check out [danielroe/hello-zero-nuxt](https://github.com/danielroe/hello-zero-nuxt) to see how to set things up with [Nuxt](https://nuxt.com/). From f883279e56a087888ee844bd962e544cb0bf5bb3 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Tue, 9 Sep 2025 14:01:15 +0200 Subject: [PATCH 5/9] some self-review --- package.json | 1 - src/create-zero.ts | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8dee34a..9eedeb2 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "devDependencies": { "@antfu/eslint-config": "latest", "@rocicorp/resolver": "1.0.2", - "@testing-library/vue": "^8.1.0", "@vitest/coverage-v8": "latest", "bumpp": "latest", "changelogithub": "13.16.0", diff --git a/src/create-zero.ts b/src/create-zero.ts index b0ae2f7..a225a1a 100644 --- a/src/create-zero.ts +++ b/src/create-zero.ts @@ -5,7 +5,7 @@ import { shallowRef, toValue, watch } from 'vue' export const zeroSymbol = Symbol('zero') as InjectionKey>> -const oldZeroCleanups = new Set() +const zeroCleanups = new Set() export function createZero(optsOrZero: MaybeRefOrGetter | { zero: Zero }>) { const z = shallowRef() as ShallowRef> @@ -15,9 +15,9 @@ export function createZero toValue(optsOrZero), (opts) => { const cleanupZeroPromise = z.value.close() - oldZeroCleanups.add(cleanupZeroPromise) + zeroCleanups.add(cleanupZeroPromise) cleanupZeroPromise.finally(() => { - oldZeroCleanups.delete(cleanupZeroPromise) + zeroCleanups.delete(cleanupZeroPromise) }) z.value = 'zero' in opts ? opts.zero : new Zero(opts) @@ -25,7 +25,7 @@ export function createZero { - // @ts-expect-error - TODO: type properly + // @ts-expect-error - The type of z doesn't line up with the type of zeroSymbol. app.provide(zeroSymbol, z) }, } From 1e71e9b03710f437be0cf3aa3861802baf78e697 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Tue, 9 Sep 2025 14:19:22 +0200 Subject: [PATCH 6/9] remove non-null assertions --- src/query.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/query.test.ts b/src/query.test.ts index cddb2ed..36cd2af 100644 --- a/src/query.test.ts +++ b/src/query.test.ts @@ -91,7 +91,7 @@ async function setupTestEnvironment(registerPlugin = true) { describe('useQuery', () => { it('useQuery', async () => { const { z, tableQuery, app } = await setupTestEnvironment() - await app!.runWithContext(async () => { + await app.runWithContext(async () => { const { data: rows, status } = useQuery(() => tableQuery) expect(rows.value).toMatchInlineSnapshot(`[ { @@ -142,7 +142,7 @@ describe('useQuery', () => { return } - await app!.runWithContext(async () => { + await app.runWithContext(async () => { const ttl = ref('1m') const materializeSpy = vi.spyOn(tableQuery, 'materialize') @@ -177,7 +177,7 @@ describe('useQuery', () => { return } - await app!.runWithContext(async () => { + await app.runWithContext(async () => { const ttl = ref('1m') let materializeSpy: MockInstance @@ -228,7 +228,7 @@ describe('useQuery', () => { it('useQuery deps change', async () => { const { z, tableQuery, app } = await setupTestEnvironment() - await app!.runWithContext(async () => { + await app.runWithContext(async () => { const a = ref(1) const { data: rows, status } = useQuery(() => @@ -290,7 +290,7 @@ describe('useQuery', () => { it('useQuery deps change watchEffect', async () => { const { z, tableQuery, app } = await setupTestEnvironment() - await app!.runWithContext(async () => { + await app.runWithContext(async () => { const a = ref(1) const { data: rows } = useQuery(() => tableQuery.where('a', a.value)) @@ -348,7 +348,7 @@ describe('useQuery', () => { return } - app!.runWithContext(() => { + app.runWithContext(() => { const a = ref(1) const { data: rows, status } = useQuery(() => byIdQuery(a.value)) From 6dd01122123f2902e1426fc762cd6170b40e6784 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Tue, 9 Sep 2025 14:23:01 +0200 Subject: [PATCH 7/9] fix readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8e68510..074dd5d 100644 --- a/README.md +++ b/README.md @@ -52,14 +52,14 @@ app.use(createZero(new Zero({ Creating `useZero` composable: ```ts -// Typed: -import { createUseZero } from 'zero-vue' -import type { Schema } from './schema.ts' import type { Mutators } from './mutators.ts' +import type { Schema } from './schema.ts' +import { createUseZero } from 'zero-vue' + +// Typed: export const useZero = createUseZero() // Untyped: -import { createUseZero } from 'zero-vue' export const useZero = createUseZero() ``` @@ -67,11 +67,11 @@ To query data: ```js import { useQuery } from 'zero-vue' import { useZero } from './use-zero.ts' + const z = useZero() const { data: users } = useQuery(z.value.query.user) ``` - > [!TIP] > See [the playground](./playground) for a full working example based on [rocicorp/hello-zero](https://github.com/rocicorp/hello-zero), or check out [danielroe/hello-zero-nuxt](https://github.com/danielroe/hello-zero-nuxt) to see how to set things up with [Nuxt](https://nuxt.com/). From 23d6d1616ab4c8d950fc7b874a90bc924e5b5848 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Wed, 10 Sep 2025 14:55:54 +0200 Subject: [PATCH 8/9] run watcher on each zero update --- src/query.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/query.ts b/src/query.ts index d774296..ee43afe 100644 --- a/src/query.ts +++ b/src/query.ts @@ -50,13 +50,13 @@ export function useQuery< } watch( - [() => toValue(query), () => z], + [() => toValue(query), () => toValue(z)], ([q, z]) => { view.value?.destroy() // Only present in v0.23+ - if (z && z.value.materialize) { - view.value = z.value.materialize(q, vueViewFactory, { ttl: ttl.value }) + if (z?.materialize) { + view.value = z.materialize(q, vueViewFactory, { ttl: ttl.value }) return } From 5a1a26333cb3db4ed8d2dac09633b1a0f5676b53 Mon Sep 17 00:00:00 2001 From: Max Stevens Date: Tue, 14 Oct 2025 14:20:20 +0200 Subject: [PATCH 9/9] install --- pnpm-lock.yaml | 52 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d3e2d3..0903932 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,13 +17,13 @@ importers: devDependencies: '@antfu/eslint-config': specifier: latest - version: 5.4.1(@vue/compiler-sfc@3.5.22)(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.4.1(@vue/compiler-sfc@3.5.22)(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1)) '@rocicorp/resolver': specifier: 1.0.2 version: 1.0.2 '@vitest/coverage-v8': specifier: latest - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1)) bumpp: specifier: latest version: 10.3.1(magicast@0.3.5) @@ -33,6 +33,9 @@ importers: eslint: specifier: latest version: 9.37.0(jiti@2.6.0) + happy-dom: + specifier: ^18.0.1 + version: 18.0.1 installed-check: specifier: latest version: 9.3.0 @@ -53,7 +56,7 @@ importers: version: 3.6.1(typescript@5.9.3)(vue-tsc@3.1.1(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) vitest: specifier: latest - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1) vue: specifier: 3.5.22 version: 3.5.22(typescript@5.9.3) @@ -1467,6 +1470,9 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@20.19.21': + resolution: {integrity: sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==} + '@types/node@22.18.10': resolution: {integrity: sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==} @@ -1491,6 +1497,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@typescript-eslint/eslint-plugin@8.44.0': resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2770,6 +2779,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + happy-dom@18.0.1: + resolution: {integrity: sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==} + engines: {node: '>=20.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -4594,6 +4607,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4708,7 +4725,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@antfu/eslint-config@5.4.1(@vue/compiler-sfc@3.5.22)(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1))': + '@antfu/eslint-config@5.4.1(@vue/compiler-sfc@3.5.22)(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 @@ -4717,7 +4734,7 @@ snapshots: '@stylistic/eslint-plugin': 5.4.0(eslint@9.37.0(jiti@2.6.0)) '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3) '@typescript-eslint/parser': 8.44.0(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.3.12(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/eslint-plugin': 1.3.12(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1)) ansis: 4.1.0 cac: 6.7.14 eslint: 9.37.0(jiti@2.6.0) @@ -6220,6 +6237,10 @@ snapshots: dependencies: '@types/node': 22.18.10 + '@types/node@20.19.21': + dependencies: + undici-types: 6.21.0 + '@types/node@22.18.10': dependencies: undici-types: 6.21.0 @@ -6248,6 +6269,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} + '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -6399,7 +6422,7 @@ snapshots: vite: 7.1.9(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -6414,18 +6437,18 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.3.12(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1))': + '@vitest/eslint-plugin@1.3.12(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@typescript-eslint/scope-manager': 8.43.0 '@typescript-eslint/utils': 8.43.0(eslint@9.37.0(jiti@2.6.0))(typescript@5.9.3) eslint: 9.37.0(jiti@2.6.0) optionalDependencies: typescript: 5.9.3 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -7799,6 +7822,12 @@ snapshots: graphemer@1.4.0: {} + happy-dom@18.0.1: + dependencies: + '@types/node': 20.19.21 + '@types/whatwg-mimetype': 3.0.2 + whatwg-mimetype: 3.0.0 + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -9693,7 +9722,7 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.10)(happy-dom@18.0.1)(jiti@2.6.0)(tsx@4.20.5)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -9721,6 +9750,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.18.10 + happy-dom: 18.0.1 transitivePeerDependencies: - jiti - less @@ -9769,6 +9799,8 @@ snapshots: webidl-conversions@3.0.1: {} + whatwg-mimetype@3.0.0: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3