@sanity/cli 3.86.2-experimental.0 → 3.87.1-canary.4
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.
- package/lib/_chunks-cjs/cli.js +63 -25
- package/lib/_chunks-cjs/cli.js.map +1 -1
- package/lib/_chunks-cjs/generateAction.js +96 -118
- package/lib/_chunks-cjs/generateAction.js.map +1 -1
- package/lib/_chunks-cjs/journeyConfig.js +5 -24
- package/lib/_chunks-cjs/journeyConfig.js.map +1 -1
- package/lib/_chunks-cjs/loadEnv.js +3 -3
- package/lib/_chunks-cjs/loadEnv.js.map +1 -1
- package/lib/workers/typegenGenerate.d.ts +36 -104
- package/lib/workers/typegenGenerate.js +111 -24
- package/lib/workers/typegenGenerate.js.map +1 -1
- package/package.json +21 -19
- package/src/actions/init-project/initProject.ts +88 -26
- package/src/actions/typegen/generate.telemetry.ts +3 -5
- package/src/actions/typegen/generateAction.ts +130 -165
- package/src/cli.ts +20 -6
- package/src/commands/projects/listProjectsCommand.ts +0 -0
- package/src/commands/projects/projectsGroup.ts +0 -0
- package/src/util/journeyConfig.ts +2 -3
- package/src/workers/typegenGenerate.ts +193 -55
- package/templates/app-sanity-ui/src/App.tsx +5 -5
- package/lib/_chunks-cjs/workerChannel.js +0 -84
- package/lib/_chunks-cjs/workerChannel.js.map +0 -1
- package/src/util/__tests__/workerChannel.test.ts +0 -222
- package/src/util/workerChannel.ts +0 -312
@@ -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
|
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
|
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
|
93
|
-
|
94
|
-
const missingSchemas: string[] = []
|
95
|
-
const invalidSchemas: string[] = []
|
44
|
+
const codegenConfig = await readConfig(flags['config-path'] || 'sanity-typegen.json')
|
96
45
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
119
|
-
|
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(),
|
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
|
-
|
144
|
-
searchPath:
|
145
|
-
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
|
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
|
-
|
161
|
-
|
162
|
-
|
92
|
+
unknownTypeNodesGenerated: 0,
|
93
|
+
typeNodesGenerated: 0,
|
94
|
+
emptyUnionTypeNodesGenerated: 0,
|
95
|
+
size: 0,
|
163
96
|
}
|
164
97
|
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
170
|
-
await fileHandle.write(generatedFileWarning)
|
126
|
+
let fileTypeString = `// Source: ${msg.filename}\n`
|
171
127
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
128
|
+
if (msg.type === 'schema') {
|
129
|
+
stats.schemaTypesCount += msg.length
|
130
|
+
fileTypeString += msg.schema
|
131
|
+
typeFile.write(fileTypeString)
|
132
|
+
return
|
133
|
+
}
|
176
134
|
|
177
|
-
|
178
|
-
for await (const queryResult of receiver.stream.queries()) {
|
135
|
+
if (msg.type === 'types') {
|
179
136
|
stats.queryFilesCount++
|
180
|
-
const {
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
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({
|
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
|
-
|
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
|
|