Skip to content

Commit

Permalink
Rework to use dynamic import for zod library (#38)
Browse files Browse the repository at this point in the history
* Rework to use dynamic import for zod library

* Set build target to es2022

* Move z declaration in eval expression

* Add picker

* Clean imports

* Add version to search params

* Rework versions fetch

* Add types version select and auto set default

* Rework tag lookup

* Replace eval with 'new Function(...)'

* Remove unnecessary type exports

* Remove match-sorter npm package (not in use)

* Remove zod and zor-error npm packages (not in use)

* Rework zod.ts to avoid class usage

* Fix typos

* Add types declaration update on version change

* Remove support for zod versions 1.x

* Refactor monaco types declaration set function

* Refactor block scope monaco instance

* Refactor zod.ts block scope variables

* Fix update version logic

* Change promise monaco init

* Refactor useEffect

---------

Co-authored-by: Marco Ilari <[email protected]>
  • Loading branch information
davc0n and marilari88 authored Jun 6, 2024
1 parent 5ae22c6 commit 3a13dd6
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 154 deletions.
22 changes: 1 addition & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
"monaco-editor": "^0.49.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.2.0",
"zod": "^3.23.8",
"zod-error": "^1.5.0"
"react-icons": "^5.2.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
Expand Down
99 changes: 52 additions & 47 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import {
ActionIcon,
Badge,
Box,
Button,
Flex,
Tooltip,
useMantineTheme,
} from '@mantine/core'
import {notifications} from '@mantine/notifications'
import Editor, {loader} from '@monaco-editor/react'
import Editor, {Monaco, loader, useMonaco} from '@monaco-editor/react'
import LZString from 'lz-string'
import {editor} from 'monaco-editor'
import {useState} from 'react'
import {useEffect, useState} from 'react'
import {FiAlertCircle, FiLink} from 'react-icons/fi'
import {LuEraser} from 'react-icons/lu'
import {ZodSchema, z} from 'zod'

import zodTypes from '../node_modules/zod/lib/types.d.ts?raw'
import {dependencies} from '../package.json'
import classes from './App.module.css'
import {CopyButton} from './features/CopyButton'
import {Validation} from './features/ValueEditor/ValueEditor'
import {AppData, appDataSchema} from './types'
import {VersionPicker} from './features/VersionPicker/VersionPicker'
import {Header} from './ui/Header/Header'
import * as zod from './zod'

const ZOD_VERSION = dependencies.zod.split('^')[1]
type AppData = {
schema: string
values: string[]
version: string
}

const ZOD_DEFAULT_VERSION = (await zod.getVersions('latest'))[0]

const editorOptions: editor.IStandaloneEditorConstructionOptions = {
minimap: {enabled: false},
Expand All @@ -46,14 +49,10 @@ const editorOptions: editor.IStandaloneEditorConstructionOptions = {
renderLineHighlight: 'none',
}

loader.init().then((monaco) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(
`declare namespace z{${zodTypes}}`,
)
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: true,
})
const monaco = await loader.init()
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: true,
})

