From 129ccc0c56a029f6941f2726be7cc0321ee52cbc Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sun, 11 Aug 2024 22:40:57 +0200 Subject: [PATCH] feat: Implement Yup Form Provider (#45) * chore: extract several utils to shared * chore: add yup provider deps * chore: refactor validation module export * chore: added flush-promises dep * feat: support casting * feat: yup schema provider implementation * feat: rename cast to defaults * chore: ci --- .github/workflows/main.yml | 4 +- package.json | 1 + packages/core/src/form/formContext.ts | 7 +- packages/core/src/form/formSnapshot.ts | 21 ++- packages/core/src/form/useForm.spec.ts | 29 +++- packages/core/src/form/useForm.ts | 3 +- packages/core/src/form/useFormActions.ts | 2 +- packages/core/src/index.ts | 3 +- packages/core/src/types/typedSchema.ts | 4 +- packages/core/src/utils/common.ts | 51 ------ packages/core/src/utils/path.ts | 3 +- packages/core/src/validation/index.ts | 1 + .../core/src/validation/useInputValidity.ts | 9 +- packages/schema-yup/README.md | 3 + packages/schema-yup/package.json | 34 ++++ packages/schema-yup/src/index.spec.ts | 164 ++++++++++++++++++ packages/schema-yup/src/index.ts | 65 +++++++ packages/shared/src/index.ts | 1 + packages/shared/src/utils.ts | 50 ++++++ pnpm-lock.yaml | 51 ++++++ 20 files changed, 426 insertions(+), 80 deletions(-) create mode 100644 packages/core/src/validation/index.ts create mode 100644 packages/schema-yup/README.md create mode 100644 packages/schema-yup/package.json create mode 100644 packages/schema-yup/src/index.spec.ts create mode 100644 packages/schema-yup/src/index.ts create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/utils.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 282a884f..20336ce1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,9 +12,11 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - cache: "pnpm" + cache: 'pnpm' - name: Install dependencies run: pnpm install + - name: Build + run: pnpm build - name: Type Check run: pnpm typecheck - name: Lint diff --git a/package.json b/package.json index f10f1051..32358fd2 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^7.1.0", "filesize": "^10.1.4", + "flush-promises": "^1.0.2", "fs-extra": "^11.2.0", "globals": "^15.9.0", "gzip-size": "^7.0.0", diff --git a/packages/core/src/form/formContext.ts b/packages/core/src/form/formContext.ts index d7a8eb39..769a8f54 100644 --- a/packages/core/src/form/formContext.ts +++ b/packages/core/src/form/formContext.ts @@ -10,9 +10,10 @@ import { ErrorsSchema, TypedSchemaError, } from '../types'; -import { cloneDeep, merge, normalizeArrayable } from '../utils/common'; +import { cloneDeep, normalizeArrayable } from '../utils/common'; import { escapePath, findLeaf, getFromPath, isPathSet, setInPath, unsetPath as unsetInObject } from '../utils/path'; import { FormSnapshot } from './formSnapshot'; +import { merge } from '../../../shared/src'; export type FormValidationMode = 'native' | 'schema'; @@ -125,8 +126,8 @@ export function createFormContext(([key, value]) => ({ path: key, errors: value as string[] })) - .filter(e => e.errors.length > 0); + .map(([key, value]) => ({ path: key, messages: value as string[] })) + .filter(e => e.messages.length > 0); } function setInitialValues(newValues: Partial, opts?: SetValueOptions) { diff --git a/packages/core/src/form/formSnapshot.ts b/packages/core/src/form/formSnapshot.ts index d5411ea4..e3d3883e 100644 --- a/packages/core/src/form/formSnapshot.ts +++ b/packages/core/src/form/formSnapshot.ts @@ -1,9 +1,10 @@ import { Ref, shallowRef, toValue } from 'vue'; -import { FormObject, MaybeGetter, MaybeAsync } from '../types'; +import { FormObject, MaybeGetter, MaybeAsync, TypedSchema } from '../types'; import { cloneDeep, isPromise } from '../utils/common'; -interface FormSnapshotOptions { +interface FormSnapshotOptions { onAsyncInit?: (values: TForm) => void; + schema?: TypedSchema; } export interface FormSnapshot { @@ -11,24 +12,26 @@ export interface FormSnapshot { originals: Ref; } -export function useFormSnapshots( +export function useFormSnapshots( provider: MaybeGetter> | undefined, - opts?: FormSnapshotOptions, + opts?: FormSnapshotOptions, ): FormSnapshot { // We need two copies of the initial values const initials = shallowRef({} as TForm) as Ref; const originals = shallowRef({} as TForm) as Ref; - const initialValuesUnref = toValue(provider); - if (isPromise(initialValuesUnref)) { - initialValuesUnref.then(inits => { + const provided = toValue(provider); + if (isPromise(provided)) { + provided.then(resolved => { + const inits = opts?.schema?.defaults?.(resolved) ?? resolved; initials.value = cloneDeep(inits || {}) as TForm; originals.value = cloneDeep(inits || {}) as TForm; opts?.onAsyncInit?.(cloneDeep(inits)); }); } else { - initials.value = cloneDeep(initialValuesUnref || {}) as TForm; - originals.value = cloneDeep(initialValuesUnref || {}) as TForm; + const inits = opts?.schema?.defaults?.(provided || ({} as TForm)) ?? provided; + initials.value = cloneDeep(inits || {}) as TForm; + originals.value = cloneDeep(inits || {}) as TForm; } return { diff --git a/packages/core/src/form/useForm.spec.ts b/packages/core/src/form/useForm.spec.ts index 0c4f84f4..4be83911 100644 --- a/packages/core/src/form/useForm.spec.ts +++ b/packages/core/src/form/useForm.spec.ts @@ -411,7 +411,7 @@ describe('validation', () => { await nextTick(); await fireEvent.click(screen.getByText('Submit')); expect(handler).not.toHaveBeenCalled(); - await fireEvent.change(screen.getByTestId('input'), { target: { value: 'test' } }); + await fireEvent.update(screen.getByTestId('input'), 'test'); await fireEvent.click(screen.getByText('Submit')); await nextTick(); expect(handler).toHaveBeenCalledOnce(); @@ -422,7 +422,7 @@ describe('validation', () => { const schema: TypedSchema = { async parse() { return { - errors: [{ path: 'test', errors: ['error'] }], + errors: [{ path: 'test', messages: ['error'] }], }; }, }; @@ -454,7 +454,7 @@ describe('validation', () => { const schema: TypedSchema = { async parse() { return { - errors: shouldError ? [{ path: 'test', errors: ['error'] }] : [], + errors: shouldError ? [{ path: 'test', messages: ['error'] }] : [], }; }, }; @@ -577,7 +577,7 @@ describe('validation', () => { const schema: TypedSchema = { async parse() { return { - errors: [{ path: 'test', errors: ['error'] }], + errors: [{ path: 'test', messages: ['error'] }], }; }, }; @@ -603,7 +603,7 @@ describe('validation', () => { const schema: TypedSchema<{ test: string }> = { async parse() { return { - errors: [{ path: 'test', errors: ['error'] }], + errors: [{ path: 'test', messages: ['error'] }], }; }, }; @@ -625,7 +625,7 @@ describe('validation', () => { const schema: TypedSchema<{ test: string }> = { async parse() { return { - errors: [{ path: 'test', errors: wasReset ? ['reset'] : ['error'] }], + errors: [{ path: 'test', messages: wasReset ? ['reset'] : ['error'] }], }; }, }; @@ -642,4 +642,21 @@ describe('validation', () => { await reset({ revalidate: true }); expect(getError('test')).toBe('reset'); }); + + test('typed schema can initialize with default values', async () => { + const { values } = await renderSetup(() => { + return useForm({ + schema: { + defaults: () => ({ test: 'foo' }), + async parse() { + return { + errors: [], + }; + }, + }, + }); + }); + + expect(values).toEqual({ test: 'foo' }); + }); }); diff --git a/packages/core/src/form/useForm.ts b/packages/core/src/form/useForm.ts index 7638afb7..3dca3898 100644 --- a/packages/core/src/form/useForm.ts +++ b/packages/core/src/form/useForm.ts @@ -36,8 +36,9 @@ export function useForm>, ) { const touchedSnapshot = useFormSnapshots(opts?.initialTouched); - const valuesSnapshot = useFormSnapshots(opts?.initialValues, { + const valuesSnapshot = useFormSnapshots(opts?.initialValues, { onAsyncInit, + schema: opts?.schema, }); const values = reactive(cloneDeep(valuesSnapshot.originals.value)) as TForm; diff --git a/packages/core/src/form/useFormActions.ts b/packages/core/src/form/useFormActions.ts index f9ba2320..a79978de 100644 --- a/packages/core/src/form/useFormActions.ts +++ b/packages/core/src/form/useFormActions.ts @@ -85,7 +85,7 @@ export function useFormActions, entry.errors); + form.setFieldErrors(entry.path as Path, entry.messages); } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0dfdaaaa..34f5ed5d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,6 +6,7 @@ export * from './useSlider'; export * from './useCheckbox'; export * from './useNumberField'; export * from './useSpinButton'; -export * from './types/index'; +export * from './types'; export * from './config'; export * from './form'; +export * from './validation'; diff --git a/packages/core/src/types/typedSchema.ts b/packages/core/src/types/typedSchema.ts index f54abf3e..d68bda89 100644 --- a/packages/core/src/types/typedSchema.ts +++ b/packages/core/src/types/typedSchema.ts @@ -2,7 +2,7 @@ import { FormObject } from './common'; export interface TypedSchemaError { path: string; - errors: string[]; + messages: string[]; } export interface TypedSchemaContext { @@ -11,7 +11,7 @@ export interface TypedSchemaContext { export interface TypedSchema { parse(values: TInput, context?: TypedSchemaContext): Promise<{ output?: TOutput; errors: TypedSchemaError[] }>; - cast?(values: Partial): TInput; + defaults?(values: TInput): TInput; } export type InferOutput = diff --git a/packages/core/src/utils/common.ts b/packages/core/src/utils/common.ts index 529e8d77..6d8621e4 100644 --- a/packages/core/src/utils/common.ts +++ b/packages/core/src/utils/common.ts @@ -132,13 +132,6 @@ export function normalizeArrayable(value: Arrayable): T[] { return Array.isArray(value) ? [...value] : [value]; } -export const isObject = (obj: unknown): obj is Record => - obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj); - -export function isIndex(value: unknown): value is number { - return Number(value) >= 0; -} - /** * Clones a value deeply. I wish we could use `structuredClone` but it's not supported because of the deep Proxy usage by Vue. * I added some shortcuts here to avoid cloning some known types we don't plan to support. @@ -152,50 +145,6 @@ export function cloneDeep(value: T): T { return klona(value); } -export function isObjectLike(value: unknown) { - return typeof value === 'object' && value !== null; -} - -export function getTag(value: unknown) { - if (value == null) { - return value === undefined ? '[object Undefined]' : '[object Null]'; - } - return Object.prototype.toString.call(value); -} - -// Reference: https://github.com/lodash/lodash/blob/master/isPlainObject.js -export function isPlainObject(value: unknown) { - if (!isObjectLike(value) || getTag(value) !== '[object Object]') { - return false; - } - if (Object.getPrototypeOf(value) === null) { - return true; - } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; -} - -export function merge(target: any, source: any) { - Object.keys(source).forEach(key => { - if (isPlainObject(source[key]) && isPlainObject(target[key])) { - if (!target[key]) { - target[key] = {}; - } - - merge(target[key], source[key]); - return; - } - - target[key] = source[key]; - }); - - return target; -} - export function isPromise(value: unknown): value is Promise { return value instanceof Promise; } diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts index 64162045..63156637 100644 --- a/packages/core/src/utils/path.ts +++ b/packages/core/src/utils/path.ts @@ -1,4 +1,5 @@ -import { isIndex, isNullOrUndefined, isObject } from './common'; +import { isIndex, isObject } from '../../../shared/src'; +import { isNullOrUndefined } from './common'; export function isContainerValue(value: unknown): value is Record { return isObject(value) || Array.isArray(value); diff --git a/packages/core/src/validation/index.ts b/packages/core/src/validation/index.ts new file mode 100644 index 00000000..aa92e539 --- /dev/null +++ b/packages/core/src/validation/index.ts @@ -0,0 +1 @@ +export * from './useInputValidity'; diff --git a/packages/core/src/validation/useInputValidity.ts b/packages/core/src/validation/useInputValidity.ts index c1a8c6b8..c348f6cd 100644 --- a/packages/core/src/validation/useInputValidity.ts +++ b/packages/core/src/validation/useInputValidity.ts @@ -15,11 +15,12 @@ export function useInputValidity(opts: InputValidityOptions) { const validationMode = form?.getValidationMode() ?? 'native'; function updateValiditySync() { - validityDetails.value = opts.inputRef?.value?.validity; - - if (validationMode === 'native') { - setErrors(opts.inputRef?.value?.validationMessage || []); + if (validationMode !== 'native') { + return; } + + validityDetails.value = opts.inputRef?.value?.validity; + setErrors(opts.inputRef?.value?.validationMessage || []); } async function updateValidity() { diff --git a/packages/schema-yup/README.md b/packages/schema-yup/README.md new file mode 100644 index 00000000..0214e371 --- /dev/null +++ b/packages/schema-yup/README.md @@ -0,0 +1,3 @@ +# Yup + +This is the typed schema implementation for the `yup` provider. diff --git a/packages/schema-yup/package.json b/packages/schema-yup/package.json new file mode 100644 index 00000000..df50d3c0 --- /dev/null +++ b/packages/schema-yup/package.json @@ -0,0 +1,34 @@ +{ + "name": "@formwerk/schema-yup", + "version": "0.0.1", + "description": "", + "sideEffects": false, + "module": "dist/schema-yup.esm.js", + "unpkg": "dist/schema-yup.js", + "main": "dist/schema-yup.js", + "types": "dist/schema-yup.d.ts", + "repository": { + "url": "https://github.com/formwerkjs/formwerk.git", + "type": "git", + "directory": "packages/schema-yup" + }, + "keywords": [ + "VueJS", + "Vue", + "validation", + "validator", + "inputs", + "form" + ], + "files": [ + "dist/*.js", + "dist/*.d.ts" + ], + "dependencies": { + "@formwerk/core": "workspace:*", + "type-fest": "^4.24.0", + "yup": "^1.3.2" + }, + "author": "", + "license": "MIT" +} diff --git a/packages/schema-yup/src/index.spec.ts b/packages/schema-yup/src/index.spec.ts new file mode 100644 index 00000000..7bdb09b7 --- /dev/null +++ b/packages/schema-yup/src/index.spec.ts @@ -0,0 +1,164 @@ +import { type Component, nextTick } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; +import { useForm, useTextField } from '@formwerk/core'; +import { defineSchema } from '.'; +import * as y from 'yup'; +import flush from 'flush-promises'; + +const requiredMessage = (field: string) => `${field} is a required field`; + +describe('schema-yup', () => { + function createInputComponent(): Component { + return { + inheritAttrs: false, + setup: (_, { attrs }) => { + const name = (attrs.name || 'test') as string; + const { errorMessage, inputProps } = useTextField({ name, label: name }); + + return { errorMessage: errorMessage, inputProps, name }; + }, + template: ` + + {{ errorMessage }} + `, + }; + } + + test('validates initially with yup schema', async () => { + await render({ + components: { Child: createInputComponent() }, + setup() { + const { getError, isValid } = useForm({ + schema: defineSchema( + y.object({ + test: y.string().required(), + }), + ), + }); + + return { getError, isValid }; + }, + template: ` +
+ + + {{ getError('test') }} + {{ isValid }} + + `, + }); + + await flush(); + expect(screen.getByTestId('form-valid').textContent).toBe('false'); + expect(screen.getByTestId('err').textContent).toBe(requiredMessage('test')); + expect(screen.getByTestId('form-err').textContent).toBe(requiredMessage('test')); + }); + + test('prevents submission if the form is not valid', async () => { + const handler = vi.fn(); + + await render({ + components: { Child: createInputComponent() }, + setup() { + const { handleSubmit } = useForm({ + schema: defineSchema( + y.object({ + test: y.string().required(), + }), + ), + }); + + return { onSubmit: handleSubmit(handler) }; + }, + template: ` +
+ + + + + `, + }); + + await nextTick(); + await fireEvent.click(screen.getByText('Submit')); + expect(handler).not.toHaveBeenCalled(); + await fireEvent.update(screen.getByTestId('test'), 'test'); + await fireEvent.click(screen.getByText('Submit')); + await flush(); + expect(handler).toHaveBeenCalledOnce(); + }); + + test('supports transformations', async () => { + const handler = vi.fn(); + + await render({ + components: { Child: createInputComponent() }, + setup() { + const { handleSubmit, getError } = useForm({ + schema: defineSchema( + y.object({ + test: y + .string() + .required() + .transform(value => (value ? `epic-${value}` : value)), + age: y + .number() + .required() + .transform(value => Number(value)), + }), + ), + }); + + return { getError, onSubmit: handleSubmit(handler) }; + }, + template: ` +
+ + + + + + `, + }); + + await flush(); + await fireEvent.update(screen.getByTestId('test'), 'test'); + await fireEvent.update(screen.getByTestId('age'), '11'); + await fireEvent.click(screen.getByText('Submit')); + await flush(); + expect(handler).toHaveBeenCalledOnce(); + expect(handler).toHaveBeenLastCalledWith({ test: 'epic-test', age: 11 }); + }); + + test('supports defaults', async () => { + const handler = vi.fn(); + + await render({ + components: { Child: createInputComponent() }, + setup() { + const { handleSubmit, getError } = useForm({ + schema: defineSchema( + y.object({ + test: y.string().required().default('default-test'), + age: y.number().required().default(22), + }), + ), + }); + + return { getError, onSubmit: handleSubmit(handler) }; + }, + template: ` +
+ + + + + + `, + }); + + await flush(); + await expect(screen.getByDisplayValue('default-test')).toBeDefined(); + await expect(screen.getByDisplayValue('22')).toBeDefined(); + }); +}); diff --git a/packages/schema-yup/src/index.ts b/packages/schema-yup/src/index.ts new file mode 100644 index 00000000..a20618ca --- /dev/null +++ b/packages/schema-yup/src/index.ts @@ -0,0 +1,65 @@ +import { InferType, Schema, ValidateOptions, ValidationError } from 'yup'; +import type { PartialDeep } from 'type-fest'; +import { TypedSchema, TypedSchemaError } from '@formwerk/core'; +import { isObject, merge } from '../../shared/src'; + +export function defineSchema, TInput = PartialDeep>( + yupSchema: TSchema, + opts: ValidateOptions = { abortEarly: false }, +): TypedSchema { + const schema: TypedSchema = { + async parse(values) { + try { + // we spread the options because yup mutates the opts object passed + const output = await yupSchema.validate(values, { ...opts }); + + return { + output, + errors: [], + }; + } catch (err) { + const error = err as ValidationError; + // Yup errors have a name prop one them. + // https://github.com/jquense/yup#validationerrorerrors-string--arraystring-value-any-path-string + if (error.name !== 'ValidationError') { + throw err; + } + + if (!error.inner?.length && error.errors.length) { + return { errors: [{ path: error.path as string, messages: error.errors }] }; + } + + const errors: Record = error.inner.reduce( + (acc, curr) => { + const path = curr.path || ''; + if (!acc[path]) { + acc[path] = { messages: [], path }; + } + + acc[path].messages.push(...curr.errors); + + return acc; + }, + {} as Record, + ); + + // list of aggregated errors + return { errors: Object.values(errors) }; + } + }, + defaults(values) { + try { + return yupSchema.cast(values); + } catch { + const defaults = yupSchema.getDefault(); + if (isObject(defaults) && isObject(values)) { + return merge(defaults, values); + } + + return values; + } + }, + }; + + return schema; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 00000000..04bca77e --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts new file mode 100644 index 00000000..1f743b0c --- /dev/null +++ b/packages/shared/src/utils.ts @@ -0,0 +1,50 @@ +export function isObjectLike(value: unknown) { + return typeof value === 'object' && value !== null; +} + +export function getTag(value: unknown) { + if (value == null) { + return value === undefined ? '[object Undefined]' : '[object Null]'; + } + return Object.prototype.toString.call(value); +} + +// Reference: https://github.com/lodash/lodash/blob/master/isPlainObject.js +export function isPlainObject(value: unknown) { + if (!isObjectLike(value) || getTag(value) !== '[object Object]') { + return false; + } + if (Object.getPrototypeOf(value) === null) { + return true; + } + let proto = value; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + + return Object.getPrototypeOf(value) === proto; +} + +export function merge(target: any, source: any) { + Object.keys(source).forEach(key => { + if (isPlainObject(source[key]) && isPlainObject(target[key])) { + if (!target[key]) { + target[key] = {}; + } + + merge(target[key], source[key]); + return; + } + + target[key] = source[key]; + }); + + return target; +} + +export function isIndex(value: unknown): value is number { + return Number(value) >= 0; +} + +export const isObject = (obj: unknown): obj is Record => + obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 078b328d..47ff4467 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: filesize: specifier: ^10.1.4 version: 10.1.4 + flush-promises: + specifier: ^1.0.2 + version: 1.0.2 fs-extra: specifier: ^11.2.0 version: 11.2.0 @@ -166,6 +169,18 @@ importers: specifier: ^2.0.29 version: 2.0.29(typescript@5.5.4) + packages/schema-yup: + dependencies: + '@formwerk/core': + specifier: workspace:* + version: link:../core + type-fest: + specifier: ^4.24.0 + version: 4.24.0 + yup: + specifier: ^1.3.2 + version: 1.4.0 + packages/test-utils: {} packages: @@ -1843,6 +1858,9 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + flush-promises@1.0.2: + resolution: {integrity: sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==} + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -2761,6 +2779,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -3101,6 +3122,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tinybench@2.8.0: resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} @@ -3128,6 +3152,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -3179,6 +3206,10 @@ packages: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + type-fest@4.24.0: resolution: {integrity: sha512-spAaHzc6qre0TlZQQ2aA/nGMe+2Z/wyGk5Z+Ru2VUfdNwT6kWO6TjevOlpebsATEG1EIQ2sOiDszud3lO5mt/Q==} engines: {node: '>=16'} @@ -3493,6 +3524,9 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + yup@1.4.0: + resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==} + zod-package-json@1.0.3: resolution: {integrity: sha512-Mb6GzuRyUEl8X+6V6xzHbd4XV0au/4gOYrYP+CAfHL32uPmGswES+v2YqonZiW1NZWVA3jkssCKSU2knonm/aQ==} engines: {node: '>=20'} @@ -5411,6 +5445,8 @@ snapshots: flatted@3.3.1: {} + flush-promises@1.0.2: {} + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -6305,6 +6341,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + property-expr@2.0.6: {} + proto-list@1.2.4: {} pseudomap@1.0.2: {} @@ -6675,6 +6713,8 @@ snapshots: through@2.3.8: {} + tiny-case@1.0.3: {} + tinybench@2.8.0: {} tinypool@1.0.0: {} @@ -6693,6 +6733,8 @@ snapshots: dependencies: is-number: 7.0.0 + toposort@2.0.2: {} + tough-cookie@4.1.4: dependencies: psl: 1.9.0 @@ -6738,6 +6780,8 @@ snapshots: type-detect@4.1.0: {} + type-fest@2.19.0: {} + type-fest@4.24.0: {} typed-array-buffer@1.0.0: @@ -7028,6 +7072,13 @@ snapshots: yocto-queue@1.1.1: {} + yup@1.4.0: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + zod-package-json@1.0.3: dependencies: zod: 3.23.8