Skip to content
Merged
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
32 changes: 32 additions & 0 deletions docs/public/.well-known/skills/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Nuxt Safe Runtime Config

Use this when modifying a Nuxt app that depends on `nuxt-safe-runtime-config`, or when adding typed runtime config validation to a Nuxt app.

## Core model

- Configure the module with `safeRuntimeConfig.$schema`.
- The schema must implement Standard Schema. Zod, Valibot, ArkType, and other Standard Schema compatible libraries are supported.
- Build-time validation is enabled by default. Runtime validation is opt-in with `validateAtRuntime: true`.
- `useSafeRuntimeConfig()` is the typed accessor for app code, server routes, and server utilities.
- Generated types live under `.nuxt/types/safe-runtime-config.d.ts`; never edit generated files directly.

## Editing workflow

1. When adding or renaming a runtime config key, update both `runtimeConfig` and the Standard Schema in `safeRuntimeConfig.$schema`.
2. Prefer `useSafeRuntimeConfig()` over `useRuntimeConfig()` wherever typed access matters.
3. If an editor or utility file runs outside Nuxt auto-import context, import the composable from `#imports`.
4. Do not ask consumers to install JSON Schema converter packages for Zod or Valibot; the module includes the conversion path.
5. After config or schema changes, run `nuxi prepare` or the project typecheck so generated types refresh.

## Validation behavior

- `onError: 'throw'` is the default and should stay in CI-facing validation paths.
- Use `onError: 'warn'` only for migration periods where missing values are expected.
- Use `validateAtRuntime: true` when deployment-time environment variables can differ from build-time values.
- Treat validation failures as configuration bugs; do not patch around them by casting the config to `any`.

## Type-safety checks

- `config.public.*` should reflect public runtime config keys.
- Private runtime config keys should stay off `config.public`.
- Unknown keys should fail typechecking after the generated declarations are refreshed.
9 changes: 9 additions & 0 deletions docs/public/.well-known/skills/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"skills": [
{
"name": "nuxt-safe-runtime-config",
"description": "Use Nuxt Safe Runtime Config to validate Nuxt runtimeConfig with Standard Schema and consume it through typed generated helpers.",
"files": ["SKILL.md"]
Comment thread
onmax marked this conversation as resolved.
}
]
}
32 changes: 32 additions & 0 deletions docs/public/.well-known/skills/nuxt-safe-runtime-config/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Nuxt Safe Runtime Config

Use this when modifying a Nuxt app that depends on `nuxt-safe-runtime-config`, or when adding typed runtime config validation to a Nuxt app.

## Core model

- Configure the module with `safeRuntimeConfig.$schema`.
- The schema must implement Standard Schema. Zod, Valibot, ArkType, and other Standard Schema compatible libraries are supported.
- Build-time validation is enabled by default. Runtime validation is opt-in with `validateAtRuntime: true`.
- `useSafeRuntimeConfig()` is the typed accessor for app code, server routes, and server utilities.
- Generated types live under `.nuxt/types/safe-runtime-config.d.ts`; never edit generated files directly.

## Editing workflow

1. When adding or renaming a runtime config key, update both `runtimeConfig` and the Standard Schema in `safeRuntimeConfig.$schema`.
2. Prefer `useSafeRuntimeConfig()` over `useRuntimeConfig()` wherever typed access matters.
3. If an editor or utility file runs outside Nuxt auto-import context, import the composable from `#imports`.
4. Do not ask consumers to install JSON Schema converter packages for Zod or Valibot; the module includes the conversion path.
5. After config or schema changes, run `nuxi prepare` or the project typecheck so generated types refresh.

## Validation behavior

- `onError: 'throw'` is the default and should stay in CI-facing validation paths.
- Use `onError: 'warn'` only for migration periods where missing values are expected.
- Use `validateAtRuntime: true` when deployment-time environment variables can differ from build-time values.
- Treat validation failures as configuration bugs; do not patch around them by casting the config to `any`.

## Type-safety checks

- `config.public.*` should reflect public runtime config keys.
- Private runtime config keys should stay off `config.public`.
- Unknown keys should fail typechecking after the generated declarations are refreshed.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"name": "onmax"
},
"license": "MIT",
"homepage": "https://nuxt-safe-runtime-config.onmax.me",
"docs": "https://nuxt-safe-runtime-config.onmax.me",
"repository": "onmax/nuxt-safe-runtime-config",
"keywords": [
"nuxt",
Expand Down
3 changes: 0 additions & 3 deletions src/runtime/nitro/validate-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,5 @@ export default (): void => {
else if (onError === 'warn') {
logger.warn(msg)
}
return
}

logger.success('Runtime config validated (server start)')
}
35 changes: 26 additions & 9 deletions src/utils/json-schema.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec'
import { toJsonSchema } from '@standard-community/standard-json'
import { errorMessage } from './error'

type NativeJsonSchemaOutput = (options: {
target: StandardJSONSchemaV1.Target
}) => Record<string, unknown> | Promise<Record<string, unknown>>

interface StandardSchemaWithNativeJsonSchema extends StandardSchemaV1 {
'~standard': StandardSchemaV1['~standard'] & {
jsonSchema?: {
output?: NativeJsonSchemaOutput
}
}
}

