@sanity/cli 3.88.1-typegen-experimental.0 → 3.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,15 @@
1
- /* eslint-disable max-statements */
2
1
  import {constants, mkdir, open, stat} from 'node:fs/promises'
3
2
  import {dirname, join} from 'node:path'
4
- import process from 'node:process'
5
3
  import {Worker} from 'node:worker_threads'
6
4
 
7
- import {DEFAULT_CONFIG, readConfig} from '@sanity/codegen'
5
+ import {readConfig} from '@sanity/codegen'
6
+ import {format as prettierFormat, resolveConfig as resolvePrettierConfig} from 'prettier'
8
7
 
9
8
  import {type CliCommandArguments, type CliCommandContext} from '../../types'
10
9
  import {getCliWorkerPath} from '../../util/cliWorker'
11
- import {createReceiver} from '../../util/workerChannel'
12
10
  import {
13
11
  type TypegenGenerateTypesWorkerData,
14
- type TypegenWorkerChannel,
12
+ type TypegenGenerateTypesWorkerMessage,
15
13
  } from '../../workers/typegenGenerate'
16
14
  import {TypesGeneratedTrace} from './generate.telemetry'
17
15
 
@@ -19,8 +17,7 @@ export interface TypegenGenerateTypesCommandFlags {
19
17
  'config-path'?: string
20
18
  }
21
19
 
