@sanity/cli 3.86.2-experimental.0 → 3.87.0

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,18 +1,15 @@
1
- /* eslint-disable complexity */
2
- /* eslint-disable max-statements */
3
- /* eslint-disable max-depth */
4
1
  import {constants, mkdir, open, stat} from 'node:fs/promises'
5
2
  import {dirname, join} from 'node:path'
6
3
  import {Worker} from 'node:worker_threads'
7
4
 
8
- import {readConfig, type TypeEvaluationStats} from '@sanity/codegen'
5
+ import {readConfig} from '@sanity/codegen'
6
+ import {format as prettierFormat, resolveConfig as resolvePrettierConfig} from 'prettier'
9
7
 
10
8
  import {type CliCommandArguments, type CliCommandContext} from '../../types'
11
9
  import {getCliWorkerPath} from '../../util/cliWorker'
12
- import {createReceiver} from '../../util/workerChannel'
13
10
  import {
14
11
  type TypegenGenerateTypesWorkerData,
15
- type TypegenWorkerChannel,
12
+ type TypegenGenerateTypesWorkerMessage,
16
13
  } from '../../workers/typegenGenerate'
17
14
  import {TypesGeneratedTrace} from './generate.telemetry'
18
15
 
@@ -34,51 +31,6 @@ const generatedFileWarning = `/**
34
31
  * ---------------------------------------------------------------------------------
35
32
  */\n\n`
36
33
 
