@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.
- package/lib/_chunks-cjs/generateAction.js +118 -96
- package/lib/_chunks-cjs/generateAction.js.map +1 -1
- package/lib/_chunks-cjs/journeyConfig.js +24 -5
- 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/_chunks-cjs/workerChannel.js +84 -0
- package/lib/_chunks-cjs/workerChannel.js.map +1 -0
- package/lib/workers/typegenGenerate.d.ts +104 -36
- package/lib/workers/typegenGenerate.js +24 -111
- package/lib/workers/typegenGenerate.js.map +1 -1
- package/package.json +18 -20
- package/src/actions/typegen/generate.telemetry.ts +5 -3
- package/src/actions/typegen/generateAction.ts +165 -130
- package/src/cli.ts +0 -0
- package/src/commands/projects/listProjectsCommand.ts +0 -0
- package/src/commands/projects/projectsGroup.ts +0 -0
- package/src/util/__tests__/workerChannel.test.ts +222 -0
- package/src/util/journeyConfig.ts +3 -2
- package/src/util/workerChannel.ts +312 -0
- package/src/workers/typegenGenerate.ts +55 -193
@@ -1,15 +1,18 @@
|
|
1
|
+
/* eslint-disable complexity */
|
2
|
+
/* eslint-disable max-statements */
|
3
|
+
/* eslint-disable max-depth */
|
1
4
|
import {constants, mkdir, open, stat} from 'node:fs/promises'
|
2
5
|
import {dirname, join} from 'node:path'
|
3
6
|
import {Worker} from 'node:worker_threads'
|
4
7
|
|
5
|
-
import {readConfig} from '@sanity/codegen'
|
6
|
-
import {format as prettierFormat, resolveConfig as resolvePrettierConfig} from 'prettier'
|
8
|
+
import {readConfig, type TypeEvaluationStats} from '@sanity/codegen'
|
7
9
|
|
8
10
|
import {type CliCommandArguments, type CliCommandContext} from '../../types'
|
9
11
|
import {getCliWorkerPath} from '../../util/cliWorker'
|
12
|
+
import {createReceiver} from '../../util/workerChannel'
|
10
13
|
import {
|
11
14
|
type TypegenGenerateTypesWorkerData,
|
12
|
-
type
|
15
|
+
type TypegenWorkerChannel,
|
13
16
|
} from '../../workers/typegenGenerate'
|
14
17
|
import {TypesGeneratedTrace} from './generate.telemetry'
|
15
18
|
|
@@ -31,6 +34,51 @@ const generatedFileWarning = `/**
|
|
31
34
|
* ---------------------------------------------------------------------------------
|
32
35
|
*/\n\n`
|
33
36
|
|
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
|
+
|
34
82
|
export default async function typegenGenerateAction(
|
35
83
|
args: CliCommandArguments<TypegenGenerateTypesCommandFlags>,
|
36
84
|
context: CliCommandContext,
|
@@ -41,170 +89,157 @@ export default async function typegenGenerateAction(
|
|
41
89
|
const trace = telemetry.trace(TypesGeneratedTrace)
|
42
90
|
trace.start()
|
43
91
|
|
44
|
-
const
|
92
|
+
const typegenConfig = await readConfig(flags['config-path'] ?? 'sanity-typegen.json')
|
45
93
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
94
|
+
const missingSchemas: string[] = []
|
95
|
+
const invalidSchemas: string[] = []
|
96
|
+
|
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
|
+
}
|
50
109
|
}
|
51
|
-
}
|
52
|
-
|
110
|
+
}
|
111
|
+
|
112
|
+
if (missingSchemas.length > 0 || invalidSchemas.length > 0) {
|
113
|
+
const errors: string[] = []
|
114
|
+
|
115
|
+
if (missingSchemas.length > 0) {
|
53
116
|
// If the user has not provided a specific schema path (eg we're using the default), give some help
|
54
117
|
const hint =
|
55
|
-
|
56
|
-
|
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}`)
|
57
128
|
}
|
58
|
-
|
129
|
+
|
130
|
+
throw new Error(errors.join('\n\n'))
|
59
131
|
}
|
60
132
|
|
61
|
-
const outputPath = join(process.cwd(),
|
133
|
+
const outputPath = join(process.cwd(), typegenConfig.generates)
|
62
134
|
const outputDir = dirname(outputPath)
|
63
135
|
await mkdir(outputDir, {recursive: true})
|
64
136
|
const workerPath = await getCliWorkerPath('typegenGenerate')
|
65
137
|
|
66
|
-
const spinner = output.spinner(
|
138
|
+
const spinner = output.spinner('Generating types')
|
67
139
|
|
68
140
|
const worker = new Worker(workerPath, {
|
69
141
|
workerData: {
|
70
142
|
workDir,
|
71
|
-
|
72
|
-
searchPath:
|
73
|
-
overloadClientMethods:
|
143
|
+
schemas: typegenConfig.schemas,
|
144
|
+
searchPath: typegenConfig.path,
|
145
|
+
overloadClientMethods: typegenConfig.overloadClientMethods,
|
146
|
+
augmentGroqModule: typegenConfig.augmentGroqModule,
|
74
147
|
} satisfies TypegenGenerateTypesWorkerData,
|
75
148
|
// eslint-disable-next-line no-process-env
|
76
149
|
env: process.env,
|
77
150
|
})
|
78
151
|
|
79
|
-
const
|
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)
|
152
|
+
const receiver = createReceiver<TypegenWorkerChannel>(worker)
|
86
153
|
|
154
|
+
let fileHandle
|
87
155
|
const stats = {
|
88
156
|
queryFilesCount: 0,
|
89
|
-
errors: 0,
|
90
157
|
queriesCount: 0,
|
158
|
+
projectionsCount: 0,
|
91
159
|
schemaTypesCount: 0,
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
size: 0,
|
160
|
+
typeEvaluationStats: null as TypeEvaluationStats | null,
|
161
|
+
outputSize: 0,
|
162
|
+
filesWithErrors: 0,
|
96
163
|
}
|
97
164
|
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
}
|
165
|
+
try {
|
166
|
+
try {
|
167
|
+
spinner.start()
|
125
168
|
|
126
|
-
|
169
|
+
fileHandle = await open(outputPath, 'w')
|
170
|
+
await fileHandle.write(generatedFileWarning)
|
127
171
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
return
|
133
|
-
}
|
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)
|
134
176
|
|
135
|
-
|
177
|
+
spinner.text = 'Generating query types...'
|
178
|
+
for await (const queryResult of receiver.stream.queries()) {
|
136
179
|
stats.queryFilesCount++
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
stats.queriesCount++
|
149
|
-
stats.typeNodesGenerated += typeNodesGenerated
|
150
|
-
stats.unknownTypeNodesGenerated += unknownTypeNodesGenerated
|
151
|
-
stats.emptyUnionTypeNodesGenerated += emptyUnionTypeNodesGenerated
|
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
|
+
}
|
152
191
|
}
|
153
|
-
typeFile.write(`${fileTypeString}\n`)
|
154
|
-
stats.size += Buffer.byteLength(fileTypeString)
|
155
192
|
}
|
156
|
-
})
|
157
|
-
worker.addListener('error', reject)
|
158
|
-
})
|
159
|
-
|
160
|
-
await typeFile.close()
|
161
193
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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}`)
|
194
|
+
spinner.text = 'Generating typemap...'
|
195
|
+
const {code: typemapCode, stats: finalStats} = await receiver.event.typemap()
|
196
|
+
stats.typeEvaluationStats = finalStats
|
197
|
+
await fileHandle.write(typemapCode)
|
183
198
|
} finally {
|
184
|
-
|
199
|
+
// Ensure the initial file handle is closed before moving on
|
200
|
+
await fileHandle?.close()
|
185
201
|
}
|
186
|
-
}
|
187
202
|
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
})
|
203
|
+
if (typegenConfig.formatGeneratedCode) {
|
204
|
+
await formatGeneratedFile(outputPath, output, spinner)
|
205
|
+
}
|
201
206
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
+
})
|
228
|
+
|
229
|
+
trace.complete()
|
230
|
+
if (stats.filesWithErrors > 0) {
|
231
|
+
spinner.warn(`Encountered errors in ${stats.filesWithErrors} files while generating types`)
|
232
|
+
}
|
206
233
|
|
207
|
-
|
208
|
-
|
209
|
-
|
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()
|
244
|
+
}
|
210
245
|
}
|
package/src/cli.ts
CHANGED
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,222 @@
|
|
1
|
+
import {EventEmitter} from 'node:events'
|
2
|
+
import {type MessagePort, type Worker} from 'node:worker_threads'
|
3
|
+
|
4
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
5
|
+
|
6
|
+
import {
|
7
|
+
createReceiver,
|
8
|
+
createReporter,
|
9
|
+
type WorkerChannel,
|
10
|
+
type WorkerChannelEvent,
|
11
|
+
type WorkerChannelStream,
|
12
|
+
} from '../workerChannel'
|
13
|
+
|
14
|
+
// Define a sample worker channel for testing
|
15
|
+
type TestWorkerChannel = WorkerChannel<{
|
16
|
+
simpleEvent: WorkerChannelEvent<string>
|
17
|
+
dataEvent: WorkerChannelEvent<{id: number; value: boolean}>
|
18
|
+
simpleStream: WorkerChannelStream<number>
|
19
|
+
endStream: WorkerChannelStream<void>
|
20
|
+
}>
|
21
|
+
|
22
|
+
// Mock Worker and MessagePort
|
23
|
+
class MockWorker extends EventEmitter {
|
24
|
+
terminated = false
|
25
|
+
terminate = vi.fn(async () => {
|
26
|
+
this.terminated = true
|
27
|
+
return 0
|
28
|
+
})
|
29
|
+
postMessage = vi.fn((message: unknown) => {
|
30
|
+
this.emit('message', message)
|
31
|
+
})
|
32
|
+
|
33
|
+
// Helper to simulate receiving a message from the parent (if needed)
|
34
|
+
receiveMessage(message: unknown) {
|
35
|
+
this.emit('message', message)
|
36
|
+
}
|
37
|
+
|
38
|
+
// Helper to simulate an error from the worker
|
39
|
+
emitError(error: unknown) {
|
40
|
+
this.emit('error', error)
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
class MockMessagePort extends EventEmitter {
|
45
|
+
postMessage = vi.fn((message: unknown) => {
|
46
|
+
// Simulate the message being sent back to the parent/receiver
|
47
|
+
// In a real scenario, this would go to the Worker's listener
|
48
|
+
mockWorkerInstance?.receiveMessage(message)
|
49
|
+
})
|
50
|
+
|
51
|
+
// Helper to simulate receiving a message (e.g., from the parent)
|
52
|
+
receiveMessage(message: unknown) {
|
53
|
+
this.emit('message', message)
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
let mockWorkerInstance: MockWorker
|
58
|
+
let mockParentPortInstance: MockMessagePort
|
59
|
+
let receiver: ReturnType<typeof createReceiver<TestWorkerChannel>>
|
60
|
+
let reporter: ReturnType<typeof createReporter<TestWorkerChannel>>
|
61
|
+
|
62
|
+
beforeEach(() => {
|
63
|
+
mockWorkerInstance = new MockWorker()
|
64
|
+
mockParentPortInstance = new MockMessagePort()
|
65
|
+
receiver = createReceiver<TestWorkerChannel>(mockWorkerInstance as unknown as Worker)
|
66
|
+
reporter = createReporter<TestWorkerChannel>(mockParentPortInstance as unknown as MessagePort)
|
67
|
+
})
|
68
|
+
|
69
|
+
afterEach(() => {
|
70
|
+
vi.clearAllMocks()
|
71
|
+
})
|
72
|
+
|
73
|
+
describe('workerChannel', () => {
|
74
|
+
it('should send and receive a simple event', async () => {
|
75
|
+
const receivedPromise = receiver.event.simpleEvent()
|
76
|
+
reporter.event.simpleEvent('hello')
|
77
|
+
|
78
|
+
await expect(receivedPromise).resolves.toBe('hello')
|
79
|
+
})
|
80
|
+
|
81
|
+
it('should send and receive an event with data object', async () => {
|
82
|
+
const payload = {id: 123, value: true}
|
83
|
+
const receivedPromise = receiver.event.dataEvent()
|
84
|
+
reporter.event.dataEvent(payload)
|
85
|
+
|
86
|
+
await expect(receivedPromise).resolves.toEqual(payload)
|
87
|
+
})
|
88
|
+
|
89
|
+
it('should send and receive a stream of data', async () => {
|
90
|
+
const receivedItems: number[] = []
|
91
|
+
const streamPromise = (async () => {
|
92
|
+
for await (const item of receiver.stream.simpleStream()) {
|
93
|
+
receivedItems.push(item)
|
94
|
+
}
|
95
|
+
})()
|
96
|
+
|
97
|
+
reporter.stream.simpleStream.emit(1)
|
98
|
+
reporter.stream.simpleStream.emit(2)
|
99
|
+
reporter.stream.simpleStream.emit(3)
|
100
|
+
reporter.stream.simpleStream.end()
|
101
|
+
|
102
|
+
await streamPromise // Wait for the stream processing to complete
|
103
|
+
|
104
|
+
expect(receivedItems).toEqual([1, 2, 3])
|
105
|
+
})
|
106
|
+
|
107
|
+
it('should handle an empty stream correctly', async () => {
|
108
|
+
let streamEntered = false
|
109
|
+
const streamPromise = (async () => {
|
110
|
+
for await (const _item of receiver.stream.endStream()) {
|
111
|
+
streamEntered = true // This should not happen
|
112
|
+
}
|
113
|
+
})()
|
114
|
+
|
115
|
+
reporter.stream.endStream.end() // End immediately
|
116
|
+
|
117
|
+
await streamPromise
|
118
|
+
|
119
|
+
expect(streamEntered).toBe(false)
|
120
|
+
})
|
121
|
+
|
122
|
+
it('should propagate errors from the worker via event receiver', async () => {
|
123
|
+
const error = new Error('Worker failed')
|
124
|
+
|
125
|
+
const receivedPromise = receiver.event.simpleEvent()
|
126
|
+
mockWorkerInstance?.emitError(error)
|
127
|
+
|
128
|
+
await expect(receivedPromise).rejects.toThrow(error)
|
129
|
+
})
|
130
|
+
|
131
|
+
it('should propagate errors from the worker via stream receiver', async () => {
|
132
|
+
const error = new Error('Worker failed during stream')
|
133
|
+
|
134
|
+
const streamPromise = (async () => {
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
136
|
+
for await (const _item of receiver.stream.simpleStream()) {
|
137
|
+
// no-op
|
138
|
+
}
|
139
|
+
})()
|
140
|
+
|
141
|
+
// Emit error before ending the stream
|
142
|
+
mockWorkerInstance?.emitError(error)
|
143
|
+
|
144
|
+
await expect(streamPromise).rejects.toThrow(error)
|
145
|
+
})
|
146
|
+
|
147
|
+
it('should handle messages arriving before receiver awaits', async () => {
|
148
|
+
// Reporter sends event *before* receiver awaits it
|
149
|
+
reporter.event.simpleEvent('early bird')
|
150
|
+
|
151
|
+
// Give a tick for the message to be processed internally by the mock
|
152
|
+
await new Promise((resolve) => setImmediate(resolve))
|
153
|
+
|
154
|
+
const receivedPromise = receiver.event.simpleEvent()
|
155
|
+
|
156
|
+
await expect(receivedPromise).resolves.toBe('early bird')
|
157
|
+
})
|
158
|
+
|
159
|
+
it('should handle stream emissions arriving before receiver iterates', async () => {
|
160
|
+
// Reporter sends stream data *before* receiver starts iterating
|
161
|
+
reporter.stream.simpleStream.emit(10)
|
162
|
+
reporter.stream.simpleStream.emit(20)
|
163
|
+
|
164
|
+
// Give a tick for messages to process
|
165
|
+
await new Promise((resolve) => setImmediate(resolve))
|
166
|
+
|
167
|
+
const receivedItems: number[] = []
|
168
|
+
const streamPromise = (async () => {
|
169
|
+
for await (const item of receiver.stream.simpleStream()) {
|
170
|
+
receivedItems.push(item)
|
171
|
+
}
|
172
|
+
})()
|
173
|
+
|
174
|
+
// Send remaining data and end
|
175
|
+
reporter.stream.simpleStream.emit(30)
|
176
|
+
reporter.stream.simpleStream.end()
|
177
|
+
|
178
|
+
await streamPromise
|
179
|
+
|
180
|
+
expect(receivedItems).toEqual([10, 20, 30])
|
181
|
+
})
|
182
|
+
|
183
|
+
it('dispose() should remove listeners and terminate worker', async () => {
|
184
|
+
expect(mockWorkerInstance?.listenerCount('message')).toBe(1)
|
185
|
+
expect(mockWorkerInstance?.listenerCount('error')).toBe(1)
|
186
|
+
|
187
|
+
const terminatePromise = receiver.dispose()
|
188
|
+
|
189
|
+
await expect(terminatePromise).resolves.toBe(0)
|
190
|
+
expect(mockWorkerInstance?.terminate).toHaveBeenCalledTimes(1)
|
191
|
+
expect(mockWorkerInstance?.listenerCount('message')).toBe(0)
|
192
|
+
expect(mockWorkerInstance?.listenerCount('error')).toBe(0)
|
193
|
+
expect(mockWorkerInstance?.terminated).toBe(true)
|
194
|
+
})
|
195
|
+
|
196
|
+
it('should throw error if parentPort is null for reporter', () => {
|
197
|
+
expect(() => createReporter<TestWorkerChannel>(null)).toThrow('parentPart was falsy')
|
198
|
+
})
|
199
|
+
|
200
|
+
it('should ignore non-worker channel messages', async () => {
|
201
|
+
const receivedPromise = receiver.event.simpleEvent()
|
202
|
+
|
203
|
+
// Send a valid message
|
204
|
+
reporter.event.simpleEvent('valid')
|
205
|
+
await expect(receivedPromise).resolves.toBe('valid')
|
206
|
+
|
207
|
+
const nextReceivedPromise = receiver.event.simpleEvent()
|
208
|
+
|
209
|
+
// Send an invalid message
|
210
|
+
mockWorkerInstance?.receiveMessage({foo: 'bar'}) // Not a valid WorkerChannelMessage
|
211
|
+
mockWorkerInstance?.receiveMessage('just a string')
|
212
|
+
mockWorkerInstance?.receiveMessage(null)
|
213
|
+
mockWorkerInstance?.receiveMessage(undefined)
|
214
|
+
mockWorkerInstance?.receiveMessage({type: 'unknown'})
|
215
|
+
|
216
|
+
// Send the actual message we are waiting for
|
217
|
+
reporter.event.simpleEvent('after invalid')
|
218
|
+
|
219
|
+
// It should eventually resolve with the correct message, ignoring the invalid ones
|
220
|
+
await expect(nextReceivedPromise).resolves.toBe('after invalid')
|
221
|
+
})
|
222
|
+
})
|
@@ -7,7 +7,6 @@ import {
|
|
7
7
|
type DocumentDefinition,
|
8
8
|
type ObjectDefinition,
|
9
9
|
} from '@sanity/types'
|
10
|
-
import {format} from 'prettier'
|
11
10
|
|
12
11
|
import {type CliApiClient} from '../types'
|
13
12
|
import {getCliWorkerPath} from './cliWorker'
|
@@ -184,6 +183,7 @@ async function fetchJourneySchema(schemaUrl: string): Promise<DocumentOrObject[]
|
|
184
183
|
async function assembleJourneySchemaTypeFileContent(schemaType: DocumentOrObject): Promise<string> {
|
185
184
|
const serialised = wrapSchemaTypeInHelpers(schemaType)
|
186
185
|
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,11 +198,12 @@ 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
|
-
function assembleJourneyIndexContent(schemas: DocumentOrObject[]): Promise<string> {
|
201
|
+
async 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')
|
206
207
|
return format(fileContents, {parser: 'typescript'})
|
207
208
|
}
|
208
209
|
|