moonflower 1.5.0 → 1.5.2
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/dist/openapi/analyzerModule/analyzerModule.cjs +1 -1
- package/dist/openapi/analyzerModule/analyzerModule.cjs.map +1 -1
- package/dist/openapi/analyzerModule/analyzerModule.d.ts.map +1 -1
- package/dist/openapi/analyzerModule/analyzerModule.mjs +159 -147
- package/dist/openapi/analyzerModule/analyzerModule.mjs.map +1 -1
- package/dist/openapi/analyzerModule/analyzerWorker.cjs +1 -1
- package/dist/openapi/analyzerModule/analyzerWorker.cjs.map +1 -1
- package/dist/openapi/analyzerModule/analyzerWorker.mjs +41 -33
- package/dist/openapi/analyzerModule/analyzerWorker.mjs.map +1 -1
- package/dist/openapi/analyzerModule/workerPool.cjs +1 -1
- package/dist/openapi/analyzerModule/workerPool.cjs.map +1 -1
- package/dist/openapi/analyzerModule/workerPool.d.ts +11 -6
- package/dist/openapi/analyzerModule/workerPool.d.ts.map +1 -1
- package/dist/openapi/analyzerModule/workerPool.mjs +23 -23
- package/dist/openapi/analyzerModule/workerPool.mjs.map +1 -1
- package/package.json +1 -1
- package/src/openapi/analyzerModule/analyzerModule.ts +56 -62
- package/src/openapi/analyzerModule/analyzerWorker.ts +30 -26
- package/src/openapi/analyzerModule/workerPool.ts +17 -7
|
@@ -53,6 +53,13 @@ type FileDiscoveryConfig = {
|
|
|
53
53
|
|
|
54
54
|
type EndpointTiming = { method: string; path: string; timing: number; sectionTimings: SectionTiming[] }
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Number of uncached files at or below which analysis runs inline on the (already-warm) main-thread
|
|
58
|
+
* Project instead of fanning out to worker threads. Each worker pays a multi-second cold-start to
|
|
59
|
+
* build its own Project, so parallelism only wins once there are several files to share that cost.
|
|
60
|
+
*/
|
|
61
|
+
const INLINE_ANALYSIS_FILE_THRESHOLD = 2
|
|
62
|
+
|
|
56
63
|
/**
|
|
57
64
|
* @param tsconfigPath Path to tsconfig file relative to project root
|
|
58
65
|
* @param sourceFilePaths Array of router source files relative to project root
|
|
@@ -213,45 +220,6 @@ export const analyzeMultipleSourceFiles = async (
|
|
|
213
220
|
return cached.flatMap((f) => f.endpoints)
|
|
214
221
|
}
|
|
215
222
|
|
|
216
|
-
// Build one task per endpoint across all uncached files
|
|
217
|
-
const OPERATIONS = ['get', 'post', 'put', 'delete', 'del', 'patch']
|
|
218
|
-
const operationsPattern = OPERATIONS.join('|')
|
|
219
|
-
|
|
220
|
-
type FileTask = { task: WorkerTask; fileName: string }
|
|
221
|
-
const allTasks: FileTask[] = []
|
|
222
|
-
|
|
223
|
-
for (const { file } of uncached) {
|
|
224
|
-
for (const routerName of file.routers.named) {
|
|
225
|
-
const routerPattern = new RegExp(`${routerName}\\.(?:${operationsPattern})`)
|
|
226
|
-
let endpointIndex = 0
|
|
227
|
-
file.sourceFile.forEachChild((node) => {
|
|
228
|
-
const nodeText = node.getText()
|
|
229
|
-
if (routerPattern.test(nodeText)) {
|
|
230
|
-
if (
|
|
231
|
-
!filterEndpointPaths ||
|
|
232
|
-
filterEndpointPaths.some((p) => resolveEndpointPath(node)?.includes(p))
|
|
233
|
-
) {
|
|
234
|
-
allTasks.push({
|
|
235
|
-
fileName: file.fileName,
|
|
236
|
-
task: {
|
|
237
|
-
taskId: crypto.randomUUID(),
|
|
238
|
-
tsconfigPath: config.tsconfigPath,
|
|
239
|
-
sourceFilePath: file.sourceFile.getFilePath(),
|
|
240
|
-
routerName,
|
|
241
|
-
endpointIndex,
|
|
242
|
-
},
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
|
-
endpointIndex++
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Dispatch all tasks to the worker pool
|
|
252
|
-
const pool = new WorkerPool(resolveWorkerUrl())
|
|
253
|
-
|
|
254
|
-
type EndpointTiming = { method: string; path: string; timing: number; sectionTimings: SectionTiming[] }
|
|
255
223
|
type FileResult = {
|
|
256
224
|
endpoints: EndpointData[]
|
|
257
225
|
fileName: string
|
|
@@ -259,37 +227,63 @@ export const analyzeMultipleSourceFiles = async (
|
|
|
259
227
|
endpointTimings: EndpointTiming[]
|
|
260
228
|
}
|
|
261
229
|
|
|
262
|
-
let results: WorkerResult[]
|
|
263
|
-
try {
|
|
264
|
-
results = await pool.runAll(allTasks.map((ft) => ft.task))
|
|
265
|
-
} finally {
|
|
266
|
-
pool.terminate()
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Group results by file
|
|
270
230
|
const byFile = new Map<string, FileResult>()
|
|
271
231
|
for (const { file } of uncached) {
|
|
272
232
|
byFile.set(file.fileName, { endpoints: [], fileName: file.fileName, timing: 0, endpointTimings: [] })
|
|
273
233
|
}
|
|
274
234
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
235
|
+
// The caller (prepareOpenApiSpec) already built and warmed a ts-morph Project on this thread, so
|
|
236
|
+
// inline analysis runs against a hot type-checker. A worker, by contrast, must spawn a thread and
|
|
237
|
+
// build its own Project from scratch — a multi-second cold-start. That cold-start only pays off
|
|
238
|
+
// when there are enough uncached files that spreading the parse work across workers beats it; below
|
|
239
|
+
// the threshold (e.g. the common single-file incremental rebuild), inline is strictly faster.
|
|
240
|
+
if (uncached.length <= INLINE_ANALYSIS_FILE_THRESHOLD) {
|
|
241
|
+
for (const { file } of uncached) {
|
|
242
|
+
const { endpoints, endpointTimings } = analyzeSourceFileEndpoints(file, filterEndpointPaths)
|
|
243
|
+
const fileResult = byFile.get(file.fileName)!
|
|
244
|
+
fileResult.endpoints = endpoints
|
|
245
|
+
fileResult.endpointTimings = endpointTimings
|
|
246
|
+
fileResult.timing = endpointTimings.reduce((sum, t) => sum + t.timing, 0)
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// One task per file: each worker analyzes a whole file in a single pass, paying its Project
|
|
250
|
+
// cold-start once and reusing the warmed-up checker for every endpoint in that file. Cap the
|
|
251
|
+
// pool at one worker per file so we never spin up a worker with nothing to do.
|
|
252
|
+
type FileTask = { task: WorkerTask; fileName: string }
|
|
253
|
+
const allTasks: FileTask[] = uncached.map(({ file }) => ({
|
|
254
|
+
fileName: file.fileName,
|
|
255
|
+
task: {
|
|
256
|
+
taskId: crypto.randomUUID(),
|
|
257
|
+
tsconfigPath: config.tsconfigPath,
|
|
258
|
+
sourceFilePath: file.sourceFile.getFilePath(),
|
|
259
|
+
routerNames: file.routers.named,
|
|
260
|
+
filterEndpointPaths,
|
|
261
|
+
},
|
|
262
|
+
}))
|
|
279
263
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
264
|
+
const pool = new WorkerPool(resolveWorkerUrl(), allTasks.length)
|
|
265
|
+
let results: WorkerResult[]
|
|
266
|
+
try {
|
|
267
|
+
results = await pool.runAll(allTasks.map((ft) => ft.task))
|
|
268
|
+
} finally {
|
|
269
|
+
pool.terminate()
|
|
283
270
|
}
|
|
284
271
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
272
|
+
// Each result maps 1:1 to a file task.
|
|
273
|
+
for (let i = 0; i < results.length; i++) {
|
|
274
|
+
const result = results[i]
|
|
275
|
+
const fileName = allTasks[i].fileName
|
|
276
|
+
const fileResult = byFile.get(fileName)!
|
|
277
|
+
|
|
278
|
+
if ('error' in result) {
|
|
279
|
+
Logger.error(`[${fileName}] Worker error: ${result.error}`)
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fileResult.endpoints = result.endpoints
|
|
284
|
+
fileResult.endpointTimings = result.endpointTimings
|
|
285
|
+
fileResult.timing = result.endpointTimings.reduce((sum, t) => sum + t.timing, 0)
|
|
286
|
+
}
|
|
293
287
|
}
|
|
294
288
|
|
|
295
289
|
// Write cache for each uncached file
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Project } from 'ts-morph'
|
|
2
2
|
import { parentPort } from 'worker_threads'
|
|
3
3
|
|
|
4
|
+
import { EndpointData } from '../types'
|
|
5
|
+
import { resolveEndpointPath } from './nodeParsers'
|
|
4
6
|
import { parseEndpoint } from './parseEndpoint'
|
|
5
|
-
import { WorkerResult, WorkerTask } from './workerPool'
|
|
7
|
+
import { EndpointTiming, WorkerResult, WorkerTask } from './workerPool'
|
|
6
8
|
|
|
7
9
|
const OPERATIONS = ['get', 'post', 'put', 'delete', 'del', 'patch']
|
|
8
10
|
const OPERATIONS_PATTERN = OPERATIONS.join('|')
|
|
@@ -30,37 +32,39 @@ parentPort!.on('message', (task: WorkerTask) => {
|
|
|
30
32
|
sourceFile = proj.addSourceFileAtPath(task.sourceFilePath)
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
let targetNode: Node<ts.Node> | undefined
|
|
35
|
+
const endpoints: EndpointData[] = []
|
|
36
|
+
const endpointTimings: EndpointTiming[] = []
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
38
|
+
task.routerNames.forEach((routerName) => {
|
|
39
|
+
const routerPattern = new RegExp(`${routerName}\\.(?:${OPERATIONS_PATTERN})`)
|
|
40
|
+
sourceFile!.forEachChild((node) => {
|
|
41
|
+
if (!routerPattern.test(node.getText())) {
|
|
42
|
+
return
|
|
42
43
|
}
|
|
43
|
-
index++
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
}
|
|
45
|
+
if (task.filterEndpointPaths) {
|
|
46
|
+
const endpointPath = resolveEndpointPath(node) ?? ''
|
|
47
|
+
if (!task.filterEndpointPaths.some((p) => endpointPath.includes(p))) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
}
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
const t1 = performance.now()
|
|
53
|
+
const { endpoint, sectionTimings } = parseEndpoint(node, task.sourceFilePath)
|
|
54
|
+
endpointTimings.push({
|
|
55
|
+
method: endpoint.method,
|
|
56
|
+
path: endpoint.path,
|
|
57
|
+
timing: performance.now() - t1,
|
|
58
|
+
sectionTimings,
|
|
59
|
+
})
|
|
60
|
+
endpoints.push(endpoint)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
58
63
|
|
|
59
64
|
const result: WorkerResult = {
|
|
60
65
|
taskId: task.taskId,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
timing: performance.now() - t1,
|
|
66
|
+
endpoints,
|
|
67
|
+
endpointTimings,
|
|
64
68
|
}
|
|
65
69
|
parentPort!.postMessage(result)
|
|
66
70
|
} catch (err) {
|
|
@@ -8,15 +8,21 @@ export type WorkerTask = {
|
|
|
8
8
|
taskId: string
|
|
9
9
|
tsconfigPath: string
|
|
10
10
|
sourceFilePath: string
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
routerNames: string[]
|
|
12
|
+
filterEndpointPaths?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type EndpointTiming = {
|
|
16
|
+
method: string
|
|
17
|
+
path: string
|
|
18
|
+
timing: number
|
|
19
|
+
sectionTimings: SectionTiming[]
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
export type WorkerResultSuccess = {
|
|
16
23
|
taskId: string
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
timing: number
|
|
24
|
+
endpoints: EndpointData[]
|
|
25
|
+
endpointTimings: EndpointTiming[]
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export type WorkerResultError = {
|
|
@@ -32,8 +38,12 @@ export class WorkerPool {
|
|
|
32
38
|
private queue: Array<{ task: WorkerTask; resolve: (r: WorkerResult) => void }> = []
|
|
33
39
|
private pending = new Map<string, (r: WorkerResult) => void>()
|
|
34
40
|
|
|
35
|
-
constructor(workerUrl: URL) {
|
|
36
|
-
const
|
|
41
|
+
constructor(workerUrl: URL, maxWorkers?: number) {
|
|
42
|
+
const cpuBound = Math.max(1, Math.min(os.cpus().length - 1, 8))
|
|
43
|
+
// Never spin up more workers than there are files to analyze: each extra worker would
|
|
44
|
+
// just pay the ts-morph Project cold-start (full TS program load + type-checker warmup)
|
|
45
|
+
// for nothing while contending for CPU with the workers that actually have work.
|
|
46
|
+
const size = maxWorkers === undefined ? cpuBound : Math.max(1, Math.min(cpuBound, maxWorkers))
|
|
37
47
|
this.workers = Array.from({ length: size }, () => {
|
|
38
48
|
const worker = new Worker(workerUrl)
|
|
39
49
|
worker.on('message', (result: WorkerResult) => {
|