37
- /**
38
- * Helper function to format the generated file using Prettier.
39
- * Handles its own file operations and error reporting.
40
- */
41
- async function formatGeneratedFile(
42
- outputPath: string,
43
- output: CliCommandContext['output'],
44
- spinner: ReturnType<CliCommandContext['output']['spinner']>,
45
- ): Promise<void> {
46
- // this is here because this is an ESM-only import
47
- const {format: prettierFormat, resolveConfig: resolvePrettierConfig} = await import('prettier')
48
-
49
- let formatFile
50
- try {
51
- // Load prettier config
52
- const prettierConfig = await resolvePrettierConfig(outputPath).catch((err) => {
53
- output.warn(`Failed to load prettier config: ${err.message}`)
54
- return null
55
- })
56
-
57
- if (prettierConfig) {
58
- spinner.text = 'Formatting generated types with Prettier...'
59
- formatFile = await open(outputPath, constants.O_RDWR)
60
- try {
61
- const code = await formatFile.readFile()
62
- const formattedCode = await prettierFormat(code.toString(), {
63
- ...prettierConfig,
64
- parser: 'typescript' as const,
65
- })
66
- await formatFile.truncate() // Truncate before writing formatted code
67
- await formatFile.write(formattedCode, 0) // Write formatted code from the beginning
68
- spinner.info('Formatted generated types with Prettier')
69
- } catch (err) {
70
- output.warn(`Failed to format generated types with Prettier: ${err.message}`)
71
- } finally {
72
- // Ensure the formatting file handle is closed
73
- await formatFile?.close()
74
- }
75
- }
76
- } catch (err) {
77
- // Catch errors during the formatting setup (e.g., opening the formatFile)
78
- output.warn(`Error during formatting setup: ${err.message}`)
79
- }
80
- }
81
-
82
34
  export default async function typegenGenerateAction(
83
35
  args: CliCommandArguments<TypegenGenerateTypesCommandFlags>,
84
36
  context: CliCommandContext,
@@ -89,157 +41,170 @@ export default async function typegenGenerateAction(
89
41
  const trace = telemetry.trace(TypesGeneratedTrace)
90
42
  trace.start()
91
43
 
92
- const typegenConfig = await readConfig(flags['config-path'] ?? 'sanity-typegen.json')
93
-
94
- const missingSchemas: string[] = []
95
- const invalidSchemas: string[] = []
44
+ const codegenConfig = await readConfig(flags['config-path'] || 'sanity-typegen.json')
96
45
 
97
- for (const schemaPath of typegenConfig.schemas.map((i) => i.schemaPath)) {
98
- try {
99
- const schemaStats = await stat(schemaPath)
100
- if (!schemaStats.isFile()) {
101
- invalidSchemas.push(schemaPath)
102
- }
103
- } catch (err) {
104
- if (err.code === 'ENOENT') {
105
- missingSchemas.push(schemaPath)
106
- } else {
107
- throw err
108
- }
46
+ try {
47
+ const schemaStats = await stat(codegenConfig.schema)
48
+ if (!schemaStats.isFile()) {
49
+ throw new Error(`Schema path is not a file: ${codegenConfig.schema}`)
109
50
  }
110
- }
111
-
112
- if (missingSchemas.length > 0 || invalidSchemas.length > 0) {
113
- const errors: string[] = []
114
-
115
- if (missingSchemas.length > 0) {
51
+ } catch (err) {
52
+ if (err.code === 'ENOENT') {
116
53
  // If the user has not provided a specific schema path (eg we're using the default), give some help
117
54
  const hint =
118
- missingSchemas.length === 1 && missingSchemas[0] === './schema.json'
119
- ? ' - did you run "sanity schema extract"?'
120
- : ''
121
- const schemaList = missingSchemas.map((path) => ` - ${path}`).join('\n')
122
- errors.push(`The following schema files were not found:\n${schemaList}${hint}`)
123
- }
124
-
125
- if (invalidSchemas.length > 0) {
126
- const schemaList = invalidSchemas.map((path) => ` - ${path}`).join('\n')
127
- errors.push(`The following schema paths are not files:\n${schemaList}`)
55
+ codegenConfig.schema === './schema.json' ? ` - did you run "sanity schema extract"?` : ''
56
+ throw new Error(`Schema file not found: ${codegenConfig.schema}${hint}`)
128
57
  }
129
-
130
- throw new Error(errors.join('\n\n'))
58
+ throw err
131
59
  }
132
60
 
133
- const outputPath = join(process.cwd(), typegenConfig.generates)
61
+ const outputPath = join(process.cwd(), codegenConfig.generates)
134
62
  const outputDir = dirname(outputPath)
135
63
  await mkdir(outputDir, {recursive: true})
136
64
  const workerPath = await getCliWorkerPath('typegenGenerate')
137
65
 
138
- const spinner = output.spinner('Generating types')
66
+ const spinner = output.spinner({}).start('Generating types')
139
67
 
140
68
  const worker = new Worker(workerPath, {
141
69
  workerData: {
142
70
  workDir,
143
- schemas: typegenConfig.schemas,
144
- searchPath: typegenConfig.path,
145
- overloadClientMethods: typegenConfig.overloadClientMethods,
146
- augmentGroqModule: typegenConfig.augmentGroqModule,
71
+ schemaPath: codegenConfig.schema,
72
+ searchPath: codegenConfig.path,
73
+ overloadClientMethods: codegenConfig.overloadClientMethods,
147
74
  } satisfies TypegenGenerateTypesWorkerData,
148
75
  // eslint-disable-next-line no-process-env
149
76
  env: process.env,
150
77
  })
151
78
 
152
- const receiver = createReceiver<TypegenWorkerChannel>(worker)
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)
153
86
 
154
- let fileHandle
155
87
  const stats = {
156
88
  queryFilesCount: 0,
89
+ errors: 0,
157
90
  queriesCount: 0,
158
- projectionsCount: 0,
159
91
  schemaTypesCount: 0,
160
- typeEvaluationStats: null as TypeEvaluationStats | null,
161
- outputSize: 0,
162
- filesWithErrors: 0,
92
+ unknownTypeNodesGenerated: 0,
93
+ typeNodesGenerated: 0,
94
+ emptyUnionTypeNodesGenerated: 0,
95
+ size: 0,
163
96
  }
164
97
 
165
- try {
166
- try {
167
- spinner.start()
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
112
+ }
113
+ if (msg.type === 'complete') {
114
+ resolve()
115
+ return
116
+ }
117
+
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
124
+ }
168
125
 
169
- fileHandle = await open(outputPath, 'w')
170
- await fileHandle.write(generatedFileWarning)
126
+ let fileTypeString = `// Source: ${msg.filename}\n`
171
127
 
172
- spinner.text = 'Generating schema types...'
173
- const {code: schemaCode, schemas} = await receiver.event.schema()
174
- stats.schemaTypesCount = schemas.reduce((total, schema) => total + schema.typeCount, 0)
175
- await fileHandle.write(schemaCode)
128
+ if (msg.type === 'schema') {
129
+ stats.schemaTypesCount += msg.length
130
+ fileTypeString += msg.schema
131
+ typeFile.write(fileTypeString)
132
+ return
133
+ }
176
134
 
177
- spinner.text = 'Generating query types...'
178
- for await (const queryResult of receiver.stream.queries()) {
135
+ if (msg.type === 'types') {
179
136
  stats.queryFilesCount++
180
- const {error, results} = queryResult
181
- if (error) {
182
- stats.filesWithErrors++
183
- }
184
- for (const result of results) {
185
- await fileHandle.write(result.code)
186
- if (result.type === 'projection') {
187
- stats.projectionsCount++
188
- } else {
189
- stats.queriesCount++
190
- }
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
191
152
  }
153
+ typeFile.write(`${fileTypeString}\n`)
154
+ stats.size += Buffer.byteLength(fileTypeString)
192
155
  }
156
+ })
157
+ worker.addListener('error', reject)
158
+ })
193
159
 
194
- spinner.text = 'Generating typemap...'
195
- const {code: typemapCode, stats: finalStats} = await receiver.event.typemap()
196
- stats.typeEvaluationStats = finalStats
197
- await fileHandle.write(typemapCode)
198
- } finally {
199
- // Ensure the initial file handle is closed before moving on
200
- await fileHandle?.close()
201
- }
202
-
203
- if (typegenConfig.formatGeneratedCode) {
204
- await formatGeneratedFile(outputPath, output, spinner)
205
- }
160
+ await typeFile.close()
206
161
 