22
- const DEFAULT_CONFIG_PATH = 'sanity-typegen.json'
23
- const GENERATED_FILE_WARNING = `/**
20
+ const generatedFileWarning = `/**
24
21
  * ---------------------------------------------------------------------------------
25
22
  * This file has been generated by Sanity TypeGen.
26
23
  * Command: \`sanity typegen generate\`
@@ -34,20 +31,6 @@ const GENERATED_FILE_WARNING = `/**
34
31
  * ---------------------------------------------------------------------------------
35
32
  */\n\n`
36
33
 
37
- const percentageFormatter = new Intl.NumberFormat('en-US', {
38
- style: 'percent',
39
- minimumFractionDigits: 1,
40
- maximumFractionDigits: 1,
41
- })
42
-
43
- const percent = (value: number): string => percentageFormatter.format(Math.min(value, 1))
44
-
45
- const count = (
46
- amount: number,
47
- plural: string,
48
- singular: string = plural.slice(0, Math.max(0, plural.length - 1)),
49
- ): string => `${amount.toLocaleString('en-US')} ${amount === 1 ? singular : plural}`
50
-
51
34
  export default async function typegenGenerateAction(
52
35
  args: CliCommandArguments<TypegenGenerateTypesCommandFlags>,
53
36
  context: CliCommandContext,
@@ -58,160 +41,170 @@ export default async function typegenGenerateAction(
58
41
  const trace = telemetry.trace(TypesGeneratedTrace)
59
42
  trace.start()
60
43
 
61
- const spinner = output.spinner('Generating types…')
62
- spinner.start()
44
+ const codegenConfig = await readConfig(flags['config-path'] || 'sanity-typegen.json')
63
45
 
64
- let codegenConfig
65
- const configPath = flags['config-path'] ?? DEFAULT_CONFIG_PATH
66
46
  try {
67
- codegenConfig = await readConfig(configPath)
68
- spinner.info(`Using typegen configuration found at "${configPath}"`)
69
- } catch (error) {
70
- if (error?.code !== 'ENOENT') throw error
71
- codegenConfig = DEFAULT_CONFIG
72
-
73
- if (configPath !== DEFAULT_CONFIG_PATH) {
74
- spinner.warn(
75
- `Configuration file not found at specified path "${configPath}". Falling back to default settings.`,
76
- )
47
+ const schemaStats = await stat(codegenConfig.schema)
48
+ if (!schemaStats.isFile()) {
49
+ throw new Error(`Schema path is not a file: ${codegenConfig.schema}`)
77
50
  }
51
+ } catch (err) {
52
+ if (err.code === 'ENOENT') {
53
+ // If the user has not provided a specific schema path (eg we're using the default), give some help
54
+ const hint =
55
+ codegenConfig.schema === './schema.json' ? ` - did you run "sanity schema extract"?` : ''
56
+ throw new Error(`Schema file not found: ${codegenConfig.schema}${hint}`)
57
+ }
58
+ throw err
78
59
  }
79
- const {
80
- schemas,
81
- augmentGroqModule,
82
- formatGeneratedCode,
83
- generates,
84
- overloadClientMethods,
85
- path: searchPath,
86
- } = codegenConfig
87
-
88
- spinner.start(`Loading schema${schemas.length === 1 ? '' : 's'}…`)
89
-
90
- const outputPath = join(process.cwd(), generates)
91
- await mkdir(dirname(outputPath), {recursive: true})
92
-
93
- const workerData: TypegenGenerateTypesWorkerData = {
94
- workDir,
95
- schemas,
96
- searchPath,
97
- overloadClientMethods,
98
- augmentGroqModule,
99
- }
100
- const worker = new Worker(await getCliWorkerPath('typegenGenerate'), {
101
- workerData,
60
+
61
+ const outputPath = join(process.cwd(), codegenConfig.generates)
62
+ const outputDir = dirname(outputPath)
63
+ await mkdir(outputDir, {recursive: true})
64
+ const workerPath = await getCliWorkerPath('typegenGenerate')
65
+
66
+ const spinner = output.spinner({}).start('Generating types')
67
+
68
+ const worker = new Worker(workerPath, {
69
+ workerData: {
70
+ workDir,
71
+ schemaPath: codegenConfig.schema,
72
+ searchPath: codegenConfig.path,
73
+ overloadClientMethods: codegenConfig.overloadClientMethods,
74
+ } satisfies TypegenGenerateTypesWorkerData,
75
+ // eslint-disable-next-line no-process-env
102
76
  env: process.env,
103
77
  })
104
- const receiver = createReceiver<TypegenWorkerChannel>(worker)
105
78
 
106
- let fileHandle
107
- let schemaStats
108
- let queryStats
79
+ const typeFile = await open(
80
+ outputPath,
81
+ // eslint-disable-next-line no-bitwise
82
+ constants.O_TRUNC | constants.O_CREAT | constants.O_WRONLY,
83
+ )
84
+
85
+ typeFile.write(generatedFileWarning)
86
+
87
+ const stats = {
88
+ queryFilesCount: 0,
89
+ errors: 0,
90
+ queriesCount: 0,
91
+ schemaTypesCount: 0,
92
+ unknownTypeNodesGenerated: 0,
93
+ typeNodesGenerated: 0,
94
+ emptyUnionTypeNodesGenerated: 0,
95
+ size: 0,
96
+ }
109
97
 
110
- try {
111
- await receiver.event.loadedSchemas()
112
- spinner.succeed(
113
- schemas.length === 1
114
- ? `Loaded schema from ${schemas[0].schemaPath}`
115
- : `Loaded ${count(schemas.length, 'schemas')}`,
116
- )
117
-
118
- spinner.start('Generating schema types…')
119
- fileHandle = await open(outputPath, 'w')
120
- await fileHandle.write(GENERATED_FILE_WARNING)
121
- const schemaResult = await receiver.event.generatedSchemaDeclarations()
122
- schemaStats = schemaResult.schemaStats
123
- await fileHandle.write(schemaResult.code)
124
-
125
- const schemaTypesCount = count(schemaStats.schemaTypesCount, 'schema types')
126
- const schemaCount = count(schemaStats.schemaCount, 'schemas')
127
- spinner.succeed(
128
- `Generated ${schemaTypesCount}${schemas.length > 1 ? ` from ${schemaCount}` : ''}`,
129
- )
130
-
131
- spinner.start('Generating query types…')
132
- const expectedFiles = (await receiver.event.fileCount()).fileCount
133
- const expectedFileCount = count(expectedFiles, 'files')
134
-
135
- for await (const {
136
- progress,
137
- ...queryResult
138
- } of receiver.stream.generatedQueryResultDeclaration()) {
139
- const queryCount = count(progress.queriesCount, 'queries', 'query')
140
- const projectionCount = count(progress.projectionsCount, 'projections')
141
- spinner.text =
142
- `Generating query types… (${percent(progress.filesCount / expectedFiles)})\n` +
143
- ` └─ Processed ${progress.filesCount} of ${expectedFileCount}. Found ${queryCount}, ${projectionCount}.`
144
-
145
- if (queryResult.type === 'error') {
146
- spinner.fail(queryResult.message)
98
+ await new Promise<void>((resolve, reject) => {
99
+ worker.addListener('message', (msg: TypegenGenerateTypesWorkerMessage) => {
100
+ if (msg.type === 'error') {
101
+ if (msg.fatal) {
102
+ trace.error(msg.error)
103
+ reject(msg.error)
104
+ return
105
+ }
106
+ const errorMessage = msg.filename
107
+ ? `${msg.error.message} in "${msg.filename}"`
108
+ : msg.error.message
109
+ spinner.fail(errorMessage)
110
+ stats.errors++
111
+ return
147
112
  }
148
-
149
- if (queryResult.type === 'declaration') {
150
- await fileHandle.write(queryResult.code)
113
+ if (msg.type === 'complete') {
114
+ resolve()
115
+ return
151
116
  }
152
- }
153
117
 
154
- const result = await receiver.event.generationComplete()
155
- queryStats = result.queryStats
156
- await fileHandle.write(result.augmentedQueryResultDeclarations.code)
157
- await fileHandle.close()
158
- fileHandle = null
159
-
160
- const queryTypesCount = count(queryStats.queriesCount, 'query types')
161
- const projectionTypesCount = count(queryStats.projectionsCount, 'projection types')
162
- const scannedFilesCount = count(queryStats.totalScannedFilesCount, 'scanned files')
163
- spinner.succeed(
164
- `Generated ${queryTypesCount} and ${projectionTypesCount} from ${scannedFilesCount}`,
165
- )
166
-
167
- if (formatGeneratedCode) {
168
- spinner.start(`Formatting generated types with prettier…`)
169
-
170
- try {
171
- const prettier = await import('prettier')
172
- const prettierConfig = await prettier.resolveConfig(outputPath)
173
-
174
- fileHandle = await open(outputPath, constants.O_RDWR)
175
- const code = await fileHandle.readFile({encoding: 'utf-8'})
176
- const formattedCode = await prettier.format(code, {
177
- ...prettierConfig,
178
- parser: 'typescript' as const,
179
- })
180
- await fileHandle.truncate()
181
- await fileHandle.write(formattedCode, 0)
182
- await fileHandle.close()
183
- fileHandle = null
184
-
185
- spinner.succeed('Formatted generated types with prettier')
186
- } catch (err) {
187
- spinner.warn(`Failed to format generated types with prettier: ${err.message}`)
118
+ if (msg.type === 'typemap') {
119
+ let typeMapStr = `// Query TypeMap\n`
120
+ typeMapStr += msg.typeMap
121
+ typeFile.write(typeMapStr)
122
+ stats.size += Buffer.byteLength(typeMapStr)
123
+ return
188
124
  }
