diff --git a/README.md b/README.md index 6e6339e..762d761 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ load `apps/extension/.output/chrome-mv3` unpacked. ``` apps/extension/ Chrome extension: recorder + editor (WXT + React) -packages/schema/ Demo format: Zod schema + TypeScript types +packages/schema/ Demo format: Valibot schema + TypeScript types packages/player-core/ Framework-agnostic playback engine packages/exporter/ Builds the exported bundle in the browser adapters/player-react/ for React diff --git a/packages/exporter/vitest.config.ts b/packages/exporter/vitest.config.ts index b71cb7e..ca96bc2 100644 --- a/packages/exporter/vitest.config.ts +++ b/packages/exporter/vitest.config.ts @@ -5,7 +5,7 @@ import { defineConfig } from "vitest/config"; // (browser-only APIs), and buildDemoBundle calls into it for any non-webp photo step. export default defineConfig({ // Pre-bundle up front — an on-demand optimize mid-run forces Vite to reload the page, - // breaking whatever test was in flight (same issue player-core hit with zod). + // breaking whatever test was in flight (same issue player-core hit with the schema's validator). optimizeDeps: { include: ["jszip"] }, test: { include: ["src/__test__/unit/**/*.test.ts"], diff --git a/packages/player-core/CLAUDE.md b/packages/player-core/CLAUDE.md index a4312dc..5bea660 100644 --- a/packages/player-core/CLAUDE.md +++ b/packages/player-core/CLAUDE.md @@ -6,7 +6,7 @@ Framework-agnostic playback engine used by both adapters and by the extension's - `pnpm build`: Vite (CSS inlined into JS, no `.css` output) + `dts-bundle-generator` for `dist/index.d.ts`. - **Keep `exports["."].default` pointing at `./dist/index.js`** (only `types` points at `src`). Resolving raw source forces every consumer's bundler to compile `.svelte` files, and the extension's build breaks. -- `@rustrak/openshowcase-schema` stays `external` in `vite.config.ts`. Inlining it bundles a second copy of zod. +- `@rustrak/openshowcase-schema` stays `external` in `vite.config.ts`. Inlining it bundles a second copy of valibot. - `pnpm size`: 35 KB brotli budget for `dist/index.js`. - `pnpm test`: Vitest projects `unit` (Node, for `core/`) and `component` (covers `__test__/component` + `__test__/integration` in real Chromium via Playwright, not jsdom). diff --git a/packages/player-core/vite.config.ts b/packages/player-core/vite.config.ts index a7e879d..ccc811d 100644 --- a/packages/player-core/vite.config.ts +++ b/packages/player-core/vite.config.ts @@ -23,9 +23,9 @@ export default defineConfig({ formats: ["es"], fileName: () => "index.js", }, - // The schema (and zod behind it) is a declared dependency, not something to inline: + // The schema (and valibot behind it) is a declared dependency, not something to inline: // every consumer already has it (the adapters import `parseDemo` from it), so bundling - // a private copy here only doubled zod in the host app. + // a private copy here only doubled valibot in the host app. rollupOptions: { external: ["@rustrak/openshowcase-schema"], }, diff --git a/packages/player-core/vitest.config.ts b/packages/player-core/vitest.config.ts index 104619c..18ffb2f 100644 --- a/packages/player-core/vitest.config.ts +++ b/packages/player-core/vitest.config.ts @@ -5,10 +5,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [tailwindcss(), svelte()], - // Pre-bundle zod up front — the integration test is the first to pull in @rustrak/openshowcase-schema's + // Pre-bundle valibot up front — the integration test is the first to pull in @rustrak/openshowcase-schema's // runtime code inside the browser project, and an on-demand optimize mid-run forces Vite // to reload the page, breaking whatever test was in flight. - optimizeDeps: { include: ["@rustrak/openshowcase-schema > zod"] }, + optimizeDeps: { include: ["@rustrak/openshowcase-schema > valibot"] }, test: { projects: [ { diff --git a/packages/schema/CLAUDE.md b/packages/schema/CLAUDE.md index 4251eba..2842ead 100644 --- a/packages/schema/CLAUDE.md +++ b/packages/schema/CLAUDE.md @@ -1,7 +1,8 @@ # @rustrak/openshowcase-schema -Zod schema + TS types for the demo format (`steps.json`). This is the source of truth for every other workspace. Its only runtime dependency is `zod` (3 KB brotli budget, `pnpm size`). +Valibot schema + TS types for the demo format (`steps.json`). This is the source of truth for every other workspace. Its only runtime dependency is `valibot` (3 KB brotli budget, `pnpm size`). Valibot was picked over Zod for the host app's bundle: see issue #4. - Bump `SCHEMA_VERSION` in `src/index.ts` on any breaking change to the `Demo`/`Step` shape. -- `parseDemo()` is the single validation point for untrusted JSON. Consumers rely on it, so don't add ad hoc validation elsewhere. +- `parseDemo()` is the single validation point for untrusted JSON. Consumers rely on it, so don't add ad hoc validation elsewhere. It throws `DemoParseError`, never the library's own error: that keeps the validator swappable without breaking the public API. +- `pnpm test`: Vitest in Node. `parse-demo.test.ts` pins the validation rules, so any change to a rule shows up there. - Changes ripple everywhere. Grep `packages/`, `adapters/` and `apps/` for a field before renaming or removing it. diff --git a/packages/schema/README.md b/packages/schema/README.md index 1138dc0..65af9a0 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -1,6 +1,6 @@ # @rustrak/openshowcase-schema -The [OpenShowcase](https://github.com/rustrak/openshowcase) demo format (`steps.json`): a Zod schema and TypeScript types. +The [OpenShowcase](https://github.com/rustrak/openshowcase) demo format (`steps.json`): a [Valibot](https://valibot.dev) schema and TypeScript types. ## Install @@ -16,7 +16,19 @@ import { parseDemo, type Demo } from "@rustrak/openshowcase-schema"; const demo: Demo = parseDemo(await (await fetch("/demos/my-demo/steps.json")).json()); ``` -`parseDemo` throws a `ZodError` if the data doesn't match the schema. +`parseDemo` throws a `DemoParseError` if the data doesn't match the schema. Its `issues` list each problem with the dot path to the offending value: + +```ts +import { DemoParseError, parseDemo } from "@rustrak/openshowcase-schema"; + +try { + parseDemo(json); +} catch (error) { + if (error instanceof DemoParseError) { + console.error(error.issues); // [{ path: "steps.1.hotspot.x", message: "..." }] + } +} +``` ## Format overview diff --git a/packages/schema/package.json b/packages/schema/package.json index 9a1dadb..30724d4 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,14 +1,14 @@ { "name": "@rustrak/openshowcase-schema", "version": "0.1.0", - "description": "Zod schema and TypeScript types for the OpenShowcase interactive demo format", + "description": "Valibot schema and TypeScript types for the OpenShowcase interactive demo format", "keywords": [ "openshowcase", "interactive-demo", "product-demo", "product-tour", "schema", - "zod" + "valibot" ], "license": "MIT", "author": "AbianS", @@ -41,10 +41,16 @@ "build": "rslib build", "dev": "rslib build --watch", "check-types": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", "size": "size-limit" }, - "dependencies": { - "zod": "4.6.5" + "devDependencies": { + "@rslib/core": "1.0.1", + "@size-limit/file": "14.0.0", + "size-limit": "14.0.0", + "typescript": "6.0.3", + "vitest": "5.0.1" }, "size-limit": [ { @@ -53,10 +59,7 @@ "limit": "3 KB" } ], - "devDependencies": { - "@rslib/core": "1.0.1", - "@size-limit/file": "14.0.0", - "size-limit": "14.0.0", - "typescript": "6.0.3" + "dependencies": { + "valibot": "1.5.0" } } diff --git a/packages/schema/rslib.config.ts b/packages/schema/rslib.config.ts index 0354add..668bdd8 100644 --- a/packages/schema/rslib.config.ts +++ b/packages/schema/rslib.config.ts @@ -1,9 +1,12 @@ import { defineConfig } from "@rslib/core"; -// Pure TS+zod library, no framework/CSS to process. autoExternal (default on) keeps `zod` +// Pure TS+valibot library, no framework/CSS to process. autoExternal (default on) keeps `valibot` // as a real `import` in the output rather than bundling it in — correct for a package meant -// to be published standalone, where the consumer brings their own zod. +// to be published standalone, where the consumer brings their own valibot. export default defineConfig({ + // Build-only tsconfig: tests live under src/ (so check-types sees them) but must not get + // emitted declarations in dist/. + source: { tsconfigPath: "./tsconfig.build.json" }, lib: [ { format: "esm", diff --git a/packages/schema/src/__test__/unit/parse-demo.test.ts b/packages/schema/src/__test__/unit/parse-demo.test.ts new file mode 100644 index 0000000..60c3b98 --- /dev/null +++ b/packages/schema/src/__test__/unit/parse-demo.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, it } from "vitest"; +import { type Demo, DemoParseError, parseDemo } from "../../index"; + +function createDemo(): Demo { + return { + id: "demo-1", + title: "Overview", + video: { + src: "assets/recording.webm", + width: 3000, + height: 1790, + durationSec: 12.5, + }, + theme: { wrapper: "browser", autoplay: true, appearance: "light" }, + steps: [ + { id: "step-1", type: "video", startTime: 1.23, endTime: 3.99 }, + { + id: "step-2", + type: "photo", + image: { src: "assets/step-1.webp", width: 3000, height: 1790 }, + hotspot: { + x: 0.16, + y: 0.32, + label: "Click the project", + bgColor: "#C5F11E", + textColor: "#0C0C0C", + position: "auto", + }, + panZoom: { x: 0.28, y: 0.32, scale: 1.8 }, + }, + { + id: "step-3", + type: "video", + startTime: 4, + endTime: 9, + playbackRate: 2, + panZoom: { x: 0.5, y: 0.5, scale: 1.5, duration: 800, easing: "fast" }, + }, + ], + }; +} + +type Mutable = Record & { steps: Record[] }; + +/** A deep copy of the demo, with `edit` applied, as untyped JSON. */ +function demoWith(edit: (demo: Mutable) => void): unknown { + const demo = JSON.parse(JSON.stringify(createDemo())) as Mutable; + edit(demo); + return demo; +} + +const photo = (demo: Mutable) => demo.steps[1] as Record; +const video = (demo: Mutable) => demo.steps[2] as Record; + +describe("parseDemo", () => { + it("returns a valid demo unchanged", () => { + expect(parseDemo(createDemo())).toEqual(createDemo()); + }); + + it("accepts a demo without a recording and with no steps", () => { + const demo = demoWith((d) => { + delete d.video; + d.steps = []; + }); + expect(parseDemo(demo)).toEqual(demo); + }); + + it("accepts a hotspot with only its position", () => { + const demo = demoWith((d) => { + photo(d).hotspot = { x: 0, y: 1 } as never; + }); + expect(parseDemo(demo)).toEqual(demo); + }); + + it("drops unknown keys", () => { + const demo = demoWith((d) => { + d.extra = 1; + photo(d).junk = true as never; + }); + expect(parseDemo(demo)).toEqual(createDemo()); + }); + + it.each<[string, (d: Mutable) => void]>([ + ["a missing title", (d) => delete d.title], + ["a numeric id", (d) => (d.id = 1)], + ["a missing theme", (d) => delete d.theme], + [ + "an unknown wrapper", + (d) => (d.theme = { ...(d.theme as object), wrapper: "phone" }), + ], + [ + "a non-boolean autoplay", + (d) => (d.theme = { ...(d.theme as object), autoplay: "yes" }), + ], + [ + "steps that aren't an array", + (d) => ((d as Record).steps = {}), + ], + [ + "a recording with zero duration", + (d) => (d.video = { ...(d.video as object), durationSec: 0 }), + ], + ["an unknown step type", (d) => (photo(d).type = "gif" as never)], + [ + "an image with zero width", + (d) => (photo(d).image = { src: "a.webp", width: 0, height: 1 } as never), + ], + [ + "a hotspot x above 1", + (d) => (photo(d).hotspot = { x: 1.5, y: 0.5 } as never), + ], + [ + "a hotspot y below 0", + (d) => (photo(d).hotspot = { x: 0.5, y: -0.1 } as never), + ], + [ + "a NaN hotspot coordinate", + (d) => (photo(d).hotspot = { x: Number.NaN, y: 0.5 } as never), + ], + [ + "an unknown hotspot position", + (d) => + (photo(d).hotspot = { x: 0.5, y: 0.5, position: "middle" } as never), + ], + [ + "a zoom scale below 1", + (d) => (photo(d).panZoom = { x: 0.5, y: 0.5, scale: 0.5 } as never), + ], + [ + "a zero zoom duration", + (d) => + (photo(d).panZoom = { x: 0.5, y: 0.5, scale: 2, duration: 0 } as never), + ], + [ + "an unknown zoom easing", + (d) => + (photo(d).panZoom = { + x: 0.5, + y: 0.5, + scale: 2, + easing: "bounce", + } as never), + ], + ["a negative clip start", (d) => (video(d).startTime = -1)], + ["a zero clip end", (d) => (video(d).endTime = 0)], + [ + "an infinite clip end", + (d) => (video(d).endTime = Number.POSITIVE_INFINITY), + ], + ["a zero playback rate", (d) => (video(d).playbackRate = 0)], + ])("rejects %s", (_, edit) => { + expect(() => parseDemo(demoWith(edit))).toThrow(DemoParseError); + }); + + it("rejects null", () => { + expect(() => parseDemo(null)).toThrow(DemoParseError); + }); +}); + +describe("DemoParseError", () => { + it("is an Error that lists every issue with its path", () => { + const data = demoWith((d) => { + d.title = 42; + photo(d).hotspot = { x: 1.5, y: 0.5 } as never; + }); + + let error: unknown; + try { + parseDemo(data); + } catch (caught) { + error = caught; + } + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(DemoParseError); + const { name, issues } = error as DemoParseError; + expect(name).toBe("DemoParseError"); + expect(issues.map((issue) => issue.path)).toEqual([ + "title", + "steps.1.hotspot.x", + ]); + for (const issue of issues) expect(issue.message).not.toBe(""); + }); + + it("names the failing path in its message", () => { + expect(() => parseDemo(demoWith((d) => (video(d).endTime = 0)))).toThrow( + /steps\.2\.endTime/, + ); + }); +}); diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 6cb0838..bd6b7fc 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,44 +1,47 @@ -import { z } from "zod"; +import * as v from "valibot"; -const frac = z.number().min(0).max(1); +// `v.number()` accepts Infinity; the demo format never has an unbounded number +const number = () => v.pipe(v.number(), v.finite()); +const positive = () => v.pipe(v.number(), v.finite(), v.gtValue(0)); +const frac = v.pipe(v.number(), v.minValue(0), v.maxValue(1)); -export const ImageAssetSchema = z.object({ - src: z.string(), - width: z.number().positive(), - height: z.number().positive(), +export const ImageAssetSchema = v.object({ + src: v.string(), + width: positive(), + height: positive(), }); -export type ImageAsset = z.infer; +export type ImageAsset = v.InferOutput; -export const VideoAssetSchema = z.object({ - src: z.string(), - width: z.number().positive(), - height: z.number().positive(), - durationSec: z.number().positive(), +export const VideoAssetSchema = v.object({ + src: v.string(), + width: positive(), + height: positive(), + durationSec: positive(), }); -export type VideoAsset = z.infer; +export type VideoAsset = v.InferOutput; -export const HotspotPositionSchema = z.enum([ +export const HotspotPositionSchema = v.picklist([ "auto", "top", "bottom", "left", "right", ]); -export type HotspotPosition = z.infer; +export type HotspotPosition = v.InferOutput; /** * Pulsing hotspot: fractional coordinates (0-1) over the image, with its own tooltip * (label) and style. The demo pauses until the user clicks it. */ -export const HotspotSchema = z.object({ +export const HotspotSchema = v.object({ x: frac, y: frac, - label: z.string().optional(), - bgColor: z.string().optional(), - textColor: z.string().optional(), - position: HotspotPositionSchema.optional(), + label: v.optional(v.string()), + bgColor: v.optional(v.string()), + textColor: v.optional(v.string()), + position: v.optional(HotspotPositionSchema), }); -export type Hotspot = z.infer; +export type Hotspot = v.InferOutput; export const defaultHotspotStyle = { // Rustrak lime (#C5F11E, the brand OpenShowcase shares) with near-black text on it @@ -54,45 +57,45 @@ export const defaultHotspotStyle = { * the scale, with the origin always fixed at 50%/50%. That way going from one zoom to another * (or to "no zoom") is a single continuous `transform` transition. */ -export const PanZoomEasingSchema = z.enum([ +export const PanZoomEasingSchema = v.picklist([ "smooth", "cinematic", "fast", "linear", ]); -export type PanZoomEasing = z.infer; +export type PanZoomEasing = v.InferOutput; -export const PanZoomSchema = z.object({ +export const PanZoomSchema = v.object({ x: frac, y: frac, - scale: z.number().min(1), + scale: v.pipe(number(), v.minValue(1)), /** Transition duration (ms). Defaults to `defaultPanZoomTiming.duration` when unset. */ - duration: z.number().positive().optional(), + duration: v.optional(positive()), /** Named easing preset (mapped to a CSS timing function in player-core). Defaults to * `defaultPanZoomTiming.easing` when unset. */ - easing: PanZoomEasingSchema.optional(), + easing: v.optional(PanZoomEasingSchema), }); -export type PanZoom = z.infer; +export type PanZoom = v.InferOutput; export const defaultPanZoomTiming = { duration: 1000, easing: "cinematic", } as const satisfies Partial; -export const ThemeWrapperSchema = z.enum(["none", "browser"]); -export type ThemeWrapper = z.infer; +export const ThemeWrapperSchema = v.picklist(["none", "browser"]); +export type ThemeWrapper = v.InferOutput; -export const ThemeAppearanceSchema = z.enum(["light", "dark"]); -export type ThemeAppearance = z.infer; +export const ThemeAppearanceSchema = v.picklist(["light", "dark"]); +export type ThemeAppearance = v.InferOutput; -export const ThemeSchema = z.object({ +export const ThemeSchema = v.object({ wrapper: ThemeWrapperSchema, /** After the last step, restart the demo from the beginning instead of stopping. */ - autoplay: z.boolean(), + autoplay: v.boolean(), /** Colors of the wrapper and the player controls — to match the light/dark mode of the embedding site. */ appearance: ThemeAppearanceSchema, }); -export type Theme = z.infer; +export type Theme = v.InferOutput; export const defaultTheme: Theme = { wrapper: "browser", @@ -100,43 +103,68 @@ export const defaultTheme: Theme = { appearance: "light", }; -export const PhotoStepSchema = z.object({ - id: z.string(), - type: z.literal("photo"), +export const PhotoStepSchema = v.object({ + id: v.string(), + type: v.literal("photo"), image: ImageAssetSchema, - hotspot: HotspotSchema.optional(), - panZoom: PanZoomSchema.optional(), + hotspot: v.optional(HotspotSchema), + panZoom: v.optional(PanZoomSchema), }); -export type PhotoStep = z.infer; +export type PhotoStep = v.InferOutput; /** A trim (in/out in seconds) of the demo's continuous recording (`Demo.video`). */ -export const VideoStepSchema = z.object({ - id: z.string(), - type: z.literal("video"), - startTime: z.number().nonnegative(), - endTime: z.number().positive(), - playbackRate: z.number().positive().optional(), - panZoom: PanZoomSchema.optional(), +export const VideoStepSchema = v.object({ + id: v.string(), + type: v.literal("video"), + startTime: v.pipe(number(), v.minValue(0)), + endTime: positive(), + playbackRate: v.optional(positive()), + panZoom: v.optional(PanZoomSchema), }); -export type VideoStep = z.infer; +export type VideoStep = v.InferOutput; -export const StepSchema = z.discriminatedUnion("type", [ - PhotoStepSchema, - VideoStepSchema, -]); -export type Step = z.infer; +export const StepSchema = v.variant("type", [PhotoStepSchema, VideoStepSchema]); +export type Step = v.InferOutput; -export const DemoSchema = z.object({ - id: z.string(), - title: z.string(), - video: VideoAssetSchema.optional(), +export const DemoSchema = v.object({ + id: v.string(), + title: v.string(), + video: v.optional(VideoAssetSchema), theme: ThemeSchema, - steps: z.array(StepSchema), + steps: v.array(StepSchema), }); -export type Demo = z.infer; +export type Demo = v.InferOutput; + +export interface DemoIssue { + /** Dot path to the offending value, e.g. `steps.1.hotspot.x` (empty for the root). */ + path: string; + message: string; +} + +/** Thrown by `parseDemo` when the data doesn't match the demo format. */ +export class DemoParseError extends Error { + override readonly name = "DemoParseError"; + readonly issues: DemoIssue[]; + + constructor(issues: DemoIssue[]) { + const lines = issues.map( + (issue) => `- ${issue.path || "(root)"}: ${issue.message}`, + ); + super(`Invalid demo:\n${lines.join("\n")}`); + this.issues = issues; + } +} +/** Validates untrusted data (e.g. a fetched `steps.json`) as a `Demo`, dropping unknown keys. */ export function parseDemo(data: unknown): Demo { - return DemoSchema.parse(data); + const result = v.safeParse(DemoSchema, data); + if (result.success) return result.output; + throw new DemoParseError( + result.issues.map((issue) => ({ + path: v.getDotPath(issue) ?? "", + message: issue.message, + })), + ); } export const SCHEMA_VERSION = 6 as const; diff --git a/packages/schema/tsconfig.build.json b/packages/schema/tsconfig.build.json new file mode 100644 index 0000000..6079c7f --- /dev/null +++ b/packages/schema/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/**/__test__"] +} diff --git a/packages/schema/vitest.config.ts b/packages/schema/vitest.config.ts new file mode 100644 index 0000000..5dc7374 --- /dev/null +++ b/packages/schema/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/__test__/unit/**/*.test.ts"], + environment: "node", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e169fe..1c0582e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,9 +435,9 @@ importers: packages/schema: dependencies: - zod: - specifier: 4.6.5 - version: 4.6.5 + valibot: + specifier: 1.5.0 + version: 1.5.0(typescript@6.0.3) devDependencies: '@rslib/core': specifier: 1.0.1 @@ -451,6 +451,9 @@ importers: typescript: specifier: 6.0.3 version: 6.0.3 + vitest: + specifier: 5.0.1 + version: 5.0.1(@types/node@26.6.2)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(vite@8.3.0(@types/node@26.6.2)) packages: @@ -2348,6 +2351,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@1.5.0: + resolution: {integrity: sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite-plugin-css-injected-by-js@5.0.2: resolution: {integrity: sha512-Hvg48QfYsCL53ju9T5zTC0Ofg90XLa8t5zhhKmPYbayoMQieNcQDkkRX8pv4TAcKPZwVisLFiwDy5uBjOn+3Ow==} peerDependencies: @@ -2529,9 +2540,6 @@ packages: zimmerframe@1.1.5: resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} - zod@4.6.5: - resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} - snapshots: '@1natsu/wait-element@4.2.0': @@ -4212,6 +4220,10 @@ snapshots: util-deprecate@1.0.2: {} + valibot@1.5.0(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vite-plugin-css-injected-by-js@5.0.2(vite@8.3.0(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.9.1)): dependencies: vite: 8.3.0(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.9.1) @@ -4359,5 +4371,3 @@ snapshots: yargs-parser: 21.1.1 zimmerframe@1.1.5: {} - - zod@4.6.5: {}