tspl-cli 0.0.0-stage → 0.1.1
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/README.md +127 -2
- package/bin/tspl.js +10 -0
- package/package.json +41 -4
- package/src/active-org.js +49 -0
- package/src/analysis/analyzer.js +61 -0
- package/src/analysis/extract.js +37 -0
- package/src/analysis/file-filter.js +69 -0
- package/src/analysis/git-reader.js +108 -0
- package/src/analysis/html-parser.js +46 -0
- package/src/analysis/js-parser.js +473 -0
- package/src/analysis/py-parser.js +66 -0
- package/src/analysis/repo-index.js +107 -0
- package/src/api-client.js +108 -0
- package/src/browser-tests.js +209 -0
- package/src/commands/analyze.command.js +81 -0
- package/src/commands/auth.command.js +250 -0
- package/src/commands/build.command.js +267 -0
- package/src/commands/connect.command.js +270 -0
- package/src/commands/engines.command.js +136 -0
- package/src/commands/find.command.js +107 -0
- package/src/commands/init.command.js +79 -0
- package/src/commands/link.command.js +65 -0
- package/src/commands/mcp.command.js +12 -0
- package/src/commands/member.command.js +82 -0
- package/src/commands/plan.command.js +161 -0
- package/src/commands/project.command.js +191 -0
- package/src/commands/setup.command.js +63 -0
- package/src/commands/sync.command.js +59 -0
- package/src/commands/task.command.js +475 -0
- package/src/commands/usage.command.js +105 -0
- package/src/config-store.js +255 -0
- package/src/connect/claude-skill.md +30 -0
- package/src/engines.js +218 -0
- package/src/find/graph.js +272 -0
- package/src/find/search.js +473 -0
- package/src/find/text.js +86 -0
- package/src/git-helper.js +102 -0
- package/src/index.js +91 -0
- package/src/mcp/server.js +708 -0
- package/src/project-config.js +197 -0
- package/src/project-init.js +447 -0
- package/src/providers/github.js +249 -0
- package/src/providers/index.js +83 -0
- package/src/runtime-check.js +18 -0
- package/src/setup.js +180 -0
- package/src/shared/approaches.js +44 -0
- package/src/shared/constants.js +89 -0
- package/src/shared/index.js +8 -0
- package/src/shared/workflow-progress.js +67 -0
- package/src/shared/workflow.js +381 -0
- package/src/stack-detector.js +47 -0
- package/src/task/approaches.js +53 -0
- package/src/task/branch-choice.js +69 -0
- package/src/task/brief.js +115 -0
- package/src/task/build.js +549 -0
- package/src/task/check.js +415 -0
- package/src/task/conventions.js +113 -0
- package/src/task/gate.js +88 -0
- package/src/task/hooks.js +82 -0
- package/src/task/plan-draft.js +43 -0
- package/src/task/plan-page.js +123 -0
- package/src/task/plan.js +293 -0
- package/src/task/planning-context.js +165 -0
- package/src/task/refine.js +73 -0
- package/src/workflow/agents.js +336 -0
- package/src/workflow/checkout-lock.js +92 -0
- package/src/workflow/client.js +82 -0
- package/src/workflow/controller.js +585 -0
- package/src/workflow/evidence.js +276 -0
- package/src/workflow/pr-body.js +52 -0
- package/src/workflow/process.js +82 -0
- package/src/workflow/review.js +128 -0
- package/src/workflow/runtime.js +148 -0
- package/src/workflow/wait.js +58 -0
- package/src/workspace.js +250 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { parse } = require('@babel/parser')
|
|
4
|
+
const { isSecretValue } = require('./file-filter')
|
|
5
|
+
|
|
6
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'request']
|
|
7
|
+
const HTTP_VERB = /^(get|post|put|patch|delete|head|options)$/i
|
|
8
|
+
const HTTP_CLIENTS = /^\$?(http|httpclient|axios|api|apiservice|apiclient|client|request|instance)$/i
|
|
9
|
+
const URL_LIKE = /^(\/|\{\}\/|https?:\/\/)/
|
|
10
|
+
const EXPRESS_ROUTERS = /^(router|app|route|api)$/i
|
|
11
|
+
const UI_ROUTE_KEYS = ['component', 'loadChildren', 'loadComponent', 'redirectTo', 'children']
|
|
12
|
+
const SKIP_KEYS = new Set(['loc', 'start', 'end', 'extra', 'leadingComments', 'trailingComments', 'innerComments', 'comments', 'tokens'])
|
|
13
|
+
const LABEL_ATTRIBUTES = /^(title|placeholder|label|text|header|headerTitle|tabBarLabel|accessibilityLabel|alt)$/
|
|
14
|
+
const LIMIT = 500
|
|
15
|
+
const LABEL_LIMIT = 150
|
|
16
|
+
const VALUE_LIMIT = 200
|
|
17
|
+
const TEXT_LIMIT = 100
|
|
18
|
+
const COMMENT_LIMIT = 50
|
|
19
|
+
const CONSTANT_NAME = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$|^[A-Z]{3,}$/
|
|
20
|
+
const CODE_LIKE = /[;{}=]|=>|\(\)|^\s*(const|let|var|if|return|import|await)\b/
|
|
21
|
+
|
|
22
|
+
const pluginsFor = ext => {
|
|
23
|
+
if (ext === '.ts') return ['typescript', 'decorators-legacy']
|
|
24
|
+
if (ext === '.tsx') return ['typescript', 'jsx', 'decorators-legacy']
|
|
25
|
+
return ['jsx', 'decorators-legacy']
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const walk = (root, visit) => {
|
|
29
|
+
const stack = [root]
|
|
30
|
+
while (stack.length) {
|
|
31
|
+
const node = stack.pop()
|
|
32
|
+
visit(node)
|
|
33
|
+
for (const key of Object.keys(node)) {
|
|
34
|
+
if (SKIP_KEYS.has(key)) continue
|
|
35
|
+
const value = node[key]
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
38
|
+
if (value[i] && typeof value[i].type === 'string') stack.push(value[i])
|
|
39
|
+
}
|
|
40
|
+
} else if (value && typeof value.type === 'string') {
|
|
41
|
+
stack.push(value)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lineOf = node => (node.loc ? node.loc.start.line : null)
|
|
48
|
+
|
|
49
|
+
const keyName = prop => {
|
|
50
|
+
if (!prop || !prop.key) return null
|
|
51
|
+
if (prop.key.type === 'Identifier') return prop.key.name
|
|
52
|
+
if (prop.key.type === 'StringLiteral') return prop.key.value
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const property = (object, name) =>
|
|
57
|
+
object.properties.find(
|
|
58
|
+
prop => (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') && keyName(prop) === name,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const text = node => {
|
|
62
|
+
if (!node) return null
|
|
63
|
+
if (node.type === 'StringLiteral') return node.value
|
|
64
|
+
if (node.type === 'TemplateLiteral') {
|
|
65
|
+
return node.quasis.map(q => q.value.cooked).join('{}')
|
|
66
|
+
}
|
|
67
|
+
if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
68
|
+
const left = text(node.left)
|
|
69
|
+
const right = text(node.right)
|
|
70
|
+
if (left === null && right === null) return null
|
|
71
|
+
return `${left === null ? '{}' : left}${right === null ? '{}' : right}`
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lastName = node => {
|
|
77
|
+
if (!node) return null
|
|
78
|
+
if (node.type === 'Identifier') return node.name
|
|
79
|
+
if (node.type === 'ThisExpression') return 'this'
|
|
80
|
+
if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
|
|
81
|
+
return node.property.type === 'Identifier' ? node.property.name : null
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const memberNames = node => {
|
|
87
|
+
if (!node) return []
|
|
88
|
+
if (node.type === 'Identifier') return [node.name]
|
|
89
|
+
if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
|
|
90
|
+
return [...memberNames(node.object), node.property.name]
|
|
91
|
+
}
|
|
92
|
+
return []
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const handlerName = route => {
|
|
96
|
+
const holder = ['config', 'options']
|
|
97
|
+
.map(key => property(route, key))
|
|
98
|
+
.find(prop => prop && prop.value && prop.value.type === 'ObjectExpression')
|
|
99
|
+
const handler = property(route, 'handler') || (holder && property(holder.value, 'handler'))
|
|
100
|
+
if (!handler || !handler.value) return null
|
|
101
|
+
const names = memberNames(handler.value).filter(name => name !== 'handler')
|
|
102
|
+
return names.length > 1 ? names[names.length - 1] : null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const labelText = value => {
|
|
106
|
+
const label = value.replace(/\s+/g, ' ').trim()
|
|
107
|
+
return label.length >= 2 && label.length <= 80 && /[a-z]/i.test(label) ? label : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const literalValue = node => {
|
|
111
|
+
if (!node) return null
|
|
112
|
+
if (node.type === 'StringLiteral') return node.value
|
|
113
|
+
if (node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') return String(node.value)
|
|
114
|
+
if (node.type === 'TemplateLiteral' && !node.expressions.length) return node.quasis.map(q => q.value.cooked).join('')
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const valueOf = (name, node) => {
|
|
119
|
+
const value = literalValue(node)
|
|
120
|
+
if (value === null || !value.trim() || value.length > 80 || isSecretValue(name, value)) return null
|
|
121
|
+
return value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const commentLines = comments =>
|
|
125
|
+
comments
|
|
126
|
+
.flatMap(comment => comment.value.split('\n'))
|
|
127
|
+
.map(line => line.replace(/^[\s*/#-]+/, '').replace(/\s+/g, ' ').trim())
|
|
128
|
+
.filter(line => line.length <= 120 && line.split(' ').length >= 3 && /[a-z]{3}/i.test(line) && !CODE_LIKE.test(line))
|
|
129
|
+
|
|
130
|
+
const isModuleExports = node =>
|
|
131
|
+
node.type === 'MemberExpression' &&
|
|
132
|
+
node.object.type === 'Identifier' &&
|
|
133
|
+
node.object.name === 'module' &&
|
|
134
|
+
node.property.type === 'Identifier' &&
|
|
135
|
+
node.property.name === 'exports'
|
|
136
|
+
|
|
137
|
+
const classInfo = (node, name) => ({
|
|
138
|
+
kind: 'class',
|
|
139
|
+
name: name || (node.id && node.id.name) || 'default',
|
|
140
|
+
line: lineOf(node),
|
|
141
|
+
methods: node.body.body
|
|
142
|
+
.filter(m => (m.type === 'ClassMethod' || m.type === 'ClassProperty') && keyName(m) && keyName(m) !== 'constructor')
|
|
143
|
+
.filter(m => m.type === 'ClassMethod' || (m.value && /Function/.test(m.value.type)))
|
|
144
|
+
.map(keyName)
|
|
145
|
+
.slice(0, 100),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const decoratorsOf = node =>
|
|
149
|
+
(node.decorators || [])
|
|
150
|
+
.map(d => d.expression)
|
|
151
|
+
.filter(e => e && e.type === 'CallExpression' && e.callee.type === 'Identifier')
|
|
152
|
+
|
|
153
|
+
const topLevelSymbols = (program, strings) => {
|
|
154
|
+
const symbols = []
|
|
155
|
+
const addDeclaration = decl => {
|
|
156
|
+
if (!decl) return
|
|
157
|
+
if (decl.type === 'FunctionDeclaration' && decl.id) {
|
|
158
|
+
symbols.push({ kind: 'function', name: decl.id.name, line: lineOf(decl) })
|
|
159
|
+
} else if (decl.type === 'ClassDeclaration') {
|
|
160
|
+
symbols.push(classInfo(decl))
|
|
161
|
+
} else if (decl.type === 'VariableDeclaration') {
|
|
162
|
+
decl.declarations.forEach(d => {
|
|
163
|
+
if (d.id.type !== 'Identifier' || !d.init) return
|
|
164
|
+
if (d.init.type === 'StringLiteral') strings.set(d.id.name, d.init.value)
|
|
165
|
+
if (/Function/.test(d.init.type)) {
|
|
166
|
+
symbols.push({ kind: 'function', name: d.id.name, line: lineOf(d) })
|
|
167
|
+
} else if (d.init.type === 'ClassExpression') {
|
|
168
|
+
symbols.push(classInfo(d.init, d.id.name))
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
program.body.forEach(statement => {
|
|
175
|
+
if (statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration') {
|
|
176
|
+
addDeclaration(statement.declaration)
|
|
177
|
+
} else if (
|
|
178
|
+
statement.type === 'ExpressionStatement' &&
|
|
179
|
+
statement.expression.type === 'AssignmentExpression' &&
|
|
180
|
+
isModuleExports(statement.expression.left) &&
|
|
181
|
+
statement.expression.right.type === 'ClassExpression'
|
|
182
|
+
) {
|
|
183
|
+
symbols.push(classInfo(statement.expression.right))
|
|
184
|
+
} else {
|
|
185
|
+
addDeclaration(statement)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
return symbols
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const extractJs = (code, ext) => {
|
|
192
|
+
let ast
|
|
193
|
+
try {
|
|
194
|
+
ast = parse(code, {
|
|
195
|
+
sourceType: 'unambiguous',
|
|
196
|
+
errorRecovery: true,
|
|
197
|
+
allowReturnOutsideFunction: true,
|
|
198
|
+
plugins: pluginsFor(ext),
|
|
199
|
+
})
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return { parseError: true }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const strings = new Map()
|
|
205
|
+
const symbols = topLevelSymbols(ast.program, strings)
|
|
206
|
+
const imports = new Set()
|
|
207
|
+
const exports = new Set()
|
|
208
|
+
const routes = []
|
|
209
|
+
const uiRoutes = []
|
|
210
|
+
const schemas = []
|
|
211
|
+
const modelNames = []
|
|
212
|
+
const modelSchemas = new Map()
|
|
213
|
+
const schemaVariables = new Map()
|
|
214
|
+
const httpCalls = []
|
|
215
|
+
const components = []
|
|
216
|
+
const screens = []
|
|
217
|
+
const refs = new Set()
|
|
218
|
+
const labels = new Set()
|
|
219
|
+
const values = []
|
|
220
|
+
const texts = new Set()
|
|
221
|
+
const topLevel = new Set()
|
|
222
|
+
ast.program.body.forEach(statement => {
|
|
223
|
+
const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement
|
|
224
|
+
if (declaration && declaration.type === 'VariableDeclaration') declaration.declarations.forEach(d => topLevel.add(d))
|
|
225
|
+
})
|
|
226
|
+
const addValue = (name, node) => {
|
|
227
|
+
const value = valueOf(name, node)
|
|
228
|
+
if (value !== null && values.length < VALUE_LIMIT) values.push({ name, value, line: lineOf(node) })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
walk(ast.program, node => {
|
|
232
|
+
switch (node.type) {
|
|
233
|
+
case 'VariableDeclarator':
|
|
234
|
+
if (node.id.type === 'Identifier' && node.init && /(New|Call)Expression/.test(node.init.type) && lastName(node.init.callee) === 'Schema') {
|
|
235
|
+
schemaVariables.set(node.init, node.id.name)
|
|
236
|
+
}
|
|
237
|
+
if (node.id.type === 'Identifier' && (topLevel.has(node) || CONSTANT_NAME.test(node.id.name))) {
|
|
238
|
+
addValue(node.id.name, node.init)
|
|
239
|
+
}
|
|
240
|
+
break
|
|
241
|
+
case 'ObjectProperty': {
|
|
242
|
+
const name = keyName(node)
|
|
243
|
+
if (name && CONSTANT_NAME.test(name)) addValue(name, node.value)
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
case 'StringLiteral':
|
|
247
|
+
case 'TemplateLiteral': {
|
|
248
|
+
const value = node.type === 'StringLiteral' ? node.value : text(node)
|
|
249
|
+
if (value && value.length >= 8 && value.length <= 120 && / [a-z]/i.test(value) && !URL_LIKE.test(value) && !isSecretValue(null, value) && texts.size < TEXT_LIMIT) {
|
|
250
|
+
texts.add(value.replace(/\s+/g, ' ').trim())
|
|
251
|
+
}
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
case 'Identifier':
|
|
255
|
+
if (node.name.length >= 4 && /[A-Z]/.test(node.name)) refs.add(node.name)
|
|
256
|
+
break
|
|
257
|
+
case 'JSXText': {
|
|
258
|
+
const label = labelText(node.value)
|
|
259
|
+
if (label) labels.add(label)
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
case 'JSXAttribute': {
|
|
263
|
+
const label = node.name.type === 'JSXIdentifier' && LABEL_ATTRIBUTES.test(node.name.name) &&
|
|
264
|
+
node.value && node.value.type === 'StringLiteral' && labelText(node.value.value)
|
|
265
|
+
if (label) labels.add(label)
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
case 'ImportDeclaration':
|
|
269
|
+
imports.add(node.source.value)
|
|
270
|
+
break
|
|
271
|
+
case 'ExportAllDeclaration':
|
|
272
|
+
imports.add(node.source.value)
|
|
273
|
+
exports.add('*')
|
|
274
|
+
break
|
|
275
|
+
case 'ExportNamedDeclaration':
|
|
276
|
+
if (node.source) imports.add(node.source.value)
|
|
277
|
+
if (node.declaration) {
|
|
278
|
+
const d = node.declaration
|
|
279
|
+
if (d.id) exports.add(d.id.name)
|
|
280
|
+
if (d.declarations) d.declarations.forEach(v => v.id.name && exports.add(v.id.name))
|
|
281
|
+
}
|
|
282
|
+
node.specifiers.forEach(s => exports.add(s.exported.name || s.exported.value))
|
|
283
|
+
break
|
|
284
|
+
case 'ExportDefaultDeclaration':
|
|
285
|
+
exports.add('default')
|
|
286
|
+
break
|
|
287
|
+
case 'AssignmentExpression': {
|
|
288
|
+
const left = node.left
|
|
289
|
+
if (isModuleExports(left)) {
|
|
290
|
+
if (node.right.type === 'ObjectExpression') {
|
|
291
|
+
node.right.properties.forEach(p => keyName(p) && exports.add(keyName(p)))
|
|
292
|
+
} else {
|
|
293
|
+
exports.add('default')
|
|
294
|
+
}
|
|
295
|
+
} else if (
|
|
296
|
+
left.type === 'MemberExpression' &&
|
|
297
|
+
left.property.type === 'Identifier' &&
|
|
298
|
+
((left.object.type === 'Identifier' && left.object.name === 'exports') || isModuleExports(left.object))
|
|
299
|
+
) {
|
|
300
|
+
exports.add(left.property.name)
|
|
301
|
+
}
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
case 'CallExpression':
|
|
305
|
+
case 'OptionalCallExpression':
|
|
306
|
+
case 'NewExpression': {
|
|
307
|
+
const callee = node.callee
|
|
308
|
+
const first = node.arguments[0]
|
|
309
|
+
if (callee.type === 'Identifier' && callee.name === 'require' && first && first.type === 'StringLiteral') {
|
|
310
|
+
imports.add(first.value)
|
|
311
|
+
}
|
|
312
|
+
if (callee.type === 'Import' && first && first.type === 'StringLiteral') {
|
|
313
|
+
imports.add(first.value)
|
|
314
|
+
}
|
|
315
|
+
if (lastName(callee) === 'Schema' && first && first.type === 'ObjectExpression') {
|
|
316
|
+
const options = node.arguments[1]
|
|
317
|
+
const collection = options && options.type === 'ObjectExpression' && property(options, 'collection')
|
|
318
|
+
schemas.push({
|
|
319
|
+
variable: schemaVariables.get(node) || null,
|
|
320
|
+
fields: first.properties.map(keyName).filter(Boolean).slice(0, 200),
|
|
321
|
+
collection: collection ? text(collection.value) || strings.get(collection.value.name) || null : null,
|
|
322
|
+
line: lineOf(node),
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
326
|
+
const method = callee.property.name
|
|
327
|
+
const owner = lastName(callee.object)
|
|
328
|
+
if (method === 'model' && first) {
|
|
329
|
+
const name = first.type === 'StringLiteral' ? first.value : strings.get(first.name)
|
|
330
|
+
if (name) modelNames.push(name)
|
|
331
|
+
const schema = node.arguments[1]
|
|
332
|
+
if (name && schema && schema.type === 'Identifier') modelSchemas.set(schema.name, name)
|
|
333
|
+
}
|
|
334
|
+
if (HTTP_METHODS.includes(method) && owner && HTTP_CLIENTS.test(owner)) {
|
|
335
|
+
const url = text(first)
|
|
336
|
+
if (url !== null) httpCalls.push({ method: method.toUpperCase(), url, line: lineOf(node) })
|
|
337
|
+
}
|
|
338
|
+
if (HTTP_METHODS.includes(method) && owner && EXPRESS_ROUTERS.test(owner) && first && first.type === 'StringLiteral' && first.value.startsWith('/')) {
|
|
339
|
+
routes.push({ method: method.toUpperCase(), path: first.value, line: lineOf(node) })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const second = node.arguments[1]
|
|
343
|
+
if (first && first.type === 'StringLiteral' && HTTP_VERB.test(first.value) && second) {
|
|
344
|
+
const url = text(second)
|
|
345
|
+
if (url !== null && URL_LIKE.test(url)) {
|
|
346
|
+
httpCalls.push({ method: first.value.toUpperCase(), url, line: lineOf(node) })
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (callee.type === 'Identifier' && callee.name === 'fetch' && first) {
|
|
350
|
+
const url = text(first)
|
|
351
|
+
const options = node.arguments[1]
|
|
352
|
+
const method = options && options.type === 'ObjectExpression' && property(options, 'method')
|
|
353
|
+
if (url !== null) {
|
|
354
|
+
httpCalls.push({ method: method ? (text(method.value) || 'GET').toUpperCase() : 'GET', url, line: lineOf(node) })
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
break
|
|
358
|
+
}
|
|
359
|
+
case 'ObjectExpression': {
|
|
360
|
+
const methodProp = property(node, 'method')
|
|
361
|
+
const pathProp = property(node, 'path')
|
|
362
|
+
const urlProp = property(node, 'url')
|
|
363
|
+
if (urlProp && urlProp.value && !pathProp) {
|
|
364
|
+
const url = text(urlProp.value)
|
|
365
|
+
const verb = methodProp && methodProp.value ? text(methodProp.value) : 'GET'
|
|
366
|
+
if (url !== null && URL_LIKE.test(url) && verb && HTTP_VERB.test(verb)) {
|
|
367
|
+
httpCalls.push({ method: verb.toUpperCase(), url, line: lineOf(node) })
|
|
368
|
+
}
|
|
369
|
+
} else if (methodProp && pathProp && pathProp.value) {
|
|
370
|
+
const path = text(pathProp.value)
|
|
371
|
+
const value = methodProp.value
|
|
372
|
+
const methods = value.type === 'ArrayExpression'
|
|
373
|
+
? value.elements.map(text).filter(Boolean)
|
|
374
|
+
: [text(value)].filter(Boolean)
|
|
375
|
+
if (path !== null && methods.length) {
|
|
376
|
+
routes.push({ method: methods.join(',').toUpperCase(), path, handler: handlerName(node), line: lineOf(node) })
|
|
377
|
+
}
|
|
378
|
+
} else if (pathProp && pathProp.value && UI_ROUTE_KEYS.some(key => property(node, key))) {
|
|
379
|
+
const path = text(pathProp.value)
|
|
380
|
+
const component = property(node, 'component')
|
|
381
|
+
const lazy = property(node, 'loadChildren') || property(node, 'loadComponent')
|
|
382
|
+
let target = null
|
|
383
|
+
if (lazy) {
|
|
384
|
+
walk(lazy.value, inner => {
|
|
385
|
+
if (!target && inner.type === 'CallExpression' && inner.callee.type === 'Import' && inner.arguments[0]) {
|
|
386
|
+
target = text(inner.arguments[0])
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
if (path !== null) {
|
|
391
|
+
uiRoutes.push({
|
|
392
|
+
path,
|
|
393
|
+
component: component && component.value.type === 'Identifier' ? component.value.name : null,
|
|
394
|
+
lazy: target,
|
|
395
|
+
line: lineOf(node),
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
case 'ClassDeclaration': {
|
|
402
|
+
decoratorsOf(node).forEach(decorator => {
|
|
403
|
+
const kind = decorator.callee.name
|
|
404
|
+
if (!['Component', 'Injectable', 'NgModule', 'Directive', 'Pipe'].includes(kind)) return
|
|
405
|
+
const config = decorator.arguments[0]
|
|
406
|
+
const selector = config && config.type === 'ObjectExpression' && property(config, 'selector')
|
|
407
|
+
const template = config && config.type === 'ObjectExpression' && property(config, 'templateUrl')
|
|
408
|
+
components.push({
|
|
409
|
+
kind: kind === 'Injectable' ? 'service' : kind.toLowerCase(),
|
|
410
|
+
name: node.id ? node.id.name : 'default',
|
|
411
|
+
selector: selector ? text(selector.value) : null,
|
|
412
|
+
template: template ? text(template.value) : null,
|
|
413
|
+
line: lineOf(node),
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
break
|
|
417
|
+
}
|
|
418
|
+
case 'JSXOpeningElement': {
|
|
419
|
+
const name = node.name
|
|
420
|
+
if (name.type === 'JSXMemberExpression' && name.property.name === 'Screen') {
|
|
421
|
+
const attr = attrName => node.attributes.find(a => a.type === 'JSXAttribute' && a.name.name === attrName)
|
|
422
|
+
const screenName = attr('name')
|
|
423
|
+
const component = attr('component')
|
|
424
|
+
if (screenName && screenName.value && screenName.value.type === 'StringLiteral') {
|
|
425
|
+
screens.push({
|
|
426
|
+
name: screenName.value.value,
|
|
427
|
+
component: component && component.value && component.value.expression && component.value.expression.type === 'Identifier'
|
|
428
|
+
? component.value.expression.name
|
|
429
|
+
: null,
|
|
430
|
+
line: lineOf(node),
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
break
|
|
435
|
+
}
|
|
436
|
+
default:
|
|
437
|
+
break
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const byPosition = !modelSchemas.size && schemas.length === modelNames.length
|
|
442
|
+
const models = schemas.map(({ variable, ...schema }, i) => ({
|
|
443
|
+
name: modelSchemas.get(variable) || (schemas.length === 1 || byPosition ? modelNames[i] || null : null),
|
|
444
|
+
...(variable ? { variable } : {}),
|
|
445
|
+
...schema,
|
|
446
|
+
}))
|
|
447
|
+
modelNames
|
|
448
|
+
.filter(name => !models.some(m => m.name === name))
|
|
449
|
+
.forEach(name => models.push({ name, fields: [], collection: null, line: null }))
|
|
450
|
+
|
|
451
|
+
const defined = new Set(symbols.map(symbol => symbol.name))
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
imports: [...imports].slice(0, LIMIT),
|
|
455
|
+
exports: [...exports].slice(0, LIMIT),
|
|
456
|
+
symbols: symbols.slice(0, LIMIT),
|
|
457
|
+
refs: [...refs].filter(name => !defined.has(name)).slice(0, LIMIT),
|
|
458
|
+
labels: [...labels].slice(0, LABEL_LIMIT),
|
|
459
|
+
values,
|
|
460
|
+
texts: [...texts].filter(value => !labels.has(value)),
|
|
461
|
+
comments: [...new Set(commentLines(ast.comments || []))].slice(0, COMMENT_LIMIT),
|
|
462
|
+
routes: routes.slice(0, LIMIT),
|
|
463
|
+
uiRoutes: uiRoutes.slice(0, LIMIT),
|
|
464
|
+
models: models.slice(0, LIMIT),
|
|
465
|
+
httpCalls: httpCalls.slice(0, LIMIT),
|
|
466
|
+
components: components.slice(0, LIMIT),
|
|
467
|
+
screens: screens.slice(0, LIMIT),
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
module.exports = {
|
|
472
|
+
extractJs,
|
|
473
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { isSecretValue } = require('./file-filter')
|
|
4
|
+
|
|
5
|
+
const LIMIT = 500
|
|
6
|
+
|
|
7
|
+
const extractPy = code => {
|
|
8
|
+
const imports = new Set()
|
|
9
|
+
const symbols = []
|
|
10
|
+
const routes = []
|
|
11
|
+
const values = []
|
|
12
|
+
const comments = new Set()
|
|
13
|
+
let currentClass = null
|
|
14
|
+
|
|
15
|
+
code.split('\n').forEach((raw, index) => {
|
|
16
|
+
const line = index + 1
|
|
17
|
+
const fromImport = raw.match(/^\s*from\s+([\w.]+)\s+import\s/)
|
|
18
|
+
const plainImport = raw.match(/^\s*import\s+([\w.]+)/)
|
|
19
|
+
const topDef = raw.match(/^(?:async\s+)?def\s+(\w+)/)
|
|
20
|
+
const topClass = raw.match(/^class\s+(\w+)/)
|
|
21
|
+
const method = raw.match(/^\s+(?:async\s+)?def\s+(\w+)/)
|
|
22
|
+
const route = raw.match(/^\s*@\w+\.(route|get|post|put|patch|delete)\(\s*['"]([^'"]+)['"]/)
|
|
23
|
+
|
|
24
|
+
const constant = raw.match(/^([A-Za-z_]\w*)\s*=\s*(?:(['"])(.*?)\2|(-?\d+(?:\.\d+)?|True|False))\s*(?:#.*)?$/)
|
|
25
|
+
const comment = raw.match(/^\s*#\s*(.+)$/)
|
|
26
|
+
if (constant) {
|
|
27
|
+
const value = constant[3] !== undefined ? constant[3] : constant[4]
|
|
28
|
+
if (value.trim() && value.length <= 80 && !isSecretValue(constant[1], value)) values.push({ name: constant[1], value, line })
|
|
29
|
+
}
|
|
30
|
+
if (comment && comment[1].split(' ').length >= 3 && comment[1].length <= 120) comments.add(comment[1].trim())
|
|
31
|
+
|
|
32
|
+
if (fromImport) imports.add(fromImport[1])
|
|
33
|
+
else if (plainImport) imports.add(plainImport[1])
|
|
34
|
+
|
|
35
|
+
if (topClass) {
|
|
36
|
+
currentClass = { kind: 'class', name: topClass[1], line, methods: [] }
|
|
37
|
+
symbols.push(currentClass)
|
|
38
|
+
} else if (topDef) {
|
|
39
|
+
currentClass = null
|
|
40
|
+
symbols.push({ kind: 'function', name: topDef[1], line })
|
|
41
|
+
} else if (method && currentClass && method[1] !== '__init__') {
|
|
42
|
+
currentClass.methods.push(method[1])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (route) {
|
|
46
|
+
routes.push({
|
|
47
|
+
method: route[1] === 'route' ? 'GET' : route[1].toUpperCase(),
|
|
48
|
+
path: route[2],
|
|
49
|
+
line,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
imports: [...imports].slice(0, LIMIT),
|
|
56
|
+
exports: [],
|
|
57
|
+
symbols: symbols.slice(0, LIMIT),
|
|
58
|
+
routes: routes.slice(0, LIMIT),
|
|
59
|
+
values: values.slice(0, 200),
|
|
60
|
+
comments: [...comments].slice(0, 50),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
extractPy,
|
|
66
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Path = require('path')
|
|
4
|
+
const { resolveRef, listFiles, fileHistory, readBlobs } = require('./git-reader')
|
|
5
|
+
const { classify } = require('./file-filter')
|
|
6
|
+
const { PARSER_VERSION, extractFacts } = require('./extract')
|
|
7
|
+
|
|
8
|
+
const BATCH = 500
|
|
9
|
+
const CONFIG_FILES = ['package.json', 'tsconfig.json', 'jsconfig.json']
|
|
10
|
+
|
|
11
|
+
const parseJson = code => {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(code)
|
|
14
|
+
} catch (err) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(
|
|
17
|
+
code
|
|
18
|
+
.replace(/("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (match, string) => string || '')
|
|
19
|
+
.replace(/,(\s*[}\]])/g, '$1'),
|
|
20
|
+
)
|
|
21
|
+
} catch (inner) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const moduleAliases = pkg =>
|
|
28
|
+
Object.entries((pkg && pkg._moduleAliases) || {})
|
|
29
|
+
.filter(([, to]) => typeof to === 'string')
|
|
30
|
+
.map(([from, to]) => ({ from, to: Path.posix.normalize(to), wildcard: false }))
|
|
31
|
+
|
|
32
|
+
const pathAliases = tsconfig => {
|
|
33
|
+
const options = (tsconfig && tsconfig.compilerOptions) || {}
|
|
34
|
+
const base = options.baseUrl || '.'
|
|
35
|
+
return Object.entries(options.paths || {})
|
|
36
|
+
.filter(([, targets]) => Array.isArray(targets) && typeof targets[0] === 'string')
|
|
37
|
+
.map(([from, targets]) => ({
|
|
38
|
+
from: from.replace(/\*$/, ''),
|
|
39
|
+
to: Path.posix.normalize(Path.posix.join(base, targets[0].replace(/\*$/, ''))).replace(/^\.$/, ''),
|
|
40
|
+
wildcard: from.endsWith('*'),
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const readConfig = async (dir, files) => {
|
|
45
|
+
const byPath = new Map(files.map(file => [file.path, file.blobHash]))
|
|
46
|
+
const wanted = CONFIG_FILES.filter(name => byPath.has(name))
|
|
47
|
+
const blobs = wanted.length ? await readBlobs(dir, wanted.map(name => byPath.get(name))) : new Map()
|
|
48
|
+
const json = name => (byPath.has(name) ? parseJson(blobs.get(byPath.get(name)) || '') : null)
|
|
49
|
+
const pkg = json('package.json')
|
|
50
|
+
return {
|
|
51
|
+
packageName: (pkg && pkg.name) || null,
|
|
52
|
+
packageMain: (pkg && typeof pkg.main === 'string' && pkg.main) || null,
|
|
53
|
+
aliases: [...moduleAliases(pkg), ...pathAliases(json('tsconfig.json') || json('jsconfig.json'))],
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const buildRepoIndex = async (repo, previous) => {
|
|
58
|
+
const branch = repo.defaultBaseBranch
|
|
59
|
+
const { ref, commitSha, date } = resolveRef(repo.path, branch)
|
|
60
|
+
const reusable = previous && previous.parserVersion === PARSER_VERSION
|
|
61
|
+
if (reusable && previous.commitSha === commitSha) {
|
|
62
|
+
return { ...previous, name: repo.name, newlyParsed: 0 }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const files = listFiles(repo.path, ref).map(file => ({
|
|
66
|
+
...file,
|
|
67
|
+
status: classify(file.path, file.size),
|
|
68
|
+
}))
|
|
69
|
+
const known = reusable ? previous.facts : {}
|
|
70
|
+
const facts = {}
|
|
71
|
+
const toParse = []
|
|
72
|
+
files
|
|
73
|
+
.filter(file => file.status === 'parsed')
|
|
74
|
+
.forEach(file => {
|
|
75
|
+
if (known[file.blobHash]) {
|
|
76
|
+
facts[file.blobHash] = known[file.blobHash]
|
|
77
|
+
} else if (!facts[file.blobHash]) {
|
|
78
|
+
facts[file.blobHash] = null
|
|
79
|
+
toParse.push(file)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < toParse.length; i += BATCH) {
|
|
84
|
+
const batch = toParse.slice(i, i + BATCH)
|
|
85
|
+
const blobs = await readBlobs(repo.path, batch.map(file => file.blobHash))
|
|
86
|
+
batch.forEach(file => {
|
|
87
|
+
facts[file.blobHash] = extractFacts(file.path, blobs.get(file.blobHash) || '')
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
name: repo.name,
|
|
93
|
+
branch,
|
|
94
|
+
commitSha,
|
|
95
|
+
date,
|
|
96
|
+
parserVersion: PARSER_VERSION,
|
|
97
|
+
...(await readConfig(repo.path, files)),
|
|
98
|
+
files,
|
|
99
|
+
facts,
|
|
100
|
+
history: fileHistory(repo.path, ref, new Set(files.filter(file => file.status === 'parsed').map(file => file.path))),
|
|
101
|
+
newlyParsed: toParse.length,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
buildRepoIndex,
|
|
107
|
+
}
|