Skip to content
Open
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
96 changes: 60 additions & 36 deletions src/utils/context.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,69 @@
import { createContext } from 'unctx'
import type { Draft07 } from '../types'

type ContextKey = 'zod3' | 'zod4' | 'valibot' | 'unknown'

const nuxtContentContext = {
zod3: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error(
'It seems you are using Zod version 3 for collection schema, but Zod is not installed, '
+ 'Nuxt Content does not ship with zod, install `zod` and `zod-to-json-schema` and it will work.',
)
},
},
zod4: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error(
'It seems you are using Zod version 4 for collection schema, but Zod is not installed, '
+ 'Nuxt Content does not ship with zod, install `zod` and it will work.',
)
},
},
valibot: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error(
'It seems you are using Valibot for collection schema, but Valibot is not installed, '
+ 'Nuxt Content does not ship with valibot, install `valibot` and `@valibot/to-json-schema` and it will work.',
)
},
},
unknown: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error('Unknown schema vendor')
},
},
set: (key: ContextKey, value: unknown) => {
nuxtContentContext[key] = value as typeof nuxtContentContext[ContextKey]
},
get: (key: ContextKey) => {
return nuxtContentContext[key]
},
// Stash the validators context on `globalThis` under a `Symbol.for` key so
// that any duplicate evaluations of this module (e.g. when jiti re-loads
// `@nuxt/content` while processing `content.config.ts` under pnpm's
// `enableGlobalVirtualStore`, where realpath differences break Node's ESM
// cache) share the same backing object. Without this, the second instance
// resets the context to its stub state and `toJSONSchema` throws even
// after the first instance detected zod/valibot.
const SINGLETON_KEY = Symbol.for('@nuxt/content:validators-context')

type SchemaHandler = { toJSONSchema: (schema: unknown, name: string) => Draft07 }

type NuxtContentContext = {
zod3: SchemaHandler
zod4: SchemaHandler
valibot: SchemaHandler
unknown: SchemaHandler
set: (key: ContextKey, value: unknown) => void
get: (key: ContextKey) => SchemaHandler
}

const nuxtContentContext: NuxtContentContext
= ((globalThis as Record<symbol, unknown>)[SINGLETON_KEY] as NuxtContentContext | undefined) ?? (
(globalThis as Record<symbol, unknown>)[SINGLETON_KEY] = {
zod3: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error(
'It seems you are using Zod version 3 for collection schema, but Zod is not installed, '
+ 'Nuxt Content does not ship with zod, install `zod` and `zod-to-json-schema` and it will work.',
)
},
},
zod4: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error(
'It seems you are using Zod version 4 for collection schema, but Zod is not installed, '
+ 'Nuxt Content does not ship with zod, install `zod` and it will work.',
)
},
},
valibot: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error(
'It seems you are using Valibot for collection schema, but Valibot is not installed, '
+ 'Nuxt Content does not ship with valibot, install `valibot` and `@valibot/to-json-schema` and it will work.',
)
},
},
unknown: {
toJSONSchema: (_schema: unknown, _name: string) => {
throw new Error('Unknown schema vendor')
},
},
set(key: ContextKey, value: unknown) {
(this as unknown as Record<ContextKey, unknown>)[key] = value
},
get(key: ContextKey) {
return (this as unknown as Record<ContextKey, SchemaHandler>)[key]
},
} satisfies NuxtContentContext
) as NuxtContentContext

const ctx = createContext<typeof nuxtContentContext>()
ctx.set(nuxtContentContext)

Expand Down
7 changes: 6 additions & 1 deletion src/utils/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { addDependency } from 'nypm'
import { resolvePackageJSON } from 'pkg-types'
import { logger } from './dev'
import nuxtContentContext from './context'
import { tryUseNuxt } from '@nuxt/kit'

export async function isPackageInstalled(packageName: string) {
// Resolve relative to @nuxt/content's own location so the check survives
// pnpm's `enableGlobalVirtualStore`, where dependencies declared by
// @nuxt/content (e.g. zod) aren't reachable from the user's project root
// and a plain dynamic import would fail.
try {
await import(packageName)
await resolvePackageJSON(packageName, { from: import.meta.url })
return true
}
catch {
Expand Down
33 changes: 33 additions & 0 deletions test/unit/validatorRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { z } from 'zod'

const SINGLETON_KEY = Symbol.for('@nuxt/content:validators-context')

function resetValidatorsContext() {
Reflect.deleteProperty(globalThis as Record<symbol, unknown>, SINGLETON_KEY)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

describe('validator registry', () => {
beforeEach(() => {
vi.resetModules()
resetValidatorsContext()
})

afterEach(() => {
resetValidatorsContext()
})

test('preserves initialized validators across duplicate module evaluations', async () => {
const { initiateValidatorsContext } = await import('../../src/utils/dependencies.ts?deps=primary')
await initiateValidatorsContext()

const { default: useContextA } = await import('../../src/utils/context.ts?ctx=a')
const { default: useContextB } = await import('../../src/utils/context.ts?ctx=b')

const contextA = useContextA()
const contextB = useContextB()

expect(contextA).toBe(contextB)
expect(() => contextB.get('zod3').toJSONSchema(z.object({ title: z.string() }), '__SCHEMA__')).not.toThrow()
})
})
Loading