const getAppDataFromSearchParams = (): AppData => {
Expand All @@ -64,13 +63,14 @@ const getAppDataFromSearchParams = (): AppData => {
return {
schema: '',
values: [],
version: ZOD_DEFAULT_VERSION,
}

const decompressedAppData =
LZString.decompressFromEncodedURIComponent(compressedAppData)
const appData = JSON.parse(decompressedAppData)

return appDataSchema.parse(appData)
return appData
}

const getURLwithAppData = (appData: AppData): string => {
Expand All @@ -83,24 +83,6 @@ const getURLwithAppData = (appData: AppData): string => {
return `${window.location.protocol}//${window.location.host}?${queryParams}`
}

const schemaSchema = z
.string()
.min(2)
.transform((s, ctx) => {
try {
return eval(s)
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Invalid schema'

ctx.addIssue({
code: z.ZodIssueCode.custom,
message: errorMessage,
})
return z.NEVER
}
})
.pipe(z.custom<ZodSchema>())

const sampleZodSchema = `z.object({
name: z.string(),
birth_year: z.number().optional()
Expand All @@ -110,15 +92,14 @@ const sampleValue = '{name: "John"}'

const appData = getAppDataFromSearchParams()

const evaluateSchema = (schema: string) => {
try {
const evaluatedSchema = schemaSchema.parse(schema)
return {evaluatedSchema}
} catch (e) {
if (e instanceof z.ZodError) return {error: e.message}
const setMonacoDeclarationTypes = async (monaco: Monaco, ver: string) => {
const declarationTypes = await zod.getDeclarationTypes(ver)

return {error: 'Invalid schema'}
}
monaco.languages.typescript.typescriptDefaults.setExtraLibs([
{
content: `declare namespace z{${declarationTypes}}`,
},
])
}

const App = () => {
Expand All @@ -130,10 +111,29 @@ const App = () => {
return appData.values.length ? appData.values : [sampleValue]
})

const {evaluatedSchema, error: schemaError} = evaluateSchema(schema)
const [isLoading, setIsLoading] = useState<boolean>(false)

const [version, setVersion] = useState(appData.version || ZOD_DEFAULT_VERSION)

const monaco = useMonaco()

const {data: evaluatedSchema, error: schemaError} = zod.validateSchema(schema)

const theme = useMantineTheme()

useEffect(() => {
if (isLoading || !monaco) return

async function updateVersion(monaco: Monaco, ver: string) {
setIsLoading(true)
await zod.setVersion(ver)
await setMonacoDeclarationTypes(monaco, ver)
setIsLoading(false)
}

updateVersion(monaco, version)
}, [version, monaco])

return (
<Box className={classes.layout}>
<Header>
Expand All @@ -147,6 +147,7 @@ const App = () => {
const urlWithAppData = getURLwithAppData({
schema,
values: values.filter((value) => typeof value == 'string'),
version,
})
navigator.clipboard.writeText(urlWithAppData)
notifications.show({
Expand All @@ -172,9 +173,13 @@ const App = () => {
>
<Flex gap="sm" align="center" flex={1}>
Zod schema
<Badge variant="default" size="lg" tt="none">
v{ZOD_VERSION}
</Badge>
<VersionPicker
value={version}
onChange={async (ver) => {
setVersion(ver)
}}
disabled={isLoading}
/>
<Button
rel="noopener noreferrer"
target="_blank"
Expand Down
74 changes: 4 additions & 70 deletions src/features/ValueEditor/ValueEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,75 +21,9 @@ import {
FiPlus,
} from 'react-icons/fi'
import {LuEraser} from 'react-icons/lu'
import {ZodSchema, z} from 'zod'
import {generateErrorMessage} from 'zod-error'
import {CopyButton} from '../CopyButton'
import classes from './ValueEditor.module.css'

const getErrorMessage = (e: unknown) => {
if (e instanceof SyntaxError) {
return 'Invalid syntax'
}

if (e instanceof ReferenceError) {
return 'Invalid reference'
}

return 'Invalid value'
}

const evaluateExpression = (expression: string) => {
try {
const evaluatedExpression = new Function(`return ${expression}`)()
return {success: true, data: evaluatedExpression} as const
} catch (e) {
return {
success: false,
error: getErrorMessage(e),
} as const
}
}

type ValidationResult =
| {
success: false
error: string
}
| {
success: true
parsedData: any
}

const validateValue = (
schema: ZodSchema<any, z.ZodTypeDef, any> | undefined,
value: string,
): ValidationResult => {
if (!schema) {
return {
success: false,
error: 'Invalid Schema',
}
}

const evaluatedValue = evaluateExpression(value)
if (!evaluatedValue.success) return evaluatedValue

try {
const validationRes = schema.safeParse(evaluatedValue.data)

return validationRes.success
? ({success: true, parsedData: validationRes.data} as const)
: ({
success: false,
error: generateErrorMessage(validationRes.error.issues),
} as const)
} catch (e) {
return {
success: false,
error: 'Cannot validate value. Please check the schema',
}
}
}
import * as zod from '../../zod'

const editorOptions: editor.IStandaloneEditorConstructionOptions = {
minimap: {enabled: false},
Expand All @@ -114,7 +48,7 @@ const editorOptions: editor.IStandaloneEditorConstructionOptions = {
}

interface Props {
schema: ZodSchema<any, z.ZodTypeDef, any> | undefined
schema: any
value: string
index: number
onAdd: () => void
Expand All @@ -135,8 +69,8 @@ export const Validation = ({
const [opened, {close, open}] = useDisclosure(false)
const [openedResult, {toggle: toggleResult}] = useDisclosure(false)

const validation = validateValue(schema, value)
const parsedData = validation.success && JSON.stringify(validation.parsedData)
const validation = zod.validateValue(schema, value)
const parsedData = validation.success && JSON.stringify(validation.data)

const errors = !validation.success && JSON.stringify(validation.error)

Expand Down
Loading

0 comments on commit 3a13dd6

Please sign in to comment.