moonflower 1.4.7 → 1.4.9

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.
@@ -13,7 +13,7 @@ import { ApiDocsHeader, OpenApiManager } from '../manager/OpenApiManager'
13
13
  import { EndpointData, ExposedModelData } from '../types'
14
14
  import { getSourceFileTimestamp, TimestampCache } from './getSourceFileTimestamp'
15
15
  import { getValuesOfObjectLiteral, resolveEndpointPath } from './nodeParsers'
16
- import { parseEndpoint } from './parseEndpoint'
16
+ import { parseEndpoint, SectionTiming } from './parseEndpoint'
17
17
  import { parseExposedModel, parseNamedExposedModels } from './parseExposedModels'
18
18
  import { SourceFileCache } from './sourceFileCache'
19
19
 
@@ -27,12 +27,15 @@ type Props = {
27
27
  | {
28
28
  cachePath: string
29
29
  }
30
+ profiling?: 'stats' | 'off' | 'debug'
30
31
  }
31
32
 
32
33
  type FileDiscoveryConfig = {
33
34
  rootPath: string
34
35
  }
35
36
 
37
+ type EndpointTiming = { method: string; path: string; timing: number; sectionTimings: SectionTiming[] }
38
+
36
39
  /**
37
40
  * @param tsconfigPath Path to tsconfig file relative to project root
38
41
  * @param sourceFilePaths Array of router source files relative to project root
@@ -43,6 +46,7 @@ export const prepareOpenApiSpec = ({
43
46
  sourceFilePaths,
44
47
  sourceFileDiscovery,
45
48
  incremental,
49
+ profiling = 'stats',
46
50
  }: Props) => {
47
51
  const openApiManager = OpenApiManager.getInstance()
48
52
 
@@ -81,7 +85,9 @@ export const prepareOpenApiSpec = ({
81
85
  targetPath: typeof sourceFileDiscovery === 'object' ? sourceFileDiscovery.rootPath : '.',
82
86
  tsConfigPath: tsconfigPath,
83
87
  })
84
- Logger.info(`File discovery took ${Math.round(performance.now() - startTime)}ms`)
88
+ if (profiling !== 'off') {
89
+ Logger.info(`File discovery took ${Math.round(performance.now() - startTime)}ms`)
90
+ }
85
91
  return files
86
92
  })()
87
93
 
@@ -120,6 +126,7 @@ export const prepareOpenApiSpec = ({
120
126
  incremental: incremental !== false,
121
127
  cachePath,
122
128
  timestampCache: {},
129
+ profiling,
123
130
  })
124
131
 
125
132
  openApiManager.setStats({
@@ -153,23 +160,49 @@ export const analyzeMultipleSourceFiles = (
153
160
  incremental: boolean
154
161
  cachePath: string
155
162
  timestampCache: TimestampCache
163
+ profiling?: 'stats' | 'off' | 'debug'
156
164
  },
157
165
  filterEndpointPaths?: string[],
158
166
  ): EndpointData[] => {
167
+ const profiling = config.profiling ?? 'stats'
159
168
  const startTime = performance.now()
160
169
  const analyzedFiles = files.map((file) => analyzeSourceFileWithCache(file, config, filterEndpointPaths))
161
- Logger.info(`Router analysis took ${Math.round(performance.now() - startTime)}ms`)
162
170
 
163
- analyzedFiles
164
- .map((f, index) => ({
165
- fileName: files[index].fileName,
166
- timeTaken: f.timing,
167
- }))
168
- .sort((a, b) => b.timeTaken - a.timeTaken)
169
- .filter((t) => t.timeTaken > 500)
170
- .forEach((t) => {
171
- Logger.info(`- [${t.fileName}] Took ${Math.round(t.timeTaken)}ms to analyze`)
172
- })
171
+ if (profiling !== 'off') {
172
+ Logger.info(`Router analysis took ${Math.round(performance.now() - startTime)}ms`)
173
+ }
174
+
175
+ if (profiling === 'stats') {
176
+ analyzedFiles
177
+ .map((f, index) => ({ fileName: files[index].fileName, timeTaken: f.timing }))
178
+ .sort((a, b) => b.timeTaken - a.timeTaken)
179
+ .filter((t) => t.timeTaken > 500)
180
+ .forEach((t) => {
181
+ Logger.info(`- [${t.fileName}] Took ${Math.round(t.timeTaken)}ms to analyze`)
182
+ })
183
+ } else if (profiling === 'debug') {
184
+ analyzedFiles
185
+ .map((f, index) => ({
186
+ fileName: files[index].fileName,
187
+ timeTaken: f.timing,
188
+ endpointTimings: f.endpointTimings,
189
+ }))
190
+ .sort((a, b) => b.timeTaken - a.timeTaken)
191
+ .forEach((t) => {
192
+ Logger.info(`- [${t.fileName}] Took ${Math.round(t.timeTaken)}ms to analyze`)
193
+ t.endpointTimings
194
+ .sort((a, b) => b.timing - a.timing)
195
+ .forEach((ep) => {
196
+ Logger.info(` - ${ep.method} ${ep.path} (${Math.round(ep.timing)}ms)`)
197
+ ep.sectionTimings
198
+ .filter((s) => s.timing >= 1)
199
+ .sort((a, b) => b.timing - a.timing)
200
+ .forEach((s) => {
201
+ Logger.info(` - ${s.section}: ${Math.round(s.timing)}ms`)
202
+ })
203
+ })
204
+ })
205
+ }
173
206
 
174
207
  return analyzedFiles.flatMap((f) => f.endpoints)
175
208
  }
@@ -180,38 +213,40 @@ export const analyzeSourceFileWithCache = (
180
213
  incremental: boolean
181
214
  cachePath: string
182
215
  timestampCache: TimestampCache
216
+ profiling?: 'stats' | 'off' | 'debug'
183
217
  },
184
218
  filterEndpointPaths?: string[],
185
- ): { endpoints: EndpointData[]; timing: number } => {
219
+ ): { endpoints: EndpointData[]; timing: number; endpointTimings: EndpointTiming[] } => {
186
220
  const timestamp = getSourceFileTimestamp(file.sourceFile, config.timestampCache)
187
221
  const cachedResults = SourceFileCache.getCachedResults(file.sourceFile, timestamp, config.cachePath)
188
222
 
189
223
  if (cachedResults) {
190
224
  Logger.debug(`[${file.fileName}] Found cached results`)
191
- return { endpoints: cachedResults.endpoints, timing: 0 }
225
+ return { endpoints: cachedResults.endpoints, timing: 0, endpointTimings: [] }
192
226
  }
193
227
  Logger.debug(`[${file.fileName}] Analyzing...`)
194
228
 
195
229
  const t1 = performance.now()
196
- const endpoints = analyzeSourceFileEndpoints(file, filterEndpointPaths)
230
+ const { endpoints, endpointTimings } = analyzeSourceFileEndpoints(file, filterEndpointPaths)
197
231
  const t2 = performance.now()
198
232
  Logger.debug(`[${file.fileName}] Analyzed in ${t2 - t1}ms`)
199
233
  SourceFileCache.cacheResults(file.sourceFile, timestamp, config.cachePath, endpoints)
200
- return { endpoints, timing: t2 - t1 }
234
+ return { endpoints, timing: t2 - t1, endpointTimings }
201
235
  }
202
236
 
203
237
  export const analyzeSourceFileEndpoints = (
204
238
  file: DiscoveredSourceFile,
205
239
  filterEndpointPaths?: string[],
206
- ): EndpointData[] => {
240
+ ): { endpoints: EndpointData[]; endpointTimings: EndpointTiming[] } => {
207
241
  const endpoints: EndpointData[] = []
242
+ const endpointTimings: EndpointTiming[] = []
208
243
  const operations = ['get', 'post', 'put', 'delete', 'del', 'patch']
209
244
  const joinedOperations = operations.join('|')
210
245
 
211
246
  file.routers.named.forEach((routerName) => {
247
+ const routerPattern = new RegExp(`${routerName}\\.(?:${joinedOperations})`)
212
248
  file.sourceFile.forEachChild((node) => {
213
249
  const nodeText = node.getText()
214
- const routerPattern = new RegExp(`${routerName}\\.(?:${joinedOperations})`)
215
250
 
216
251
  if (routerPattern.test(nodeText)) {
217
252
  const endpointPath = resolveEndpointPath(node) ?? ''
@@ -220,12 +255,20 @@ export const analyzeSourceFileEndpoints = (
220
255
  return
221
256
  }
222
257
 
223
- endpoints.push(parseEndpoint(node, file.fileName))
258
+ const t1 = performance.now()
259
+ const { endpoint, sectionTimings } = parseEndpoint(node, file.fileName)
260
+ endpointTimings.push({
261
+ method: endpoint.method,
262
+ path: endpoint.path,
263
+ timing: performance.now() - t1,
264
+ sectionTimings,
265
+ })
266
+ endpoints.push(endpoint)
224
267
  }
225
268
  })
226
269
  })
227
270
 
228
- return endpoints
271
+ return { endpoints, endpointTimings }
229
272
  }
230
273
 
231
274
  export const analyzeSourceFileApiHeader = (sourceFile: SourceFile): ApiDocsHeader | null => {
@@ -829,11 +829,12 @@ const computeProperTypeShape = (type: Type, atLocation: Node, stack: Type[]): Sh
829
829
  .getProperties()
830
830
  .map((prop) => {
831
831
  const valueDeclaration = prop.getValueDeclaration() || prop.getDeclarations()[0]!
832
+ const shape = getProperTypeShape(prop.getTypeAtLocation(atLocation), atLocation, nextStack)
832
833
  if (!valueDeclaration) {
833
834
  return {
834
835
  role: 'property' as const,
835
836
  identifier: prop.getName(),
836
- shape: getProperTypeShape(prop.getTypeAtLocation(atLocation), atLocation, nextStack),
837
+ shape,
837
838
  optional: false,
838
839
  }
839
840
  }
@@ -846,18 +847,17 @@ const computeProperTypeShape = (type: Type, atLocation: Node, stack: Type[]): Sh
846
847
  return {
847
848
  role: 'property' as const,
848
849
  identifier: prop.getName(),
849
- shape: getProperTypeShape(prop.getTypeAtLocation(atLocation), atLocation, nextStack),
850
+ shape,
850
851
  optional: false,
851
852
  }
852
853
  }
853
854
 
854
855
  const isOptional = prop.getTypeAtLocation(atLocation).isNullable()
855
856
 
856
- const shape = getProperTypeShape(prop.getTypeAtLocation(atLocation), atLocation, nextStack)
857
857
  return {
858
858
  role: 'property' as const,
859
859
  identifier: prop.getName(),
860
- shape: shape,
860
+ shape,
861
861
  optional: isOptional,
862
862
  }
863
863
  })
@@ -14,7 +14,12 @@ import {
14
14
  resolveEndpointPath,
15
15
  } from './nodeParsers'
16
16
 
17
- export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
17
+ export type SectionTiming = { section: string; timing: number }
18
+
19
+ export const parseEndpoint = (
20
+ node: Node<ts.Node>,
21
+ sourceFilePath: string,
22
+ ): { endpoint: EndpointData; sectionTimings: SectionTiming[] } => {
18
23
  const parsedEndpointMethod = node
19
24
  .getFirstDescendantByKind(SyntaxKind.PropertyAccessExpression)!
20
25
  .getText()
@@ -46,9 +51,20 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
46
51
  error: Error
47
52
  }[] = []
48
53
 
54
+ const sectionTimings: SectionTiming[] = []
55
+
56
+ const time = <T>(section: string, fn: () => T): T => {
57
+ const t1 = performance.now()
58
+ const result = fn()
59
+ sectionTimings.push({ section, timing: performance.now() - t1 })
60
+ return result
61
+ }
62
+
63
+ const hookNodes = getHookNodes(node)
64
+
49
65
  // API documentation
50
66
  try {
51
- const entries = parseApiDocumentation(node)
67
+ const entries = time('useApiEndpoint', () => parseApiDocumentation(hookNodes.useApiEndpoint))
52
68
  entries.forEach((param) => {
53
69
  endpointData[param.identifier] = param.value as string & string[]
54
70
  })
@@ -62,7 +78,9 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
62
78
 
63
79
  // Request params
64
80
  try {
65
- endpointData.requestPathParams = parseRequestParams(node, endpointPath)
81
+ endpointData.requestPathParams = time('usePathParams', () =>
82
+ parseRequestParams(hookNodes.usePathParams, endpointPath),
83
+ )
66
84
  } catch (err) {
67
85
  warningData.push({
68
86
  segment: 'path',
@@ -73,7 +91,9 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
73
91
 
74
92
  // Request query
75
93
  try {
76
- endpointData.requestQuery = parseRequestObjectInput(node, 'useQueryParams')
94
+ endpointData.requestQuery = time('useQueryParams', () =>
95
+ parseRequestObjectInput(hookNodes.useQueryParams, 'useQueryParams'),
96
+ )
77
97
  } catch (err) {
78
98
  warningData.push({
79
99
  segment: 'query',
@@ -84,7 +104,9 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
84
104
 
85
105
  // Request headers
86
106
  try {
87
- endpointData.requestHeaders = parseRequestObjectInput(node, 'useHeaderParams')
107
+ endpointData.requestHeaders = time('useHeaderParams', () =>
108
+ parseRequestObjectInput(hookNodes.useHeaderParams, 'useHeaderParams'),
109
+ )
88
110
  } catch (err) {
89
111
  warningData.push({
90
112
  segment: 'headers',
@@ -95,7 +117,7 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
95
117
 
96
118
  // Raw request body
97
119
  try {
98
- const parsedBody = parseRequestRawBody(node)
120
+ const parsedBody = time('useRequestRawBody', () => parseRequestRawBody(hookNodes.useRequestRawBody))
99
121
  if (parsedBody) {
100
122
  endpointData.rawBody = parsedBody
101
123
  }
@@ -109,7 +131,9 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
109
131
 
110
132
  // Object request body
111
133
  try {
112
- endpointData.objectBody = parseRequestObjectInput(node, 'useRequestBody')
134
+ endpointData.objectBody = time('useRequestBody', () =>
135
+ parseRequestObjectInput(hookNodes.useRequestBody, 'useRequestBody'),
136
+ )
113
137
  } catch (err) {
114
138
  warningData.push({
115
139
  segment: 'objectBody',
@@ -120,7 +144,7 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
120
144
 
121
145
  // Request response
122
146
  try {
123
- endpointData.responses = parseRequestResponse(node)
147
+ endpointData.responses = time('response', () => parseRequestResponse(node))
124
148
  } catch (err) {
125
149
  warningData.push({
126
150
  segment: 'response',
@@ -129,28 +153,36 @@ export const parseEndpoint = (node: Node<ts.Node>, sourceFilePath: string) => {
129
153
  Logger.error('Error', err)
130
154
  }
131
155
 
132
- return endpointData
156
+ return { endpoint: endpointData, sectionTimings }
133
157
  }
134
158
 
135
- const getHookNode = (
136
- endpointNode: Node<ts.Node>,
137
- hookName:
138
- | 'useApiEndpoint'
139
- | 'usePathParams'
140
- | 'useQueryParams'
141
- | 'useHeaderParams'
142
- | 'useRequestBody'
143
- | 'useRequestRawBody',
144
- ) => {
145
- const callExpressions = endpointNode.getDescendantsOfKind(SyntaxKind.CallExpression)
146
- const matchingCallExpressions = callExpressions.filter((node) => {
147
- return node.getFirstChildByKind(SyntaxKind.Identifier)?.getText() === hookName
148
- })
149
- return matchingCallExpressions[0] ?? null
159
+ type HookName =
160
+ | 'useApiEndpoint'
161
+ | 'usePathParams'
162
+ | 'useQueryParams'
163
+ | 'useHeaderParams'
164
+ | 'useRequestBody'
165
+ | 'useRequestRawBody'
166
+
167
+ const getHookNodes = (endpointNode: Node<ts.Node>): Record<HookName, Node<ts.CallExpression> | null> => {
168
+ const result: Record<HookName, Node<ts.CallExpression> | null> = {
169
+ useApiEndpoint: null,
170
+ usePathParams: null,
171
+ useQueryParams: null,
172
+ useHeaderParams: null,
173
+ useRequestBody: null,
174
+ useRequestRawBody: null,
175
+ }
176
+ for (const node of endpointNode.getDescendantsOfKind(SyntaxKind.CallExpression)) {
177
+ const name = node.getFirstChildByKind(SyntaxKind.Identifier)?.getText() as HookName | undefined
178
+ if (name && name in result && result[name] === null) {
179
+ result[name] = node
180
+ }
181
+ }
182
+ return result
150
183
  }
151
184
 
152
- const parseApiDocumentation = (node: Node<ts.Node>) => {
153
- const hookNode = getHookNode(node, 'useApiEndpoint')
185
+ const parseApiDocumentation = (hookNode: Node<ts.CallExpression> | null) => {
154
186
  if (!hookNode) {
155
187
  return []
156
188
  }
@@ -170,8 +202,10 @@ const parseApiDocumentation = (node: Node<ts.Node>) => {
170
202
  }[]
171
203
  }
172
204
 
173
- const parseRequestParams = (node: Node<ts.Node>, endpointPath: string): EndpointData['requestPathParams'] => {
174
- const hookNode = getHookNode(node, 'usePathParams')
205
+ const parseRequestParams = (
206
+ hookNode: Node<ts.CallExpression> | null,
207
+ endpointPath: string,
208
+ ): EndpointData['requestPathParams'] => {
175
209
  if (!hookNode) {
176
210
  return []
177
211
  }
@@ -203,8 +237,9 @@ const parseRequestParams = (node: Node<ts.Node>, endpointPath: string): Endpoint
203
237
  }))
204
238
  }
205
239
 
206
- const parseRequestRawBody = (node: Node<ts.Node>): NonNullable<EndpointData['rawBody']> | null => {
207
- const hookNode = getHookNode(node, 'useRequestRawBody')
240
+ const parseRequestRawBody = (
241
+ hookNode: Node<ts.CallExpression> | null,
242
+ ): NonNullable<EndpointData['rawBody']> | null => {
208
243
  if (!hookNode) {
209
244
  return null
210
245
  }
@@ -222,10 +257,9 @@ const parseRequestRawBody = (node: Node<ts.Node>): NonNullable<EndpointData['raw
222
257
  }
223
258
 
224
259
  const parseRequestObjectInput = (
225
- node: Node<ts.Node>,
260
+ hookNode: Node<ts.CallExpression> | null,
226
261
  nodeName: 'useQueryParams' | 'useHeaderParams' | 'useRequestBody',
227
262
  ): EndpointData['requestQuery'] | EndpointData['objectBody'] => {
228
- const hookNode = getHookNode(node, nodeName)
229
263
  if (!hookNode) {
230
264
  return []
231
265
  }
@@ -23,7 +23,7 @@ describe('OpenApi Analyzer', () => {
23
23
  },
24
24
  [`/test/${id}`],
25
25
  )
26
- const endpoint = analysisResult.find((endpoint) => endpoint.path.startsWith(`/test/${id}`))
26
+ const endpoint = analysisResult.endpoints.find((endpoint) => endpoint.path.startsWith(`/test/${id}`))
27
27
  if (!endpoint) {
28
28
  throw new Error(`No endpoint with id ${id} found!`)
29
29
  }
@@ -39,7 +39,7 @@ describe('OpenApi Analyzer', () => {
39
39
  },
40
40
  [`/test/${id}`],
41
41
  )
42
- const endpoints = analysisResult.filter((endpoint) => endpoint.path.startsWith(`/test/${id}`))
42
+ const endpoints = analysisResult.endpoints.filter((endpoint) => endpoint.path.startsWith(`/test/${id}`))
43
43
  if (endpoints.length === 0) {
44
44
  throw new Error(`No endpoint with id ${id} found!`)
45
45
  }
@@ -19,7 +19,7 @@ describe('OpenApi Analyzer (Zod Validator)', () => {
19
19
  },
20
20
  [`/test/${id}`],
21
21
  )
22
- const endpoint = analysisResult.find((endpoint) => endpoint.path.startsWith(`/test/${id}`))
22
+ const endpoint = analysisResult.endpoints.find((endpoint) => endpoint.path.startsWith(`/test/${id}`))
23
23
  if (!endpoint) {
24
24
  throw new Error(`No endpoint with id ${id} found!`)
25
25
  }