@sanity/cli 3.86.1 → 3.86.2-experimental.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.
@@ -0,0 +1,312 @@
1
+ // NOTE: this file was originally copied from
2
+ // https://github.com/sanity-io/sanity/blob/4c4e03d407106dbda12f52cfd9511fbfe75a9696/packages/sanity/src/_internal/cli/util/workerChannels.ts
3
+ import {type MessagePort, type Worker} from 'node:worker_threads'
4
+
5
+ type StreamReporter<TPayload = unknown> = {emit: (payload: TPayload) => void; end: () => void}
6
+ type EventReporter<TPayload = unknown> = (payload: TPayload) => void
7
+ type EventReceiver<TPayload = unknown> = () => Promise<TPayload>
8
+ type StreamReceiver<TPayload = unknown> = () => AsyncIterable<TPayload>
9
+
10
+ type EventKeys<TWorkerChannel extends WorkerChannel> = {
11
+ [K in keyof TWorkerChannel]: TWorkerChannel[K] extends WorkerChannelEvent<any> ? K : never
12
+ }[keyof TWorkerChannel]
13
+ type StreamKeys<TWorkerChannel extends WorkerChannel> = {
14
+ [K in keyof TWorkerChannel]: TWorkerChannel[K] extends WorkerChannelStream<any> ? K : never
15
+ }[keyof TWorkerChannel]
16
+
17
+ type EventMessage<TPayload = unknown> = {type: 'event'; name: string; payload: TPayload}
18
+ type StreamEmissionMessage<TPayload = unknown> = {type: 'emission'; name: string; payload: TPayload}
19
+ type StreamEndMessage = {type: 'end'; name: string}
20
+ type WorkerChannelMessage = EventMessage | StreamEmissionMessage | StreamEndMessage
21
+
22
+ /**
23
+ * Represents the definition of a "worker channel" to report progress from the
24
+ * worker to the parent. Worker channels can define named events or streams and
25
+ * the worker will report events and streams while the parent will await them.
26
+ * This allows the control flow of the parent to follow the control flow of the
27
+ * worker 1-to-1.
28
+ *
29
+ * @example
30
+ *
31
+ * ```ts
32
+ * // Define the channel interface (shared between parent and worker)
33
+ * type MyWorkerChannel = WorkerChannel<{
34
+ * compileStart: WorkerChannelEvent<void>
35
+ * compileProgress: WorkerChannelStream<{ file: string; progress: number }>
36
+ * compileEnd: WorkerChannelEvent<{ duration: number }>
37
+ * }>;
38
+ *
39
+ * // --- In the worker file (e.g., worker.ts) ---
40
+ * import { parentPort } from 'node:worker_threads';
41
+ * import { createReporter } from './workerChannels';
42
+ *
43
+ * const report = createReporter<MyWorkerChannel>(parentPort);
44
+ *
45
+ * async function runCompilation() {
46
+ * report.event.compileStart(); // Signal start
47
+ *
48
+ * const files = ['a.js', 'b.js', 'c.js'];
49
+ * for (const file of files) {
50
+ * // Simulate work and report progress
51
+ * await new Promise(resolve => setTimeout(resolve, 100));
52
+ * report.stream.compileProgress.emit({ file, progress: 100 });
53
+ * }
54
+ * report.stream.compileProgress.end(); // Signal end of progress stream
55
+ *
56
+ * report.event.compileEnd({ duration: 300 }); // Signal end with result
57
+ * }
58
+ *
59
+ * runCompilation();
60
+ *
61
+ * // --- In the parent file (e.g., main.ts) ---
62
+ * import { Worker } from 'node:worker_threads';
63
+ * import { createReceiver } from './workerChannels';
64
+ *
65
+ * const worker = new Worker('./worker.js');
66
+ * const receiver = createReceiver<MyWorkerChannel>(worker);
67
+ *
68
+ * async function monitorCompilation() {
69
+ * console.log('Waiting for compilation to start...');
70
+ * await receiver.event.compileStart();
71
+ * console.log('Compilation started.');
72
+ *
73
+ * console.log('Receiving progress:');
74
+ * for await (const progress of receiver.stream.compileProgress()) {
75
+ * console.log(` - ${progress.file}: ${progress.progress}%`);
76
+ * }
77
+ *
78
+ * console.log('Waiting for compilation to end...');
79
+ * const { duration } = await receiver.event.compileEnd();
80
+ * console.log(`Compilation finished in ${duration}ms.`);
81
+ *
82
+ * await receiver.dispose(); // Clean up listeners and terminate worker
83
+ * }
84
+ *
85
+ * monitorCompilation();
86
+ * ```
87
+ *
88
+ * @internal
89
+ */
90
+ export type WorkerChannel<
91
+ TWorkerChannel extends Record<
92
+ string,
93
+ WorkerChannelEvent<unknown> | WorkerChannelStream<unknown>
94
+ > = Record<string, WorkerChannelEvent<unknown> | WorkerChannelStream<unknown>>,
95
+ > = TWorkerChannel
96
+
97
+ /** @internal */
98
+ export type WorkerChannelEvent<TPayload = void> = {type: 'event'; payload: TPayload}
99
+ /** @internal */
100
+ export type WorkerChannelStream<TPayload = void> = {type: 'stream'; payload: TPayload}
101
+
102
+ export interface WorkerChannelReporter<TWorkerChannel extends WorkerChannel> {
103
+ event: {
104
+ [K in EventKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelEvent<infer TPayload>
105
+ ? EventReporter<TPayload>
106
+ : void
107
+ }
108
+ stream: {
109
+ [K in StreamKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelStream<infer TPayload>
110
+ ? StreamReporter<TPayload>
111
+ : void
112
+ }
113
+ }
114
+
115
+ export interface WorkerChannelReceiver<TWorkerChannel extends WorkerChannel> {
116
+ event: {
117
+ [K in EventKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelEvent<infer TPayload>
118
+ ? EventReceiver<TPayload>
119
+ : void
120
+ }
121
+ stream: {
122
+ [K in StreamKeys<TWorkerChannel>]: TWorkerChannel[K] extends WorkerChannelStream<infer TPayload>
123
+ ? StreamReceiver<TPayload>
124
+ : void
125
+ }
126
+ // TODO: good candidate for [Symbol.asyncDispose] when our tooling better supports it
127
+ dispose: () => Promise<number>
128
+ }
129
+
130
+ /**
131
+ * A simple queue that has two primary methods: `push(message)` and
132
+ * `await next()`. This message queue is used by the "receiver" of the worker
133
+ * channel and this class handles buffering incoming messages if the worker is
134
+ * producing faster than the parent as well as returning a promise if there is
135
+ * no message yet in the queue when the parent awaits `next()`.
136
+ */
137
+ class MessageQueue<T> {
138
+ resolver: ((result: IteratorResult<T>) => void) | null = null
139
+ queue: T[] = []
140
+ private ended = false // Flag to indicate if end() was called
141
+
142
+ push(message: T) {
143
+ if (this.ended) {
144
+ // Don't push messages after the queue has ended
145
+ return
146
+ }
147
+ if (this.resolver) {
148
+ this.resolver({value: message, done: false})
149
+ this.resolver = null
150
+ } else {
151
+ this.queue.push(message)
152
+ }
153
+ }
154
+
155
+ next(): Promise<IteratorResult<T>> {
156
+ if (this.queue.length) {
157
+ return Promise.resolve({value: this.queue.shift()!, done: false})
158
+ }
159
+
160
+ if (this.ended) {
161
+ // If end() was called before and queue is empty, resolve immediately as done
162
+ return Promise.resolve({value: undefined, done: true})
163
+ }
164
+
165
+ return new Promise((resolve) => (this.resolver = resolve))
166
+ }
167
+
168
+ end() {
169
+ if (this.resolver) {
170
+ this.resolver({value: undefined, done: true})
171
+ this.resolver = null // Clear resolver after ending
172
+ } else {
173
+ // If resolver is null, it means next() hasn't been called yet or
174
+ // previous next() was resolved by a push(). Mark as ended so the
175
+ // *next* call to next() resolves immediately as done.
176
+ this.ended = true
177
+ }
178
+ }
179
+ }
180
+
181
+ function isWorkerChannelMessage(message: unknown): message is WorkerChannelMessage {
182
+ if (typeof message !== 'object') return false
183
+ if (!message) return false
184
+ if (!('type' in message)) return false
185
+ if (typeof message.type !== 'string') return false
186
+ const types: string[] = ['event', 'emission', 'end'] satisfies WorkerChannelMessage['type'][]
187
+ return types.includes(message.type)
188
+ }
189
+
190
+ /**
191
+ * Creates a "worker channel receiver" that subscribes to incoming messages
192
+ * from the given worker and returns promises for worker channel events and
193
+ * async iterators for worker channel streams.
194
+ */
195
+ export function createReceiver<TWorkerChannel extends WorkerChannel>(
196
+ worker: Worker,
197
+ ): WorkerChannelReceiver<TWorkerChannel> {
198
+ const _events = new Map<string, MessageQueue<EventMessage>>()
199
+ const _streams = new Map<string, MessageQueue<StreamEmissionMessage>>()
200
+ const errors = new MessageQueue<{type: 'error'; error: unknown}>()
201
+
202
+ const eventQueue = (name: string) => {
203
+ const queue = _events.get(name) ?? new MessageQueue()
204
+ if (!_events.has(name)) _events.set(name, queue)
205
+ return queue
206
+ }
207
+
208
+ const streamQueue = (name: string) => {
209
+ const queue = _streams.get(name) ?? new MessageQueue()
210
+ if (!_streams.has(name)) _streams.set(name, queue)
211
+ return queue
212
+ }
213
+
214
+ const handleMessage = (message: unknown) => {
215
+ if (!isWorkerChannelMessage(message)) return
216
+ if (message.type === 'event') eventQueue(message.name).push(message)
217
+ if (message.type === 'emission') streamQueue(message.name).push(message)
218
+ if (message.type === 'end') streamQueue(message.name).end()
219
+ }
220
+
221
+ const handleError = (error: unknown) => {
222
+ errors.push({type: 'error', error})
223
+ }
224
+
225
+ worker.addListener('message', handleMessage)
226
+ worker.addListener('error', handleError)
227
+
228
+ return {
229
+ event: new Proxy({} as WorkerChannelReceiver<TWorkerChannel>['event'], {
230
+ get: (target, name) => {
231
+ if (typeof name !== 'string') return target[name as keyof typeof target]
232
+
233
+ const eventReceiver: EventReceiver = async () => {
234
+ const {value} = await Promise.race([eventQueue(name).next(), errors.next()])
235
+ if (value.type === 'error') throw value.error
236
+ return value.payload
237
+ }
238
+
239
+ return eventReceiver
240
+ },
241
+ }),
242
+ stream: new Proxy({} as WorkerChannelReceiver<TWorkerChannel>['stream'], {
243
+ get: (target, prop) => {
244
+ if (typeof prop !== 'string') return target[prop as keyof typeof target]
245
+ const name = prop // alias for better typescript narrowing
246
+
247
+ async function* streamReceiver() {
248
+ while (true) {
249
+ const {value, done} = await Promise.race([streamQueue(name).next(), errors.next()])
250
+ if (done) return
251
+ if (value.type === 'error') throw value.error
252
+ yield value.payload
253
+ }
254
+ }
255
+
256
+ return streamReceiver satisfies StreamReceiver
257
+ },
258
+ }),
259
+ dispose: () => {
260
+ worker.removeListener('message', handleMessage)
261
+ worker.removeListener('error', handleError)
262
+ return worker.terminate()
263
+ },
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Creates a "worker channel reporter" that sends messages to the given
269
+ * `parentPort` to be received by a worker channel receiver.
270
+ *
271
+ * @internal
272
+ */
273
+ export function createReporter<TWorkerChannel extends WorkerChannel>(
274
+ parentPort: MessagePort | null,
275
+ ): WorkerChannelReporter<TWorkerChannel> {
276
+ if (!parentPort) {
277
+ throw new Error('parentPart was falsy')
278
+ }
279
+
280
+ return {
281
+ event: new Proxy({} as WorkerChannelReporter<TWorkerChannel>['event'], {
282
+ get: (target, name) => {
283
+ if (typeof name !== 'string') return target[name as keyof typeof target]
284
+
285
+ const eventReporter: EventReporter = (payload) => {
286
+ const message: EventMessage = {type: 'event', name, payload}
287
+ parentPort.postMessage(message)
288
+ }
289
+
290
+ return eventReporter
291
+ },
292
+ }),
293
+ stream: new Proxy({} as WorkerChannelReporter<TWorkerChannel>['stream'], {
294
+ get: (target, name) => {
295
+ if (typeof name !== 'string') return target[name as keyof typeof target]
296
+
297
+ const streamReporter: StreamReporter = {
298
+ emit: (payload) => {
299
+ const message: StreamEmissionMessage = {type: 'emission', name, payload}
300
+ parentPort.postMessage(message)
301
+ },
302
+ end: () => {
303
+ const message: StreamEndMessage = {type: 'end', name}
304
+ parentPort.postMessage(message)
305
+ },
306
+ }
307
+
308
+ return streamReporter
309
+ },
310
+ }),
311
+ }
312
+ }
@@ -2,228 +2,90 @@ import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_t
2
2
 
3
3
  import {
4
4
  findQueriesInPath,
5
+ type GeneratedQueries,
6
+ type GeneratedSchema,
7
+ type GeneratedTypemap,
8
+ generateTypes,
5
9
  getResolver,
6
10
  readSchema,
7
11
  registerBabel,
8
- safeParseQuery,
9
- TypeGenerator,
10
12
  } from '@sanity/codegen'
11
13
  import createDebug from 'debug'
12
- import {typeEvaluate, type TypeNode} from 'groq-js'
14
+ import {type SchemaType} from 'groq-js'
15
+
16
+ import {
17
+ createReporter,
18
+ type WorkerChannel,
19
+ type WorkerChannelEvent,
20
+ type WorkerChannelStream,
21
+ } from '../util/workerChannel'
13
22
 
14
23
  const $info = createDebug('sanity:codegen:generate:info')
15
- const $warn = createDebug('sanity:codegen:generate:warn')
16
24
 
17
25
  export interface TypegenGenerateTypesWorkerData {
18
26
  workDir: string
19
- workspaceName?: string
20
- schemaPath: string
27
+ schemas: {
28
+ projectId: string | 'default'
29
+ dataset: string | 'default'
30
+ schemaPath: string
31
+ }[]
21
32
  searchPath: string | string[]
22
- overloadClientMethods?: boolean
33
+ overloadClientMethods: boolean
34
+ augmentGroqModule: boolean
23
35
  }
24
36
 
25
- export type TypegenGenerateTypesWorkerMessage =
26
- | {
27
- type: 'error'
28
- error: Error
29
- fatal: boolean
30
- query?: string
31
- filename?: string
32
- }
33
- | {
34
- type: 'types'
35
- filename: string
36
- types: {
37
- queryName: string
38
- query: string
39
- type: string
40
- unknownTypeNodesGenerated: number
41
- typeNodesGenerated: number
42
- emptyUnionTypeNodesGenerated: number
43
- }[]
44
- }
45
- | {
46
- type: 'schema'
47
- filename: string
48
- schema: string
49
- length: number
50
- }
51
- | {
52
- type: 'typemap'
53
- typeMap: string
54
- }
55
- | {
56
- type: 'complete'
57
- }
37
+ /** @internal */
38
+ export type TypegenWorkerChannel = WorkerChannel<{
39
+ schema: WorkerChannelEvent<GeneratedSchema>
40
+ queries: WorkerChannelStream<GeneratedQueries>
41
+ typemap: WorkerChannelEvent<GeneratedTypemap>
42
+ }>
58
43
 
59
44
  if (isMainThread || !parentPort) {
60
45
  throw new Error('This module must be run as a worker thread')
61
46
  }
62
47
 
48
+ const report = createReporter<TypegenWorkerChannel>(parentPort)
63
49
  const opts = _workerData as TypegenGenerateTypesWorkerData
64
50
 
65
- registerBabel()
66
-
67
51
  async function main() {
68
- const schema = await readSchema(opts.schemaPath)
52
+ const schemas: {
53
+ schema: SchemaType
54
+ projectId: string | 'default'
55
+ dataset: string | 'default'
56
+ filename: string
57
+ }[] = []
58
+
59
+ for (const schemaConfig of opts.schemas) {
60
+ $info(`Reading schema from ${schemaConfig.schemaPath}...`)
61
+ const schema = await readSchema(schemaConfig.schemaPath)
62
+ schemas.push({
63
+ schema,
64
+ projectId: schemaConfig.projectId,
65
+ dataset: schemaConfig.dataset,
66
+ filename: schemaConfig.schemaPath,
67
+ })
68
+ }
69
+ $info(`Read ${schemas.length} schema definition${schemas.length === 1 ? '' : 's'} successfully.`)
69
70
 
70
- const typeGenerator = new TypeGenerator(schema)
71
- const schemaTypes = [typeGenerator.generateSchemaTypes(), TypeGenerator.generateKnownTypes()]
72
- .join('\n')
73
- .trim()
74
71
  const resolver = getResolver()
75
72
 
76
- parentPort?.postMessage({
77
- type: 'schema',
78
- schema: `${schemaTypes.trim()}\n`,
79
- filename: 'schema.json',
80
- length: schema.length,
81
- } satisfies TypegenGenerateTypesWorkerMessage)
82
-
83
- const queries = findQueriesInPath({
84
- path: opts.searchPath,
85
- resolver,
73
+ const result = generateTypes({
74
+ schemas,
75
+ queriesByFile: findQueriesInPath({path: opts.searchPath, resolver}),
76
+ augmentGroqModule: opts.augmentGroqModule,
77
+ overloadClientMethods: opts.overloadClientMethods,
86
78
  })
87
79
 
88
- const allQueries = []
89
-
90
- for await (const result of queries) {
91
- if (result.type === 'error') {
92
- parentPort?.postMessage({
93
- type: 'error',
94
- error: result.error,
95
- fatal: false,
96
- filename: result.filename,
97
- } satisfies TypegenGenerateTypesWorkerMessage)
98
- continue
99
- }
100
- $info(`Processing ${result.queries.length} queries in "${result.filename}"...`)
101
-
102
- const fileQueryTypes: {
103
- queryName: string
104
- query: string
105
- type: string
106
- typeName: string
107
- typeNode: TypeNode
108
- unknownTypeNodesGenerated: number
109
- typeNodesGenerated: number
110
- emptyUnionTypeNodesGenerated: number
111
- }[] = []
112
- for (const {name: queryName, result: query} of result.queries) {
113
- try {
114
- const ast = safeParseQuery(query)
115
- const queryTypes = typeEvaluate(ast, schema)
116
-
117
- const typeName = `${queryName}Result`
118
- const type = typeGenerator.generateTypeNodeTypes(typeName, queryTypes)
119
-
120
- const queryTypeStats = walkAndCountQueryTypeNodeStats(queryTypes)
121
- fileQueryTypes.push({
122
- queryName,
123
- query,
124
- typeName,
125
- typeNode: queryTypes,
126
- type: `${type.trim()}\n`,
127
- unknownTypeNodesGenerated: queryTypeStats.unknownTypes,
128
- typeNodesGenerated: queryTypeStats.allTypes,
129
- emptyUnionTypeNodesGenerated: queryTypeStats.emptyUnions,
130
- })
131
- } catch (err) {
132
- parentPort?.postMessage({
133
- type: 'error',
134
- error: new Error(
135
- `Error generating types for query "${queryName}" in "${result.filename}": ${err.message}`,
136
- {cause: err},
137
- ),
138
- fatal: false,
139
- query,
140
- } satisfies TypegenGenerateTypesWorkerMessage)
141
- }
142
- }
80
+ report.event.schema(await result.generatedSchema())
143
81
 
144
- if (fileQueryTypes.length > 0) {
145
- $info(`Generated types for ${fileQueryTypes.length} queries in "${result.filename}"\n`)
146
- parentPort?.postMessage({
147
- type: 'types',
148
- types: fileQueryTypes,
149
- filename: result.filename,
150
- } satisfies TypegenGenerateTypesWorkerMessage)
151
- }
152
-
153
- if (fileQueryTypes.length > 0) {
154
- allQueries.push(...fileQueryTypes)
155
- }
156
- }
157
-
158
- if (opts.overloadClientMethods && allQueries.length > 0) {
159
- const typeMap = `${typeGenerator.generateQueryMap(allQueries).trim()}\n`
160
- parentPort?.postMessage({
161
- type: 'typemap',
162
- typeMap,
163
- } satisfies TypegenGenerateTypesWorkerMessage)
82
+ for await (const {filename, results} of result.generatedQueries()) {
83
+ report.stream.queries.emit({filename, results})
164
84
  }
85
+ report.stream.queries.end()
165
86
 
166
- parentPort?.postMessage({
167
- type: 'complete',
168
- } satisfies TypegenGenerateTypesWorkerMessage)
169
- }
170
-
171
- function walkAndCountQueryTypeNodeStats(typeNode: TypeNode): {
172
- allTypes: number
173
- unknownTypes: number
174
- emptyUnions: number
175
- } {
176
- switch (typeNode.type) {
177
- case 'unknown': {
178
- return {allTypes: 1, unknownTypes: 1, emptyUnions: 0}
179
- }
180
- case 'array': {
181
- const acc = walkAndCountQueryTypeNodeStats(typeNode.of)
182
- acc.allTypes += 1 // count the array type itself
183
- return acc
184
- }
185
- case 'object': {
186
- // if the rest is unknown, we count it as one unknown type
187
- if (typeNode.rest && typeNode.rest.type === 'unknown') {
188
- return {allTypes: 2, unknownTypes: 1, emptyUnions: 0} // count the object type itself as well
189
- }
190
-
191
- const restStats = typeNode.rest
192
- ? walkAndCountQueryTypeNodeStats(typeNode.rest)
193
- : {allTypes: 1, unknownTypes: 0, emptyUnions: 0} // count the object type itself
194
-
195
- return Object.values(typeNode.attributes).reduce((acc, attribute) => {
196
- const {allTypes, unknownTypes, emptyUnions} = walkAndCountQueryTypeNodeStats(
197
- attribute.value,
198
- )
199
- return {
200
- allTypes: acc.allTypes + allTypes,
201
- unknownTypes: acc.unknownTypes + unknownTypes,
202
- emptyUnions: acc.emptyUnions + emptyUnions,
203
- }
204
- }, restStats)
205
- }
206
- case 'union': {
207
- if (typeNode.of.length === 0) {
208
- return {allTypes: 1, unknownTypes: 0, emptyUnions: 1}
209
- }
210
-
211
- return typeNode.of.reduce(
212
- (acc, type) => {
213
- const {allTypes, unknownTypes, emptyUnions} = walkAndCountQueryTypeNodeStats(type)
214
- return {
215
- allTypes: acc.allTypes + allTypes,
216
- unknownTypes: acc.unknownTypes + unknownTypes,
217
- emptyUnions: acc.emptyUnions + emptyUnions,
218
- }
219
- },
220
- {allTypes: 1, unknownTypes: 0, emptyUnions: 0}, // count the union type itself
221
- )
222
- }
223
- default: {
224
- return {allTypes: 1, unknownTypes: 0, emptyUnions: 0}
225
- }
226
- }
87
+ report.event.typemap(await result.generatedTypemap())
227
88
  }
228
89
 
90
+ registerBabel()
229
91
  main()