Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@
"unist-util-stringify-position": "^4.0.0",
"unist-util-visit": "^5.0.0",
"ws": "^8.18.3",
"zod": "^3.25.75",
"zod-to-json-schema": "^3.24.6"
"zod": "^4.1.3"
},
"peerDependencies": {
"@electric-sql/pglite": "*",
"@libsql/client": "*",
"better-sqlite3": "^12.2.0",
"sqlite3": "*"
"sqlite3": "*",
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"@electric-sql/pglite": {
Expand All @@ -123,6 +123,9 @@
},
"better-sqlite3": {
"optional": true
},
"zod": {
"optional": true
}
},
"devDependencies": {
Expand All @@ -138,7 +141,6 @@
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "^1.2.19",
"@types/micromatch": "^4.0.9",
"@types/minimatch": "^6.0.0",
"@types/node": "^24.2.0",
"@types/pg": "^8.15.5",
"@types/ws": "^8.18.1",
Expand Down
4 changes: 2 additions & 2 deletions playground/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ const data = defineCollection({
domain: z.array(z.string()),
tutorial: z.array(
z.record(
z.string(),
z.object({
name: z.string(),
type: z.string(),
born: z.number(),
}),
),
),
)),
author: z.string(),
published: z.boolean(),
}),
Expand Down
1,145 changes: 684 additions & 461 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/types/collection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ZodObject, ZodRawShape } from 'zod'
import type { ZodObject, ZodRawShape } from 'zod/v4'
import type { Draft07 } from '../types/schema'
import type { MarkdownRoot } from './content'

