@sanity/cli
Version:
Sanity CLI tool for managing Sanity installations, managing plugins, schemas and datasets
211 lines (184 loc) • 7.04 kB
text/typescript
import {constants, mkdir, open, stat} from 'node:fs/promises'
import {dirname, join} from 'node:path'
import {Worker} from 'node:worker_threads'
import {readConfig} from '@sanity/codegen'
import {format as prettierFormat, resolveConfig as resolvePrettierConfig} from 'prettier'
import {type CliCommandArguments, type CliCommandContext} from '../../types'
import {getCliWorkerPath} from '../../util/cliWorker'
import {
type TypegenGenerateTypesWorkerData,
type TypegenGenerateTypesWorkerMessage,
} from '../../workers/typegenGenerate'
import {TypesGeneratedTrace} from './generate.telemetry'
export interface TypegenGenerateTypesCommandFlags {
'config-path'?: string
}
const generatedFileWarning = `/**
* ---------------------------------------------------------------------------------
* This file has been generated by Sanity TypeGen.
* Command: \`sanity typegen generate\`
*
* Any modifications made directly to this file will be overwritten the next time
* the TypeScript definitions are generated. Please make changes to the Sanity
* schema definitions and/or GROQ queries if you need to update these types.
*
* For more information on how to use Sanity TypeGen, visit the official documentation:
* https://www.sanity.io/docs/sanity-typegen
* ---------------------------------------------------------------------------------
*/\n\n`
export default async function typegenGenerateAction(
args: CliCommandArguments<TypegenGenerateTypesCommandFlags>,
context: CliCommandContext,
): Promise<void> {
const flags = args.extOptions
const {output, workDir, telemetry} = context
const trace = telemetry.trace(TypesGeneratedTrace)
trace.start()
const codegenConfig = await readConfig(flags['config-path'] || 'sanity-typegen.json')
try {
const schemaStats = await stat(codegenConfig.schema)
if (!schemaStats.isFile()) {
throw new Error(`Schema path is not a file: ${codegenConfig.schema}`)
}
} catch (err) {
if (err.code === 'ENOENT') {
// If the user has not provided a specific schema path (eg we're using the default), give some help
const hint =
codegenConfig.schema === './schema.json' ? ` - did you run "sanity schema extract"?` : ''
throw new Error(`Schema file not found: ${codegenConfig.schema}${hint}`)
}
throw err
}
const outputPath = join(process.cwd(), codegenConfig.generates)
const outputDir = dirname(outputPath)
await mkdir(outputDir, {recursive: true})
const workerPath = await getCliWorkerPath('typegenGenerate')
const spinner = output.spinner({}).start('Generating types')
const worker = new Worker(workerPath, {
workerData: {
workDir,
schemaPath: codegenConfig.schema,
searchPath: codegenConfig.path,
overloadClientMethods: codegenConfig.overloadClientMethods,
} satisfies TypegenGenerateTypesWorkerData,
// eslint-disable-next-line no-process-env
env: process.env,
})
const typeFile = await open(
outputPath,
// eslint-disable-next-line no-bitwise
constants.O_TRUNC | constants.O_CREAT | constants.O_WRONLY,
)
typeFile.write(generatedFileWarning)
const stats = {
queryFilesCount: 0,
errors: 0,
queriesCount: 0,
schemaTypesCount: 0,
unknownTypeNodesGenerated: 0,
typeNodesGenerated: 0,
emptyUnionTypeNodesGenerated: 0,
size: 0,
}
await new Promise<void>((resolve, reject) => {
worker.addListener('message', (msg: TypegenGenerateTypesWorkerMessage) => {
if (msg.type === 'error') {
if (msg.fatal) {
trace.error(msg.error)
reject(msg.error)
return
}
const errorMessage = msg.filename
? `${msg.error.message} in "${msg.filename}"`
: msg.error.message
spinner.fail(errorMessage)
stats.errors++
return
}
if (msg.type === 'complete') {
resolve()
return
}
if (msg.type === 'typemap') {
let typeMapStr = `// Query TypeMap\n`
typeMapStr += msg.typeMap
typeFile.write(typeMapStr)
stats.size += Buffer.byteLength(typeMapStr)
return
}
let fileTypeString = `// Source: ${msg.filename}\n`
if (msg.type === 'schema') {
stats.schemaTypesCount += msg.length
fileTypeString += msg.schema
typeFile.write(fileTypeString)
return
}
if (msg.type === 'types') {
stats.queryFilesCount++
for (const {
queryName,
query,
type,
typeNodesGenerated,
unknownTypeNodesGenerated,
emptyUnionTypeNodesGenerated,
} of msg.types) {
fileTypeString += `// Variable: ${queryName}\n`
fileTypeString += `// Query: ${query.replace(/(\r\n|\n|\r)/gm, '').trim()}\n`
fileTypeString += type
stats.queriesCount++
stats.typeNodesGenerated += typeNodesGenerated
stats.unknownTypeNodesGenerated += unknownTypeNodesGenerated
stats.emptyUnionTypeNodesGenerated += emptyUnionTypeNodesGenerated
}
typeFile.write(`${fileTypeString}\n`)
stats.size += Buffer.byteLength(fileTypeString)
}
})
worker.addListener('error', reject)
})
await typeFile.close()
const prettierConfig = codegenConfig.formatGeneratedCode
? await resolvePrettierConfig(outputPath).catch((err) => {
output.warn(`Failed to load prettier config: ${err.message}`)
return null
})
: null
if (prettierConfig) {
const formatFile = await open(outputPath, constants.O_RDWR)
try {
const code = await formatFile.readFile()
const formattedCode = await prettierFormat(code.toString(), {
...prettierConfig,
parser: 'typescript' as const,
})
await formatFile.truncate()
await formatFile.write(formattedCode, 0)
spinner.info('Formatted generated types with Prettier')
} catch (err) {
output.warn(`Failed to format generated types with Prettier: ${err.message}`)
} finally {
await formatFile.close()
}
}
trace.log({
outputSize: stats.size,
queriesCount: stats.queriesCount,
schemaTypesCount: stats.schemaTypesCount,
queryFilesCount: stats.queryFilesCount,
filesWithErrors: stats.errors,
typeNodesGenerated: stats.typeNodesGenerated,
unknownTypeNodesGenerated: stats.unknownTypeNodesGenerated,
unknownTypeNodesRatio:
stats.typeNodesGenerated > 0 ? stats.unknownTypeNodesGenerated / stats.typeNodesGenerated : 0,
emptyUnionTypeNodesGenerated: stats.emptyUnionTypeNodesGenerated,
configOverloadClientMethods: codegenConfig.overloadClientMethods,
})
trace.complete()
if (stats.errors > 0) {
spinner.warn(`Encountered errors in ${stats.errors} files while generating types`)
}
spinner.succeed(
`Generated TypeScript types for ${stats.schemaTypesCount} schema types and ${stats.queriesCount} GROQ queries in ${stats.queryFilesCount} files into: ${codegenConfig.generates}`,
)
}