Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/pr-1091.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!-- auto-generated -->
---
'@sanity/cli-core': minor
---

feat: polyglot typegen
66 changes: 64 additions & 2 deletions packages/@sanity/cli-core/src/config/cli/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,71 @@
import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-compiler'
import {z} from 'zod/mini'

import {type CliConfig, type TypeGenConfig} from './types/cliConfig'
import {
type CliConfig,
type GoLanguageConfig,
type PhpLanguageConfig,
type PolyglotTypeGenConfig,
type SwiftLanguageConfig,
type TypeGenConfig,
type TypeScriptLanguageConfig,
} from './types/cliConfig'
import {type UserViteConfig} from './types/userViteConfig'

const K_NEW = ['typescript', 'go', 'php', 'swift'] as const
const K_LEGACY = [
'schema',
'generates',
'path',
'overloadClientMethods',
'formatGeneratedCode',
] as const

const GO_PACKAGE_RE = /^[a-z][a-z0-9_]*$/
const PHP_NAMESPACE_RE = /^[A-Z][A-Za-z0-9_]*(\\[A-Z][A-Za-z0-9_]*)*$/

const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === 'object' && v !== null && !Array.isArray(v)

const present = (block: Record<string, unknown>, keys: readonly string[]): string[] =>
keys.filter((k) => block[k] !== undefined)

const typegenSchema = z.custom<
(Partial<TypeGenConfig> & {enabled?: boolean}) | PolyglotTypeGenConfig
>((raw) => {
if (raw === undefined || raw === null) return true
if (!isRecord(raw)) return false
const block = raw as Record<string, unknown>

const newKeys = present(block, K_NEW)
const legacyKeys = present(block, K_LEGACY)
if (newKeys.length > 0 && legacyKeys.length > 0) return false

if (block.go !== undefined) {
const go = block.go as GoLanguageConfig
if (!isRecord(go)) return false
if (typeof go.schema !== 'string' || typeof go.generates !== 'string') return false
if (go.packageName !== undefined && !GO_PACKAGE_RE.test(String(go.packageName))) return false
}
if (block.php !== undefined) {
const php = block.php as PhpLanguageConfig
if (!isRecord(php)) return false
if (typeof php.schema !== 'string' || typeof php.generates !== 'string') return false
if (php.namespace !== undefined && !PHP_NAMESPACE_RE.test(String(php.namespace))) return false
}
if (block.swift !== undefined) {
const swift = block.swift as SwiftLanguageConfig
if (!isRecord(swift)) return false
if (typeof swift.schema !== 'string' || typeof swift.generates !== 'string') return false
}
if (block.typescript !== undefined) {
const ts = block.typescript as TypeScriptLanguageConfig
if (!isRecord(ts)) return false
if (typeof ts.schema !== 'string' || typeof ts.generates !== 'string') return false
}
return true
}, 'typegen: invalid shape — mixed legacy+per-language form or invalid per-language fields')

/**
* @public
*/
Expand Down Expand Up @@ -86,5 +148,5 @@ export const cliConfigSchema = z.object({

vite: z.optional(z.custom<UserViteConfig>()),

typegen: z.optional(z.custom<Partial<TypeGenConfig> & {enabled?: boolean}>()),
typegen: z.optional(typegenSchema),
}) satisfies z.core.$ZodType<CliConfig>
94 changes: 84 additions & 10 deletions packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,82 @@ import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-comp

import {type UserViteConfig} from './userViteConfig'

/**
* Legacy flat-form typegen config. Equivalent to nesting these fields under `typegen.typescript`.
* @public
*/
export interface TypeGenConfig {
formatGeneratedCode: boolean
formatGeneratedCode: 'oxfmt' | 'prettier' | boolean
generates: string
overloadClientMethods: boolean
path: string | string[]
schema: string
}

/**
* Fields shared by every per-language typegen sub-block.
* @public
*/
export interface BaseLanguageConfig {
/** Output file path. Must end in the language's file extension. */
generates: string
/** Path to the pre-extracted Sanity schema JSON file. */
schema: string

/** Format the emitted source. Defaults to `true`. */
formatGeneratedCode?: 'oxfmt' | 'prettier' | boolean
}

/**
* TypeScript per-language typegen config.
* @public
*/
export interface TypeScriptLanguageConfig extends BaseLanguageConfig {
/** Overload `@sanity/client` methods to return typed results. Default true. */
overloadClientMethods?: boolean
/** Source file globs for GROQ query extraction. */
path?: string | string[]
}

/**
* Go per-language typegen config.
* @public
*/
export interface GoLanguageConfig extends BaseLanguageConfig {
/** Go package name. Defaults to the basename of `dirname(generates)`. */
packageName?: string
}

/**
* PHP per-language typegen config.
* @public
*/
export interface PhpLanguageConfig extends BaseLanguageConfig {
/** PHP namespace. Defaults to `Sanity\\Generated`. */
namespace?: string
}

/**
* Swift per-language typegen config. Reserved for future per-language knobs.
* @public
*/
export type SwiftLanguageConfig = BaseLanguageConfig

/**
* Polyglot typegen configuration. Configure one or more language sub-blocks under
* `typegen`. The legacy flat shape (`typegen: {schema, generates, ...}`) is still
* accepted and folds into `typegen.typescript` with a deprecation warning.
* @public
*/
export interface PolyglotTypeGenConfig {
/** Enable typegen as part of sanity dev and sanity build. */
enabled?: boolean
go?: GoLanguageConfig
php?: PhpLanguageConfig
swift?: SwiftLanguageConfig
typescript?: TypeScriptLanguageConfig
}

/**
* @public
*/
Expand Down Expand Up @@ -134,16 +202,22 @@ export interface CliConfig {
studioHost?: string

/**
* Configuration for Sanity typegen
* Configuration for Sanity typegen.
*
* Accepts either:
* - The new per-language form: `{ typescript?, go?, php?, swift?, enabled? }`
* - The legacy flat form: `{ schema, generates, path?, overloadClientMethods?, formatGeneratedCode?, enabled? }` (deprecated; folds into `typegen.typescript`)
*/
typegen?: Partial<TypeGenConfig> & {
/**
* Enable typegen as part of sanity dev and sanity build.
* When enabled, types are generated on startup and when files change.
* Defaults to `false`
*/
enabled?: boolean
}
typegen?:
| (Partial<TypeGenConfig> & {
/**
* Enable typegen as part of sanity dev and sanity build.
* When enabled, types are generated on startup and when files change.
* Defaults to `false`
*/
enabled?: boolean
})
| PolyglotTypeGenConfig

/** Exposes the default Vite configuration for custom apps and the Studio so it can be changed and extended. */
vite?: UserViteConfig
Expand Down
Loading