Expand Down
5 changes: 3 additions & 2 deletions src/utils/collection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ZodRawShape } from 'zod'
import type { ZodRawShape } from 'zod/v4'
import { hash } from 'ohash'
import type { Collection, ResolvedCollection, CollectionSource, DefinedCollection, ResolvedCollectionSource, CustomCollectionSource, ResolvedCustomCollectionSource } from '../types/collection'
import { getOrderedSchemaKeys, describeProperty, getCollectionFieldsTypes } from '../runtime/internal/schema'
Expand Down Expand Up @@ -161,7 +161,8 @@ export function generateCollectionInsert(collection: ResolvedCollection, data: P
values.push(Number(valueToInsert))
}
else if (property?.sqlType === 'DATE') {
values.push(`'${new Date(valueToInsert as string).toISOString()}'`)
const dateValue = valueToInsert instanceof Date ? valueToInsert : new Date(valueToInsert as string)
values.push(`'${dateValue.toISOString()}'`)
}
else if (property?.enum) {
values.push(`'${String(valueToInsert).replace(/\n/g, '\\n').replace(/'/g, '\'\'')}'`)
Expand Down
2 changes: 1 addition & 1 deletion src/utils/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as z from 'zod'
import * as z from 'zod/v4'
import { ContentFileExtension } from '../types/content'
import type { Draft07 } from '../types'

Expand Down
143 changes: 101 additions & 42 deletions src/utils/zod.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,127 @@
import type { ZodOptionalDef, ZodType } from 'zod'
import { zodToJsonSchema, ignoreOverride } from 'zod-to-json-schema'
import { z as zod } from 'zod'
import { createDefu } from 'defu'
import type { Draft07, EditorOptions } from '../types'
import type { ZodType } from 'zod/v4'
import { z as zod } from 'zod/v4'
import type {
Draft07,
EditorOptions,
Draft07DefinitionProperty,
Draft07DefinitionPropertyAnyOf,
Draft07DefinitionPropertyAllOf,
} from '../types'

const defu = createDefu((obj, key, value) => {
if (Array.isArray(obj[key]) && Array.isArray(value)) {
obj[key] = value
return true
}
})

declare module 'zod' {
interface ZodTypeDef {
declare module 'zod/v4' {
interface GlobalMeta {
editor?: EditorOptions
}

interface ZodType {
editor(options: EditorOptions): this
}
}

export type ZodFieldType = 'ZodString' | 'ZodNumber' | 'ZodBoolean' | 'ZodDate' | 'ZodEnum'
export type ZodFieldType
= | 'ZodString'
| 'ZodNumber'
| 'ZodBoolean'
| 'ZodDate'
| 'ZodEnum'
export type SqlFieldType = 'VARCHAR' | 'INT' | 'BOOLEAN' | 'DATE' | 'TEXT'

// Loose helper type to silence any usage only at this augmentation point.
// We intentionally keep it minimal to avoid leaking `any` elsewhere.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(zod.ZodType as any).prototype.editor = function (options: EditorOptions) {
this._def.editor = { ...this._def.editor, ...options }
return this
type ZodAny = any

;(zod.ZodType as unknown as { prototype: ZodAny }).prototype.editor = function (this: ZodAny, options: EditorOptions) {
const currentMeta = this.meta() || {}
const currentEditor = (currentMeta as { editor?: EditorOptions }).editor || {}

const newMeta = {
...currentMeta,
editor: { ...currentEditor, ...options },
}

return this.meta(newMeta) as unknown as ZodType
}

export const z = zod

export function getEditorOptions(schema: ZodType): EditorOptions | undefined {
const meta = schema.meta()
return meta ? (meta as { editor?: EditorOptions }).editor : undefined
}

// Function to get the underlying Zod type
export function getUnderlyingType(zodType: ZodType): ZodType {
while ((zodType._def as ZodOptionalDef).innerType) {
zodType = (zodType._def as ZodOptionalDef).innerType as ZodType
let currentType = zodType
while (
currentType.constructor.name === 'ZodOptional'
|| currentType.constructor.name === 'ZodNullable'
|| currentType.constructor.name === 'ZodDefault'
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
currentType = (currentType as any)._def.innerType as ZodType
}
return zodType
return currentType
}

export function getUnderlyingTypeName(zodType: ZodType): string {
return getUnderlyingType(zodType).constructor.name
}

export function zodToStandardSchema(schema: zod.ZodSchema, name: string): Draft07 {
const jsonSchema = zodToJsonSchema(schema, { name, $refStrategy: 'none' }) as Draft07
const jsonSchemaWithEditorMeta = zodToJsonSchema(
schema,
{
name,
$refStrategy: 'none',
override: (def) => {
if (def.editor) {
return {
$content: {
editor: def.editor,
},
} as never
export function zodToStandardSchema(
schema: zod.ZodSchema,
name: string,
): Draft07 {
try {
const baseSchema = zod.toJSONSchema(schema, {
target: 'draft-7',
unrepresentable: 'any',
override: (ctx) => {
const def = ctx.zodSchema._zod?.def
if (def?.type === 'date') {
ctx.jsonSchema.type = 'string'
ctx.jsonSchema.format = 'date-time'
}
},
})

return ignoreOverride
const draft07Schema: Draft07 = {
$schema: 'http://json-schema.org/draft-07/schema#',
$ref: `#/definitions/${name}`,
definitions: {
[name]: {
type: (baseSchema.type as string) || 'object',
properties:
(baseSchema.properties as Record<
string,
| Draft07DefinitionProperty
| Draft07DefinitionPropertyAnyOf
| Draft07DefinitionPropertyAllOf
>) || {},
required: (baseSchema.required as string[]) || [],
additionalProperties:
typeof baseSchema.additionalProperties === 'boolean'
? baseSchema.additionalProperties
: false,
},
},
}) as Draft07
}

return defu(jsonSchema, jsonSchemaWithEditorMeta)
return draft07Schema
}
catch (error) {
console.error(
'Zod toJSONSchema error for schema:',
schema.constructor.name,
error,
)
return {
$schema: 'http://json-schema.org/draft-07/schema#',
$ref: `#/definitions/${name}`,
definitions: {
[name]: {
type: 'object',
properties: {},
required: [],
additionalProperties: false,
},
},
}
}
}
2 changes: 1 addition & 1 deletion test/unit/defineCollection.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import { defineCollection } from '../../src/utils/collection'

const metaFields = ['id', 'stem', 'meta', 'extension']
Expand Down
2 changes: 1 addition & 1 deletion test/unit/generateCollectionInsert.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import { generateCollectionInsert, defineCollection, resolveCollection, getTableName, SLICE_SIZE, MAX_SQL_QUERY_SIZE } from '../../src/utils/collection'

describe('generateCollectionInsert', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/unit/generateCollectionTableDefinition.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import { generateCollectionTableDefinition, defineCollection, resolveCollection, getTableName } from '../../src/utils/collection'

describe('generateCollectionTableDefinition', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/unit/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import { defineCollection } from '../../src/utils'
import { resolveCollection } from '../../src/utils/collection'
import { parseContent } from '../utils/content'
Expand Down
2 changes: 1 addition & 1 deletion test/unit/parseContent.csv.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, test, expect, assert } from 'vitest'
import csvToJson from 'csvtojson'
import { z } from 'zod'
import * as z from 'zod/v4'
import { parseContent } from '../utils/content'
import { defineCollection } from '../../src/utils'
import { resolveCollection } from '../../src/utils/collection'
Expand Down
2 changes: 1 addition & 1 deletion test/unit/parseContent.json.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, test, expect, assert } from 'vitest'

import { z } from 'zod'
import * as z from 'zod/v4'
import { parseContent } from '../utils/content'
import { defineCollection } from '../../src/utils'
import { resolveCollection } from '../../src/utils/collection'
Expand Down
2 changes: 1 addition & 1 deletion test/unit/parseContent.md-highlighter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, assert } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import GithubLight from 'shiki/themes/github-light.mjs'
import type { MDCElement } from '@nuxtjs/mdc'
import type { Nuxt } from '@nuxt/schema'
Expand Down
2 changes: 1 addition & 1 deletion test/unit/parseContent.md.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, assert } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import { visit } from 'unist-util-visit'
import type { Nuxt } from '@nuxt/schema'
import { parseContent } from '../utils/content'
Expand Down
2 changes: 1 addition & 1 deletion test/unit/parseContent.path-meta.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, assert } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import { parseContent } from '../utils/content'
import { defineCollection } from '../../src/utils'
import { resolveCollection } from '../../src/utils/collection'
Expand Down
2 changes: 1 addition & 1 deletion test/unit/parseContent.yaml.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, assert } from 'vitest'
import { z } from 'zod'
import * as z from 'zod/v4'
import { parseContent } from '../utils/content'
import { defineCollection } from '../../src/utils'
import { resolveCollection } from '../../src/utils/collection'
Expand Down
46 changes: 46 additions & 0 deletions test/unit/zodRawSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest'
import { z } from 'zod/v4'
import { z as localZ, getEditorOptions, zodToStandardSchema } from '../../src/utils/zod'
import { pageSchema } from '../../src/utils/schema'

interface WithEditor { editor(o: { input: 'icon' }): { _def: unknown } }
const editorSchema = (localZ.string() as unknown as WithEditor).editor({ input: 'icon' })

const nullableSchema = z.object({ title: z.string().nullable() })

describe('zod raw toJSONSchema passthrough', () => {
it('preserves editor metadata via helper', () => {
const meta = getEditorOptions(editorSchema as unknown as import('zod/v4').ZodTypeAny)
expect(meta).toBeDefined()
expect(meta).toMatchObject({ input: 'icon' })
})

it('keeps nullable field in required and anyOf from zod.toJSONSchema', () => {
const std = zodToStandardSchema(nullableSchema, 'Test')
const def = std.definitions.Test
expect(def.required).toContain('title')
const titleProp = (def.properties as Record<string, unknown>).title as { anyOf: Array<{ type: string }> }
expect(titleProp.anyOf).toBeDefined()
const anyOfTypes = titleProp.anyOf.map(p => p.type)
expect(anyOfTypes).toContain('string')
expect(anyOfTypes).toContain('null')
})

it('retains navigation union anyOf members unchanged', () => {
const std = zodToStandardSchema(pageSchema, 'Page')
const def = std.definitions.Page
const navigationProp = (def.properties as Record<string, unknown>).navigation as { anyOf: Array<{ type: string }> }
expect(navigationProp.anyOf).toBeDefined()
const types = navigationProp.anyOf.map(p => p.type).sort()
expect(types).toEqual(['boolean', 'object'])
})

it('retains seo allOf composition', () => {
const std = zodToStandardSchema(pageSchema, 'Page')
const def = std.definitions.Page
const seoProp = (def.properties as Record<string, unknown>).seo as { allOf: unknown[] }
expect(seoProp.allOf).toBeDefined()
expect(Array.isArray(seoProp.allOf)).toBe(true)
expect(seoProp.allOf.length).toBe(2)
})
})
Loading