189
- }
190
125
 
191
- // Gather final stats and report success
192
- const outputStat = await stat(outputPath)
126
+ let fileTypeString = `// Source: ${msg.filename}\n`
127
+
128
+ if (msg.type === 'schema') {
129
+ stats.schemaTypesCount += msg.length
130
+ fileTypeString += msg.schema
131
+ typeFile.write(fileTypeString)
132
+ return
133
+ }
193
134
 
194
- trace.log({
195
- outputSize: outputStat.size,
196
- ...schemaStats,
197
- ...queryStats,
198
- configOverloadClientMethods: overloadClientMethods,
199
- configAugmentGroqModule: augmentGroqModule,
135
+ if (msg.type === 'types') {
136
+ stats.queryFilesCount++
137
+ for (const {
138
+ queryName,
139
+ query,
140
+ type,
141
+ typeNodesGenerated,
142
+ unknownTypeNodesGenerated,
143
+ emptyUnionTypeNodesGenerated,
144
+ } of msg.types) {
145
+ fileTypeString += `// Variable: ${queryName}\n`
146
+ fileTypeString += `// Query: ${query.replace(/(\r\n|\n|\r)/gm, '').trim()}\n`
147
+ fileTypeString += type
148
+ stats.queriesCount++
149
+ stats.typeNodesGenerated += typeNodesGenerated
150
+ stats.unknownTypeNodesGenerated += unknownTypeNodesGenerated
151
+ stats.emptyUnionTypeNodesGenerated += emptyUnionTypeNodesGenerated
152
+ }
153
+ typeFile.write(`${fileTypeString}\n`)
154
+ stats.size += Buffer.byteLength(fileTypeString)
155
+ }
200
156
  })