207
- // Gather final stats and report success
208
- const finalStat = await stat(outputPath)
209
- stats.outputSize = finalStat.size
210
-
211
- trace.log({
212
- outputSize: stats.outputSize,
213
- queriesCount: stats.queriesCount,
214
- projectionsCount: stats.projectionsCount,
215
- schemaTypesCount: stats.schemaTypesCount,
216
- queryFilesCount: stats.queryFilesCount,
217
- filesWithErrors: stats.filesWithErrors,
218
- typeNodesGenerated: stats.typeEvaluationStats?.totalTypeNodes,
219
- unknownTypeNodesGenerated: stats.typeEvaluationStats?.unknownTypeCount,
220
- unknownTypeNodesRatio:
221
- stats.typeEvaluationStats && stats.typeEvaluationStats.totalTypeNodes > 0
222
- ? stats.typeEvaluationStats.unknownTypeCount / stats.typeEvaluationStats.totalTypeNodes
223
- : 0,
224
- emptyUnionTypeNodesGenerated: stats.typeEvaluationStats?.emptyUnionCount,
225
- configOverloadClientMethods: typegenConfig.overloadClientMethods,
226
- configAugmentGroqModule: typegenConfig.augmentGroqModule,
227
- })
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
228
168
 
229
- trace.complete()
230
- if (stats.filesWithErrors > 0) {
231
- spinner.warn(`Encountered errors in ${stats.filesWithErrors} files while generating types`)
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()
232
185
  }
186
+ }
233
187
 