function getNativeJSONSchemaOutput(schema: StandardSchemaV1): NativeJsonSchemaOutput | undefined {
const output = (schema as StandardSchemaWithNativeJsonSchema)['~standard']?.jsonSchema?.output
return typeof output === 'function' ? output : undefined
}

export function hasNativeJSONSchema(schema: StandardSchemaV1): boolean {
return typeof (schema?.['~standard'] as any)?.jsonSchema?.output === 'function'
return Boolean(getNativeJSONSchemaOutput(schema))
}

export async function getJSONSchema(
schema: StandardSchemaV1,
target: StandardJSONSchemaV1.Target = 'draft-2020-12',
onFallback?: (message: string) => void,
): Promise<Record<string, unknown>> {
if (hasNativeJSONSchema(schema)) {
const nativeOutput = getNativeJSONSchemaOutput(schema)

if (nativeOutput) {
try {
return (schema['~standard'] as any).jsonSchema.output({ target })
return await nativeOutput({ target })
}
catch {
onFallback?.(`Native JSON Schema failed for target "${target}", using fallback`)
catch (error) {
onFallback?.(`Native JSON Schema failed for target "${target}" (${errorMessage(error)}), using @standard-community/standard-json fallback`)
}
}
else {
onFallback?.('Schema does not support native JSON Schema, using @standard-community/standard-json fallback')
}

return await toJsonSchema(schema as any) as Record<string, unknown>
return await toJsonSchema(schema) as Record<string, unknown>
}
4 changes: 0 additions & 4 deletions src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export interface ResolvedValidationOptions {
export interface ValidationLogger {
error: (message: string) => void
warn: (message: string) => void
success: (message: string) => void
}

export interface RuntimeValidationArtifacts {
Expand Down Expand Up @@ -88,10 +87,7 @@ export async function validateRuntimeConfig(
if ('issues' in result && result.issues && result.issues.length > 0) {
const errorLines = result.issues.map((issue, index) => ` ${index + 1}. ${formatIssue(issue)}`)
reportError(`Validation failed!\n${errorLines.join('\n')}`, onError, logger, 'Runtime config validation failed')
return
}

logger.success('Validated Runtime Config')
}

function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
Expand Down
44 changes: 44 additions & 0 deletions test/agent-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'

const rootDir = fileURLToPath(new URL('..', import.meta.url))

describe('agent context metadata', () => {
it('advertises docs from package metadata', () => {
const pkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8')) as {
docs?: string
homepage?: string
}

expect(pkg.homepage).toBe('https://nuxt-safe-runtime-config.onmax.me')
expect(pkg.docs).toBe('https://nuxt-safe-runtime-config.onmax.me')
})

it('publishes a well-known skill for module-specific context', () => {
const indexPath = join(rootDir, 'docs/public/.well-known/skills/index.json')
const compatibilitySkillPath = join(rootDir, 'docs/public/.well-known/skills/SKILL.md')
const skillPath = join(rootDir, 'docs/public/.well-known/skills/nuxt-safe-runtime-config/SKILL.md')
const index = JSON.parse(readFileSync(indexPath, 'utf8')) as {
skills?: Array<{ name?: string, description?: string, files?: string[] }>
}

expect(existsSync(skillPath)).toBe(true)
expect(existsSync(compatibilitySkillPath)).toBe(true)
expect(index.skills).toEqual([
{
name: 'nuxt-safe-runtime-config',
description: 'Use Nuxt Safe Runtime Config to validate Nuxt runtimeConfig with Standard Schema and consume it through typed generated helpers.',
files: ['SKILL.md'],
},
])

const skill = readFileSync(skillPath, 'utf8')
const compatibilitySkill = readFileSync(compatibilitySkillPath, 'utf8')
expect(compatibilitySkill).toBe(skill)
expect(skill).toContain('useSafeRuntimeConfig()')
expect(skill).toContain('safeRuntimeConfig.$schema')
expect(skill.toLowerCase()).not.toContain('shelve')
})
})
23 changes: 22 additions & 1 deletion test/json-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,33 @@ describe('json-schema detection', () => {
const warnings: string[] = []
const jsonSchema = await getJSONSchema(schema, 'draft-2020-12', message => warnings.push(message))

expect(warnings).toEqual(['Schema does not support native JSON Schema, using @standard-community/standard-json fallback'])
expect(warnings).toEqual([])
expect(jsonSchema).toHaveProperty('type', 'object')
expect(jsonSchema).toHaveProperty('properties')
expect((jsonSchema.properties as Record<string, unknown>)).toHaveProperty('name')
})

it('warns when native JSON Schema output fails before fallback conversion', async () => {
const schema = {
'~standard': {
version: 1,
vendor: 'test',
validate: () => ({ value: {} }),
jsonSchema: {
output: () => {
throw new Error('native unavailable')
},
},
},
} as unknown as StandardSchemaV1

const warnings: string[] = []
await expect(getJSONSchema(schema, 'draft-2020-12', message => warnings.push(message))).rejects.toThrow()

expect(warnings).toHaveLength(1)
expect(warnings[0]).toContain('Native JSON Schema failed for target "draft-2020-12"')
})

it('detects native JSON Schema on callable ArkType schemas', async () => {
const schema = type({ name: 'string' })
expect(hasNativeJSONSchema(schema as any)).toBe(true)
Expand Down
Loading