diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx
index e6ab451..728359d 100644
--- a/examples/react/src/App.tsx
+++ b/examples/react/src/App.tsx
@@ -1,6 +1,6 @@
-import './App.css';
-import { z } from 'zod';
-import { schema, useSnapshot } from '../../../src/index';
+import './App.css'
+import { z } from 'zod'
+import { schema, useSnapshot } from '../../../src/index'
const userSchema = z.object({
username: z.string(),
@@ -10,10 +10,10 @@ const userSchema = z.object({
lastName: z.string(),
address: z.object({
city: z.string(),
- country: z.string(),
- }),
- }),
-});
+ country: z.string()
+ })
+ })
+})
const userState = schema(userSchema).proxy(
{
@@ -24,77 +24,75 @@ const userState = schema(userSchema).proxy(
lastName: 'Smith',
address: {
city: 'Wonderland',
- country: 'Fantasy',
- },
- },
+ country: 'Fantasy'
+ }
+ }
},
- { safeParse: true, errorHandler: (e) => console.log(e.message) },
-);
+ { safeParse: true, errorHandler: (e) => console.log(e.message) }
+)
function App() {
- const user = useSnapshot(userState);
+ const user = useSnapshot(userState)
return (
Vite + React
-
+
(userState.username = e.target.value)}
/>
Username: {user.username}
-
+
(userState.age = Number(e.target.value))}
+ onChange={(e) => (userState.age = e.target.value)}
/>
Age: {user.age}
-
+
(userState.profile.firstName = e.target.value)}
/>
First Name: {user.profile.firstName}
-
+
(userState.profile.lastName = e.target.value)}
/>
Last Name: {user.profile.lastName}
-
+
(userState.profile.address.city = e.target.value)}
/>
City: {user.profile.address.city}
-
+
- (userState.profile.address.country = Number(e.target.value))
- }
+ onChange={(e) => (userState.profile.address.country = e.target.value)}
/>
Last Name: {user.profile.address.country}
- );
+ )
}
-export default App;
+export default App
diff --git a/examples/react/src/test.tsx b/examples/react/src/test.tsx
new file mode 100644
index 0000000..dffee3a
--- /dev/null
+++ b/examples/react/src/test.tsx
@@ -0,0 +1,6 @@
+import { ImageMetadataSchema, ImageMetadata } from './types'
+import { schema } from '../../../src/index'
+
+const store = schema(ImageMetadataSchema).proxy({
+ base64: null
+})
diff --git a/examples/react/src/types.ts b/examples/react/src/types.ts
new file mode 100644
index 0000000..fd76bc4
--- /dev/null
+++ b/examples/react/src/types.ts
@@ -0,0 +1,48 @@
+import { z } from 'zod'
+
+export type ImageMetadataType = {
+ base64: string
+ filename: string
+ tags: string[]
+ title: string
+ contentType: string
+ width: number
+ height: number
+}
+
+export type SelectFieldOption = { label: string; value: string }
+
+// Base64 validation regex
+const base64Regex =
+ /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
+
+// Content type validation regex (matches any MIME type starting with 'image/')
+const contentTypeRegex = /^image\/[a-zA-Z0-9+\.-]+$/
+
+// Filename validation regex (simple validation to exclude invalid filename characters)
+const filenameRegex = /^[^<>:"/\\|?*\x00-\x1F]+$/
+
+// Define the ImageMetadata schema
+export const ImageMetadataSchema = z.object({
+ base64: z.string().regex(base64Regex, { message: 'Invalid image data' }),
+ filename: z
+ .string({ required_error: 'Filename cannot be empty' })
+ .regex(filenameRegex, { message: 'Filename contains invalid characters' }),
+ tags: z.array(z.string({ required_error: 'Tags cannot be empty strings' })),
+ title: z.string({ required_error: 'Title cannot be empty' }),
+ contentType: z.string().regex(contentTypeRegex, {
+ message:
+ 'Content type must start with "image/" and contain valid characters'
+ }),
+ width: z
+ .number()
+ .int({ message: 'Width must be an integer' })
+ .positive({ message: 'Width must be a positive number' }),
+ height: z
+ .number()
+ .int({ message: 'Height must be an integer' })
+ .positive({ message: 'Height must be a positive number' })
+})
+
+// Export the TypeScript type inferred from the schema
+export type ImageMetadata = z.infer
diff --git a/src/index.ts b/src/index.ts
index 16c37f3..9a86fda 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,83 +1,86 @@
/* eslint-disable */
-import { z, ZodType } from 'zod';
-import { proxy as vproxy, useSnapshot as vsnap, getVersion } from 'valtio';
-import _ from 'lodash';
+import { z, ZodType } from 'zod'
+import { proxy as vproxy, useSnapshot as vsnap, getVersion } from 'valtio'
+import _ from 'lodash'
+import type { O } from 'vitest/dist/reporters-yx5ZTtEV.js'
type ValtioProxy = {
- [P in keyof T]: T[P];
-};
+ [P in keyof T]: T[P]
+}
type SchemaConfig = {
- parseAsync?: boolean;
- safeParse?: boolean;
- errorHandler?: (error: unknown) => void;
-};
+ parseAsync?: boolean
+ safeParse?: boolean
+ errorHandler?: (error: unknown) => void
+}
const defaultConfig = {
parseAsync: false,
safeParse: false,
- errorHandler: (error: unknown) => console.error(error),
-};
+ errorHandler: (error: unknown) => console.error(error)
+}
export const vzGlobalConfig = {
safeParse: false,
- errorHandler: (error: unknown) => console.error(error),
-};
+ errorHandler: (error: unknown) => console.error(error)
+}
const isObject = (x: unknown): x is object =>
- typeof x === 'object' && x !== null;
+ typeof x === 'object' && x !== null
-type MergedConfig = Required;
+type MergedConfig = Required
type SchemaMeta = SchemaConfig & {
- initialState: unknown;
-};
+ initialState: unknown
+}
-type PropType = string | number | symbol;
-const schemaMeta = new WeakMap, SchemaMeta>();
-const pathList = new WeakMap<{}, PropType[]>();
-const isProxySymbol = Symbol('isProxy');
+type PropType = string | number | symbol
+const schemaMeta = new WeakMap, SchemaMeta>()
+const pathList = new WeakMap<{}, PropType[]>()
+const isProxySymbol = Symbol('isProxy')
type SchemaReturn> = {
proxy: {
- (initialState: any, config?: SchemaConfig): ValtioProxy>;
- };
-};
+ (initialState: any, config?: SchemaConfig): ValtioProxy>
+ }
+}
-const valtioStoreSymbol = Symbol('valtioStore');
+const valtioStoreSymbol = Symbol('valtioStore')
export const useSnapshot = (store: any) => {
- const valtioStore = store[valtioStoreSymbol];
- return vsnap(valtioStore[valtioStoreSymbol]);
-};
+ const valtioStore = store[valtioStoreSymbol]
+ return vsnap(valtioStore[valtioStoreSymbol])
+}
export const schema = >(
- zodSchema: T,
-): SchemaReturn => {
- const proxy = (
- initialState: z.infer,
- config: SchemaConfig = {},
- ): ValtioProxy> => {
+ zodSchema: T
+): {
+ proxy: (initialState: O, config?: SchemaConfig) => O
+} => {
+ const proxy = (
+ initialState: O,
+ config: SchemaConfig = {}
+ ): O => {
if (!isObject(initialState)) {
- throw new Error('object required');
+ throw new Error('object required')
}
- const mergedConfig: MergedConfig = { ...defaultConfig, ...config };
+ const mergedConfig: MergedConfig = { ...defaultConfig, ...config }
- const parseAsync = mergedConfig.parseAsync;
- const safeParse = mergedConfig.safeParse;
- const errorHandler = mergedConfig.errorHandler;
+ const parseAsync = mergedConfig.parseAsync
+ const safeParse = mergedConfig.safeParse
+ const errorHandler = mergedConfig.errorHandler
// before proxying, validate the initial state
if (parseAsync) {
zodSchema.parseAsync(initialState).catch((e) => {
- throw e;
- });
+ throw e
+ })
} else {
- zodSchema.parse(initialState);
+ zodSchema.parse(initialState)
}
- const valtioProxy = vproxy(initialState);
+ const valtioProxy = vproxy(initialState)
const createProxy = (target: any, parentPath: PropType[] = []): any => {
if (!schemaMeta.has(zodSchema)) {
@@ -85,100 +88,100 @@ export const schema = >(
safeParse,
parseAsync,
errorHandler,
- initialState,
- });
+ initialState
+ })
}
return new Proxy(target, {
get(target, prop, receiver) {
- const value = Reflect.get(target, prop, receiver);
+ const value = Reflect.get(target, prop, receiver)
if (isObject(value)) {
if ((value as any)[isProxySymbol]) {
- return value;
+ return value
} else {
- const newPath = parentPath.concat(prop);
- pathList.set(value, newPath);
- const proxyObj = createProxy(value, newPath);
- proxyObj[isProxySymbol] = true;
- return proxyObj;
+ const newPath = parentPath.concat(prop)
+ pathList.set(value, newPath)
+ const proxyObj = createProxy(value, newPath)
+ proxyObj[isProxySymbol] = true
+ return proxyObj
}
} else {
- const pathToGet = [...(pathList.get(target) || []), prop];
- return _.get(valtioProxy, pathToGet, value);
+ const pathToGet = [...(pathList.get(target) || []), prop]
+ return _.get(valtioProxy, pathToGet, value)
}
},
set(target, prop, value, receiver) {
const originalObject = schemaMeta.get(zodSchema)!
- .initialState as z.infer;
+ .initialState as z.infer
- const objectToValidate = _.cloneDeep(originalObject);
- const pathToSet = [...(pathList.get(target) || []), prop];
+ const objectToValidate = _.cloneDeep(originalObject)
+ const pathToSet = [...(pathList.get(target) || []), prop]
- _.set(objectToValidate, pathToSet, value);
+ _.set(objectToValidate, pathToSet, value)
const handleAsyncParse = async () => {
try {
- const parsedValue = await zodSchema.parseAsync(objectToValidate);
- _.set(valtioProxy, pathToSet, value);
- Reflect.set(target, prop, value, receiver);
- return true;
+ const parsedValue = await zodSchema.parseAsync(objectToValidate)
+ _.set(valtioProxy, pathToSet, value)
+ Reflect.set(target, prop, value, receiver)
+ return true
} catch (error) {
- errorHandler(error);
+ errorHandler(error)
if (!safeParse) {
- throw error;
+ throw error
}
- return true;
+ return true
}
- };
+ }
const handleSyncParse = () => {
try {
if (safeParse) {
- const result = zodSchema.safeParse(objectToValidate);
+ const result = zodSchema.safeParse(objectToValidate)
if (result.success) {
- _.set(valtioProxy, pathToSet, value);
- Reflect.set(target, prop, value, receiver);
- return true;
+ _.set(valtioProxy, pathToSet, value)
+ Reflect.set(target, prop, value, receiver)
+ return true
} else {
- errorHandler(result.error);
+ errorHandler(result.error)
// need to return true here to prevent an error from being thrown
// ifrom the proxy not updating the value
- return true;
+ return true
}
} else {
- const parsedValue = zodSchema.parse(objectToValidate);
- _.set(valtioProxy, pathToSet, value);
- Reflect.set(target, prop, value, receiver);
- return true;
+ const parsedValue = zodSchema.parse(objectToValidate)
+ _.set(valtioProxy, pathToSet, value)
+ Reflect.set(target, prop, value, receiver)
+ return true
}
} catch (error) {
- errorHandler(error);
+ errorHandler(error)
if (!safeParse) {
- throw error;
+ throw error
}
- return true;
+ return true
}
- };
+ }
if (parseAsync) {
handleAsyncParse().catch((error) => {
- errorHandler(error);
+ errorHandler(error)
if (!safeParse) {
- throw error;
+ throw error
}
- });
- return true;
+ })
+ return true
} else {
- return handleSyncParse();
+ return handleSyncParse()
}
- },
- });
- };
+ }
+ })
+ }
- const store = createProxy(valtioProxy);
- store[valtioStoreSymbol] = valtioProxy;
+ const store = createProxy(valtioProxy)
+ store[valtioStoreSymbol] = valtioProxy
- return store;
- };
- return { proxy };
-};
+ return store
+ }
+ return { proxy }
+}