234
- spinner.succeed(
235
- `Generated TypeScript types for ${stats.schemaTypesCount} schema types, ${stats.queriesCount} GROQ queries, ${stats.projectionsCount} GROQ projections, in ${stats.queryFilesCount} files into: ${typegenConfig.generates}`,
236
- )
237
- } catch (err) {
238
- spinner.fail('Type generation failed')
239
- trace.error(err instanceof Error ? err : new Error(String(err)))
240
- throw err // Re-throw the error after logging
241
- } finally {
242
- // Ensure the worker receiver is always disposed
243
- await receiver.dispose()
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`)
244
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
+ )
245
210
  }
package/src/cli.ts CHANGED
@@ -56,9 +56,6 @@ export async function runCli(cliRoot: string, {cliVersion}: {cliVersion: string}
56
56
  process.exit(1)
57
57
  }
58
58
 
59
- loadAndSetEnvFromDotEnvFiles({workDir, cmd: args.groupOrCommand})
60
- maybeFixMissingWindowsEnvVar()
61
-
62
59
  // Check if there are updates available for the CLI, and notify if there is
63
60
  await runUpdateCheck({pkg, cwd, workDir}).notify()
64
61
 
@@ -67,11 +64,20 @@ export async function runCli(cliRoot: string, {cliVersion}: {cliVersion: string}
67
64
 
68
65
  // Try to figure out if we're in a v2 or v3 context by finding a config
69
66
  debug(`Reading CLI config from "${workDir}"`)
70
- const cliConfig = await getCliConfig(workDir, {forked: true})
67
+ let cliConfig = await getCliConfig(workDir, {forked: true})
71
68
  if (!cliConfig) {
72
69
  debug('No CLI config found')
73
70
  }
74
71
 
72
+ // Figure out if the app is a studio or an app from the CLI config
73
+ const isApp = Boolean(cliConfig && 'app' in cliConfig)
74
+ // Load the environment variables from
75
+ loadAndSetEnvFromDotEnvFiles({workDir, cmd: args.groupOrCommand, isApp})
76
+ maybeFixMissingWindowsEnvVar()
77
+
78
+ // Reload the the cli config so env vars can work.
79
+ cliConfig = await getCliConfig(workDir, {forked: true})
80
+
75
81
  const {logger: telemetry, flush: flushTelemetry} = createTelemetryStore<TelemetryUserProperties>({
76
82
  projectId: cliConfig?.config?.api?.projectId,
77
83
  env: process.env,
@@ -274,7 +280,15 @@ function warnOnNonProductionEnvironment(): void {
274
280
  )
275
281
  }
276
282
 
277
- function loadAndSetEnvFromDotEnvFiles({workDir, cmd}: {workDir: string; cmd: string}) {
283
+ function loadAndSetEnvFromDotEnvFiles({
284
+ workDir,
285
+ cmd,
286
+ isApp,
287
+ }: {
288
+ workDir: string
289
+ cmd: string
290
+ isApp: boolean
291
+ }) {
278
292
  /* eslint-disable no-process-env */
279
293
 
280
294
  // Do a cheap lookup for a sanity.json file. If there is one, assume it is a v2 project,
@@ -309,7 +323,7 @@ function loadAndSetEnvFromDotEnvFiles({workDir, cmd}: {workDir: string; cmd: str
309
323
 
310
324
  debug('Loading environment files using %s mode', mode)
311
325
 
312
- const studioEnv = loadEnv(mode, workDir, ['SANITY_STUDIO_'])
326
+ const studioEnv = loadEnv(mode, workDir, [isApp ? 'SANITY_APP_' : 'SANITY_STUDIO_'])
313
327
  process.env = {...process.env, ...studioEnv}
314
328
  /* eslint-disable no-process-env */
315
329
  }
File without changes
File without changes
@@ -7,6 +7,7 @@ import {
7
7
  type DocumentDefinition,
8
8
  type ObjectDefinition,
9
9
  } from '@sanity/types'
10
+ import {format} from 'prettier'
10
11
 
11
12
  import {type CliApiClient} from '../types'
12
13
  import {getCliWorkerPath} from './cliWorker'
@@ -183,7 +184,6 @@ async function fetchJourneySchema(schemaUrl: string): Promise<DocumentOrObject[]
183
184
  async function assembleJourneySchemaTypeFileContent(schemaType: DocumentOrObject): Promise<string> {
184
185
  const serialised = wrapSchemaTypeInHelpers(schemaType)
185
186
  const imports = getImports(serialised)
186
- const {format} = await import('prettier')
187
187
  const prettifiedSchemaType = await format(serialised, {
188
188
  parser: 'typescript',
189
189
  printWidth: 40,
@@ -198,12 +198,11 @@ async function assembleJourneySchemaTypeFileContent(schemaType: DocumentOrObject
198
198
  * @param schemas - The Journey schemas to assemble into an index file
199
199
  * @returns The index file as a string
200
200
  */
201
- async function assembleJourneyIndexContent(schemas: DocumentOrObject[]): Promise<string> {
201
+ function assembleJourneyIndexContent(schemas: DocumentOrObject[]): Promise<string> {
202
202
  const sortedSchema = schemas.slice().sort((a, b) => (a.name > b.name ? 1 : -1))
203
203
  const imports = sortedSchema.map((schema) => `import { ${schema.name} } from './${schema.name}'`)
204
204
  const exports = sortedSchema.map((schema) => schema.name).join(',')
205
205
  const fileContents = `${imports.join('\n')}\n\nexport const schemaTypes = [${exports}]`
206
- const {format} = await import('prettier')
207
206
  return format(fileContents, {parser: 'typescript'})
208
207
  }
209
208