157
+ worker.addListener('error', reject)
158
+ })
201
159
 
202
- if (queryStats.errorCount > 0) {
203
- spinner.warn(
204
- `Encountered ${count(queryStats.errorCount, 'errors')} in ${count(queryStats.filesWithErrors, 'files')} while generating types to ${generates}`,
205
- )
206
- } else {
207
- spinner.succeed(`Successfully generated types to ${generates}`)
160
+ await typeFile.close()
161
+
162
+ const prettierConfig = codegenConfig.formatGeneratedCode
163
+ ? await resolvePrettierConfig(outputPath).catch((err) => {
164
+ output.warn(`Failed to load prettier config: ${err.message}`)
165
+ return null
166
+ })
167
+ : null
168
+
169
+ if (prettierConfig) {
170
+ const formatFile = await open(outputPath, constants.O_RDWR)
171
+ try {
172
+ const code = await formatFile.readFile()
173
+ const formattedCode = await prettierFormat(code.toString(), {
174
+ ...prettierConfig,
175
+ parser: 'typescript' as const,
176
+ })
177
+ await formatFile.truncate()
178
+ await formatFile.write(formattedCode, 0)
179
+
180
+ spinner.info('Formatted generated types with Prettier')
181
+ } catch (err) {
182
+ output.warn(`Failed to format generated types with Prettier: ${err.message}`)
183
+ } finally {
184
+ await formatFile.close()
208
185
  }
209
- } catch (err) {
210
- trace.error(err)
211
- throw err
212
- } finally {
213
- await fileHandle?.close()
214
- await receiver.dispose()
215
- trace.complete()
216
186
  }
187
+
188
+ trace.log({
189
+ outputSize: stats.size,
190
+ queriesCount: stats.queriesCount,
191
+ schemaTypesCount: stats.schemaTypesCount,
192
+ queryFilesCount: stats.queryFilesCount,
193
+ filesWithErrors: stats.errors,
194
+ typeNodesGenerated: stats.typeNodesGenerated,
195
+ unknownTypeNodesGenerated: stats.unknownTypeNodesGenerated,
196
+ unknownTypeNodesRatio:
197
+ stats.typeNodesGenerated > 0 ? stats.unknownTypeNodesGenerated / stats.typeNodesGenerated : 0,
198
+ emptyUnionTypeNodesGenerated: stats.emptyUnionTypeNodesGenerated,
199
+ configOverloadClientMethods: codegenConfig.overloadClientMethods,
200
+ })
201
+
202
+ trace.complete()
203
+ if (stats.errors > 0) {
204
+ spinner.warn(`Encountered errors in ${stats.errors} files while generating types`)
205
+ }
206
+
207
+ spinner.succeed(
208
+ `Generated TypeScript types for ${stats.schemaTypesCount} schema types and ${stats.queriesCount} GROQ queries in ${stats.queryFilesCount} files into: ${codegenConfig.generates}`,
209
+ )
217
210
  }
package/src/cli.ts CHANGED
File without changes
File without changes
File without changes