diff --git a/packages/varden/package.json b/packages/varden/package.json index 98ee04d..ecede27 100644 --- a/packages/varden/package.json +++ b/packages/varden/package.json @@ -48,15 +48,15 @@ }, "devDependencies": { "@types/node": "^24.12.0", - "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue": "^6.0.7", "@vitest/browser": "3.2.4", "@vitest/coverage-v8": "^3.2.4", - "@vue/test-utils": "^2.4.6", + "@vue/test-utils": "^2.4.10", "@vue/tsconfig": "^0.8.1", "changelogen": "^0.6.2", "dequal": "^2.0.3", "eslint-plugin-package-json": "^1.1.0", - "playwright": "^1.58.2", + "playwright": "^1.60.0", "tsdown": "^0.21.6", "typescript": "~6.0.2", "unplugin-vue": "^7.1.1", diff --git a/packages/varden/src/lib.ts b/packages/varden/src/lib.ts index 85efe34..35f4370 100644 --- a/packages/varden/src/lib.ts +++ b/packages/varden/src/lib.ts @@ -15,6 +15,8 @@ import { Empty, isEmptyObject, type CompiledPath, + type ArrayPaths, + type GetArray, } from './path'; import { createFieldMeta, type FieldMeta } from './field-metadata'; @@ -45,6 +47,16 @@ export interface FormContext { isDirty>(path: Path): boolean; getError>(path: Path): string | null; submit(): void; + pop>(path: Path): undefined | GetArray; + shift>(path: Path): undefined | GetArray; + push>(path: Path, value: GetArray): void; + unshift>(path: Path, value: GetArray): void; + splice>( + path: Path, + index?: number, + deleteCount?: number, + ...items: GetArray[] + ): void; } export interface _FormContext extends FormContext { @@ -182,6 +194,40 @@ export function useForm(props: FormProps): FormContext { return false; }); + const setValue: FormContext['setValue'] = (path, value) => { + const compiledPath = Array.isArray(path) ? path : toCompiledPath(path); + const stringPath = typeof path === 'string' ? path : compiledPath.join('.'); + + set(currentValues.value, compiledPath, cloneFn(value)); + + const meta = fields.get(stringPath); + const isDirty = !equalsFn(get(initialValues, compiledPath, Empty), value); + if (meta) { + meta.dirty = isDirty; + } else { + fields.set(stringPath, createFieldMeta(false, isDirty, '', 0)); + } + + // cleanup child fields + for (const [nestedPath, nestedMeta] of fields) { + if (nestedPath.startsWith(`${stringPath}.`)) { + if (nestedMeta !== undefined) { + if (nestedMeta.refCount === 0) { + fields.delete(nestedPath); + } else { + const compiledNestedPath = toCompiledPath(nestedPath); + nestedMeta.dirty = !equalsFn( + get(initialValues, compiledNestedPath), + get(currentValues.value, compiledNestedPath), + ); + } + } + } + } + + applyValidation(); + }; + return { values: readonly(currentValues) as FormContext['values'], dirty, @@ -189,42 +235,7 @@ export function useForm(props: FormProps): FormContext { __meta: fields, reset, resetField, - setValue, Value extends Get>( - path: Path | CompiledPath, - value: Value | undefined, - ) { - const compiledPath = Array.isArray(path) ? path : toCompiledPath(path); - const stringPath = typeof path === 'string' ? path : compiledPath.join('.'); - - set(currentValues.value, compiledPath, cloneFn(value)); - - const meta = fields.get(stringPath); - const isDirty = !equalsFn(get(initialValues, compiledPath, Empty), value); - if (meta) { - meta.dirty = isDirty; - } else { - fields.set(stringPath, createFieldMeta(false, isDirty, '', 0)); - } - - // cleanup child fields - for (const [nestedPath, nestedMeta] of fields) { - if (nestedPath.startsWith(`${stringPath}.`)) { - if (nestedMeta !== undefined) { - if (nestedMeta.refCount === 0) { - fields.delete(nestedPath); - } else { - const compiledNestedPath = toCompiledPath(nestedPath); - nestedMeta.dirty = !equalsFn( - get(initialValues, compiledNestedPath), - get(currentValues.value, compiledNestedPath), - ); - } - } - } - } - - applyValidation(); - }, + setValue, getValue, Value extends Get>(path: Path | CompiledPath): Value { const a = get(currentValues.value, Array.isArray(path) ? path : toCompiledPath(path)); return cloneFn(toRaw(a)); @@ -273,5 +284,51 @@ export function useForm(props: FormProps): FormContext { getError>(path: Path): string | null { return fields.get(path)?.error ?? null; }, + // arrays + pop>(path: Path): undefined | GetArray { + const compiledPath = toCompiledPath(path); + const value = get(currentValues.value, compiledPath); + return Array.isArray(value) ? value.pop() : undefined; + }, + shift>(path: Path): undefined | GetArray { + const compiledPath = toCompiledPath(path); + const value = get(currentValues.value, compiledPath); + return Array.isArray(value) ? value.shift() : undefined; + }, + push>(path: Path, value: GetArray): void { + const compiledPath = toCompiledPath(path); + const arr = get(currentValues.value, compiledPath); + if (Array.isArray(arr)) { + arr.push(cloneFn(value)); + } else { + // @ts-expect-error GetArray/Get conversion + setValue(path, [cloneFn(value)]); + } + }, + unshift>(path: Path, value: GetArray): void { + const compiledPath = toCompiledPath(path); + const arr = get(currentValues.value, compiledPath); + if (Array.isArray(arr)) { + arr.unshift(cloneFn(value)); + } else { + // @ts-expect-error GetArray/Get conversion + setValue(path, [cloneFn(value)]); + } + }, + splice>( + path: Path, + index?: number, + deleteCount?: number, + ...items: GetArray[] + ): void { + const compiledPath = toCompiledPath(path); + const arr = get(currentValues.value, compiledPath); + if (Array.isArray(arr)) { + arr.splice(index ?? 0, deleteCount ?? 0, ...(items ?? []).map((v) => cloneFn(v))); + } else if (Array.isArray(items)) { + // @ts-expect-error GetArray/Get conversion + setValue(path, items.map((v) => cloneFn(v))); + } + }, }; } diff --git a/packages/varden/src/path.ts b/packages/varden/src/path.ts index 49f6396..169fe24 100644 --- a/packages/varden/src/path.ts +++ b/packages/varden/src/path.ts @@ -1,12 +1,46 @@ export type Paths = T extends Array - ? `${Paths}` + ? (U extends object ? never : `${number}`) | `${number}.${Paths}` : T extends object ? { [K in keyof T & (string | number)]: K extends string ? `${K}` | `${K}.${Paths}` : never; }[keyof T & (string | number)] : never; +export type ArrayPaths = T extends Array + ? (U extends Array ? `${number}` : never) | `${number}.${ArrayPaths}` + : T extends object + ? { + [K in keyof T & (string | number)]: K extends string + ? T[K] extends Array + ? `${K}` | `${K}.${ArrayPaths}` + : `${K}.${ArrayPaths}` + : never; + }[keyof T & (string | number)] + : never; + export type Get> = P extends `${infer K}.${infer R}` + ? K extends keyof T + ? R extends Paths + ? Get + : T[K] extends Array + ? R extends `${number}` + ? U + : never + : never + : T extends Array + ? K extends `${number}` + ? Get ? R : never> + : never + : never + : P extends keyof T + ? T[P] + : T extends Array + ? P extends `${number}` + ? U + : never + : never; + +type _GetArray> = P extends `${infer K}.${infer R}` ? K extends keyof T ? R extends Paths ? Get @@ -16,6 +50,11 @@ export type Get> = P extends `${infer K}.${infer R}` ? T[P] : never; +// Specialized Get for ArrayPaths as a workaround for tsc stack overflow error +export type GetArray> = _GetArray extends Array + ? R + : never; + export type CompiledPath = Array; export function toCompiledPath(path: string): CompiledPath { diff --git a/packages/varden/tests/field-value.spec.ts b/packages/varden/tests/field-value.spec.ts index e4f48da..cb6b75e 100644 --- a/packages/varden/tests/field-value.spec.ts +++ b/packages/varden/tests/field-value.spec.ts @@ -174,3 +174,185 @@ describe('form.setValue object', () => { expect(form.values.value.user?.name).toBe('initial'); }); }); + +describe('form arrays', () => { + it('should push into existing array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', []); + form.push('users', { name: 'test' }); + expect(form.values.value.users).toEqual([{ name: 'test' }]); + }); + + it('should create a new array if none exists', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.push('users', { name: 'test' }); + expect(form.values.value.users).toEqual([{ name: 'test' }]); + }); + + it('should pop from existing array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', [{ name: 'a' }, { name: 'b' }]); + const popped = form.pop('users'); + expect(popped).toEqual({ name: 'b' }); + expect(form.values.value.users).toEqual([{ name: 'a' }]); + }); + + it('should return undefined when popping from empty array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', []); + const popped = form.pop('users'); + expect(popped).toBeUndefined(); + expect(form.values.value.users).toEqual([]); + }); + + it('should return undefined when popping from non-existent array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + const popped = form.pop('users'); + expect(popped).toBeUndefined(); + }); + + it('should shift from existing array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', [{ name: 'a' }, { name: 'b' }]); + const shifted = form.shift('users'); + expect(shifted).toEqual({ name: 'a' }); + expect(form.values.value.users).toEqual([{ name: 'b' }]); + }); + + it('should return undefined when shifting from empty array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', []); + const shifted = form.shift('users'); + expect(shifted).toBeUndefined(); + expect(form.values.value.users).toEqual([]); + }); + + it('should return undefined when shifting from non-existent array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + const shifted = form.shift('users'); + expect(shifted).toBeUndefined(); + }); + + it('should unshift into existing array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', [{ name: 'a' }]); + form.unshift('users', { name: 'b' }); + expect(form.values.value.users).toEqual([{ name: 'b' }, { name: 'a' }]); + }); + + it('should create a new array when unshifting into non-existent array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.unshift('users', { name: 'test' }); + expect(form.values.value.users).toEqual([{ name: 'test' }]); + }); + + it('should splice to insert items at index', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', [{ name: 'a' }, { name: 'c' }]); + form.splice('users', 1, 0, { name: 'b' }); + expect(form.values.value.users).toEqual([ + { name: 'a' }, + { name: 'b' }, + { name: 'c' }, + ]); + }); + + it('should splice to remove items at index', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', [ + { name: 'a' }, + { name: 'b' }, + { name: 'c' }, + ]); + form.splice('users', 1, 1); + expect(form.values.value.users).toEqual([{ name: 'a' }, { name: 'c' }]); + }); + + it('should splice to replace items at index', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', [ + { name: 'a' }, + { name: 'b' }, + { name: 'c' }, + ]); + form.splice('users', 1, 1, { name: 'x' }); + expect(form.values.value.users).toEqual([ + { name: 'a' }, + { name: 'x' }, + { name: 'c' }, + ]); + }); + + it('should create a new array when splicing into non-existent array', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.splice('users', 0, 0, { name: 'a' }, { name: 'b' }); + expect(form.values.value.users).toEqual([{ name: 'a' }, { name: 'b' }]); + }); + + it('should use defaults when splice is called without index or deleteCount', () => { + const form = useForm({ + onSubmit, + schema: v.object({ users: v.array(v.object({ name: v.string() })) }), + }); + + form.setValue('users', [{ name: 'a' }]); + form.splice('users'); + expect(form.values.value.users).toEqual([{ name: 'a' }]); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e6c40c..04d569a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,17 +79,17 @@ importers: specifier: ^24.12.0 version: 24.12.0 '@vitejs/plugin-vue': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vue@3.5.31(typescript@6.0.2)) + specifier: ^6.0.7 + version: 6.0.7(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vue@3.5.31(typescript@6.0.2)) '@vitest/browser': specifier: 3.2.4 - version: 3.2.4(playwright@1.58.2)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4) + version: 3.2.4(playwright@1.60.0)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) '@vue/test-utils': - specifier: ^2.4.6 - version: 2.4.6 + specifier: ^2.4.10 + version: 2.4.10(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.31(vue@3.5.31(typescript@6.0.2)))(vue@3.5.31(typescript@6.0.2)) '@vue/tsconfig': specifier: ^0.8.1 version: 0.8.1(typescript@6.0.2)(vue@3.5.31(typescript@6.0.2)) @@ -103,8 +103,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(@types/estree@1.0.8)(eslint@10.4.0(jiti@2.6.1)) playwright: - specifier: ^1.58.2 - version: 1.58.2 + specifier: ^1.60.0 + version: 1.60.0 tsdown: specifier: ^0.21.6 version: 0.21.6(typescript@6.0.2)(vue-tsc@3.2.9(typescript@6.0.2)) @@ -947,6 +947,9 @@ packages: '@rolldown/pluginutils@1.0.0-rc.12': resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.50.1': resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} cpu: [arm] @@ -1473,6 +1476,13 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vue: ^3.2.25 + '@vitejs/plugin-vue@6.0.7': + resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + '@vitest/browser@3.2.4': resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} peerDependencies: @@ -1608,8 +1618,15 @@ packages: '@vue/shared@3.5.34': resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} - '@vue/test-utils@2.4.6': - resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + '@vue/test-utils@2.4.10': + resolution: {integrity: sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true '@vue/tsconfig@0.7.0': resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==} @@ -2623,13 +2640,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} engines: {node: '>=18'} hasBin: true @@ -3244,8 +3261,8 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue-component-type-helpers@2.2.12: - resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + vue-component-type-helpers@3.3.2: + resolution: {integrity: sha512-l4Z2Y34m7nFMlx8vrslJaVtXxUpzgDMSESC7TakG/c5kwjYT/do+E0NcT2/vWDzaoIhsShg/2OKwX7Q4nbzC0g==} vue-eslint-parser@10.4.0: resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} @@ -3910,6 +3927,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.50.1': optional: true @@ -4325,13 +4344,13 @@ snapshots: vite: 7.1.5(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) vue: 3.5.31(typescript@5.8.3) - '@vitejs/plugin-vue@6.0.1(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vue@3.5.31(typescript@6.0.2))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vue@3.5.31(typescript@6.0.2))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.29 + '@rolldown/pluginutils': 1.0.1 vite: 8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1) vue: 3.5.31(typescript@6.0.2) - '@vitest/browser@3.2.4(playwright@1.58.2)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.60.0)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) @@ -4343,7 +4362,7 @@ snapshots: vitest: 3.2.4(@types/node@24.12.0)(@vitest/browser@3.2.4)(jiti@2.6.1)(lightningcss@1.32.0) ws: 8.20.0 optionalDependencies: - playwright: 1.58.2 + playwright: 1.60.0 transitivePeerDependencies: - bufferutil - msw @@ -4367,7 +4386,7 @@ snapshots: tinyrainbow: 2.0.0 vitest: 3.2.4(@types/node@24.12.0)(@vitest/browser@3.2.4)(jiti@2.6.1)(lightningcss@1.32.0) optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.58.2)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.60.0)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -4568,10 +4587,14 @@ snapshots: '@vue/shared@3.5.34': {} - '@vue/test-utils@2.4.6': + '@vue/test-utils@2.4.10(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.31(vue@3.5.31(typescript@6.0.2)))(vue@3.5.31(typescript@6.0.2))': dependencies: + '@vue/compiler-dom': 3.5.34 js-beautify: 1.15.4 - vue-component-type-helpers: 2.2.12 + vue: 3.5.31(typescript@6.0.2) + vue-component-type-helpers: 3.3.2 + optionalDependencies: + '@vue/server-renderer': 3.5.31(vue@3.5.31(typescript@6.0.2)) '@vue/tsconfig@0.7.0(typescript@5.8.3)(vue@3.5.31(typescript@5.8.3))': optionalDependencies: @@ -5564,11 +5587,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.58.2: {} + playwright-core@1.60.0: {} - playwright@1.58.2: + playwright@1.60.0: dependencies: - playwright-core: 1.58.2 + playwright-core: 1.60.0 optionalDependencies: fsevents: 2.3.2 @@ -6202,7 +6225,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.12.0 - '@vitest/browser': 3.2.4(playwright@1.58.2)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.60.0)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1))(vitest@3.2.4) transitivePeerDependencies: - jiti - less @@ -6219,7 +6242,7 @@ snapshots: vscode-uri@3.1.0: {} - vue-component-type-helpers@2.2.12: {} + vue-component-type-helpers@3.3.2: {} vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.6.1)): dependencies: