@bubblelab/bubble-runtime 0.1.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/LICENSE.txt +202 -0
- package/dist/extraction/BubbleParser.d.ts +80 -0
- package/dist/extraction/BubbleParser.d.ts.map +1 -0
- package/dist/extraction/BubbleParser.js +926 -0
- package/dist/extraction/BubbleParser.js.map +1 -0
- package/dist/extraction/index.d.ts +2 -0
- package/dist/extraction/index.d.ts.map +1 -0
- package/dist/extraction/index.js +4 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/injection/BubbleInjector.d.ts +71 -0
- package/dist/injection/BubbleInjector.d.ts.map +1 -0
- package/dist/injection/BubbleInjector.js +326 -0
- package/dist/injection/BubbleInjector.js.map +1 -0
- package/dist/injection/LoggerInjector.d.ts +33 -0
- package/dist/injection/LoggerInjector.d.ts.map +1 -0
- package/dist/injection/LoggerInjector.js +154 -0
- package/dist/injection/LoggerInjector.js.map +1 -0
- package/dist/injection/index.d.ts +3 -0
- package/dist/injection/index.d.ts.map +1 -0
- package/dist/injection/index.js +3 -0
- package/dist/injection/index.js.map +1 -0
- package/dist/parse/BubbleScript.d.ts +141 -0
- package/dist/parse/BubbleScript.d.ts.map +1 -0
- package/dist/parse/BubbleScript.js +513 -0
- package/dist/parse/BubbleScript.js.map +1 -0
- package/dist/parse/index.d.ts +3 -0
- package/dist/parse/index.d.ts.map +1 -0
- package/dist/parse/index.js +3 -0
- package/dist/parse/index.js.map +1 -0
- package/dist/parse/traceDependencies.d.ts +18 -0
- package/dist/parse/traceDependencies.d.ts.map +1 -0
- package/dist/parse/traceDependencies.js +181 -0
- package/dist/parse/traceDependencies.js.map +1 -0
- package/dist/runtime/BubbleRunner.d.ts +91 -0
- package/dist/runtime/BubbleRunner.d.ts.map +1 -0
- package/dist/runtime/BubbleRunner.js +586 -0
- package/dist/runtime/BubbleRunner.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/types.d.ts +29 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/bubble-helper.d.ts +11 -0
- package/dist/utils/bubble-helper.d.ts.map +1 -0
- package/dist/utils/bubble-helper.js +15 -0
- package/dist/utils/bubble-helper.js.map +1 -0
- package/dist/utils/parameter-formatter.d.ts +22 -0
- package/dist/utils/parameter-formatter.d.ts.map +1 -0
- package/dist/utils/parameter-formatter.js +219 -0
- package/dist/utils/parameter-formatter.js.map +1 -0
- package/dist/validation/BubbleValidator.d.ts +43 -0
- package/dist/validation/BubbleValidator.d.ts.map +1 -0
- package/dist/validation/BubbleValidator.js +172 -0
- package/dist/validation/BubbleValidator.js.map +1 -0
- package/dist/validation/index.d.ts +27 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +126 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
import { buildClassNameLookup } from '../utils/bubble-helper';
|
|
2
|
+
import { BubbleParameterType } from '@bubblelab/shared-schemas';
|
|
3
|
+
import { parseToolsParamValue } from '../utils/parameter-formatter';
|
|
4
|
+
export class BubbleParser {
|
|
5
|
+
bubbleScript;
|
|
6
|
+
constructor(bubbleScript) {
|
|
7
|
+
this.bubbleScript = bubbleScript;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse bubble dependencies from an AST using the provided factory and scope manager
|
|
11
|
+
*/
|
|
12
|
+
parseBubblesFromAST(bubbleFactory, ast, scopeManager) {
|
|
13
|
+
// Build registry lookup from bubble-core
|
|
14
|
+
const classNameToInfo = buildClassNameLookup(bubbleFactory);
|
|
15
|
+
if (classNameToInfo.size === 0) {
|
|
16
|
+
throw new Error('Failed to trace bubble dependencies: No bubbles found in BubbleFactory');
|
|
17
|
+
}
|
|
18
|
+
const nodes = {};
|
|
19
|
+
const errors = [];
|
|
20
|
+
// Find handle method location
|
|
21
|
+
const handleMethodLocation = this.findHandleMethodLocation(ast);
|
|
22
|
+
// Visit AST nodes to find bubble instantiations
|
|
23
|
+
this.visitNode(ast, nodes, classNameToInfo, scopeManager);
|
|
24
|
+
if (errors.length > 0) {
|
|
25
|
+
throw new Error(`Failed to trace bubble dependencies: ${errors.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
// Build a set of used variable IDs to ensure uniqueness for any synthetic IDs we allocate
|
|
28
|
+
const usedVariableIds = new Set();
|
|
29
|
+
for (const [idStr, node] of Object.entries(nodes)) {
|
|
30
|
+
const id = Number(idStr);
|
|
31
|
+
if (!Number.isNaN(id))
|
|
32
|
+
usedVariableIds.add(id);
|
|
33
|
+
for (const param of node.parameters) {
|
|
34
|
+
if (typeof param.variableId === 'number') {
|
|
35
|
+
usedVariableIds.add(param.variableId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// For each bubble, compute flat dependencies and construct a detailed dependency graph
|
|
40
|
+
for (const bubble of Object.values(nodes)) {
|
|
41
|
+
const all = this.findDependenciesForBubble([bubble.bubbleName], bubbleFactory, bubble.parameters);
|
|
42
|
+
bubble.dependencies = all;
|
|
43
|
+
// If this node is an ai-agent, extract tools for graph inclusion at the root level
|
|
44
|
+
let rootAIAgentTools;
|
|
45
|
+
if (bubble.bubbleName === 'ai-agent') {
|
|
46
|
+
const toolsParam = bubble.parameters.find((p) => p.name === 'tools');
|
|
47
|
+
const tools = toolsParam
|
|
48
|
+
? parseToolsParamValue(toolsParam.value)
|
|
49
|
+
: null;
|
|
50
|
+
if (Array.isArray(tools)) {
|
|
51
|
+
rootAIAgentTools = tools
|
|
52
|
+
.map((t) => t?.name)
|
|
53
|
+
.filter((n) => typeof n === 'string');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Build hierarchical graph annotated with uniqueId and variableId
|
|
57
|
+
const ordinalCounters = new Map();
|
|
58
|
+
bubble.dependencyGraph = this.buildDependencyGraph(bubble.bubbleName, bubbleFactory, new Set(), rootAIAgentTools, String(bubble.variableId), // Root uniqueId starts with the root variableId string
|
|
59
|
+
ordinalCounters, usedVariableIds, bubble.variableId, // Root variable id mirrors the parsed bubble's variable id
|
|
60
|
+
true, // suppress adding self segment for root
|
|
61
|
+
bubble.variableName);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
bubbles: nodes,
|
|
65
|
+
handleMethodLocation,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
findDependenciesForBubble(currentDependencies, bubbleFactory, parameters, seen = new Set()) {
|
|
69
|
+
const queue = [...currentDependencies];
|
|
70
|
+
// Mark initial seeds as seen so they are not included in results
|
|
71
|
+
for (const seed of currentDependencies)
|
|
72
|
+
seen.add(seed);
|
|
73
|
+
const result = [];
|
|
74
|
+
while (queue.length > 0) {
|
|
75
|
+
const name = queue.shift();
|
|
76
|
+
// If the bubble is an ai agent, add the tools to the dependencies
|
|
77
|
+
if (name === 'ai-agent') {
|
|
78
|
+
const toolsParam = parameters.find((param) => param.name === 'tools');
|
|
79
|
+
const tools = toolsParam
|
|
80
|
+
? parseToolsParamValue(toolsParam.value)
|
|
81
|
+
: null;
|
|
82
|
+
if (Array.isArray(tools)) {
|
|
83
|
+
for (const tool of tools) {
|
|
84
|
+
if (tool &&
|
|
85
|
+
typeof tool === 'object' &&
|
|
86
|
+
typeof tool.name === 'string') {
|
|
87
|
+
const toolName = tool.name;
|
|
88
|
+
if (seen.has(toolName))
|
|
89
|
+
continue;
|
|
90
|
+
seen.add(toolName);
|
|
91
|
+
result.push(toolName);
|
|
92
|
+
queue.push(toolName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const metadata = bubbleFactory.getMetadata(name);
|
|
98
|
+
const detailed = metadata?.bubbleDependenciesDetailed || [];
|
|
99
|
+
if (Array.isArray(detailed) && detailed.length > 0) {
|
|
100
|
+
for (const spec of detailed) {
|
|
101
|
+
const depName = spec.name;
|
|
102
|
+
if (!seen.has(depName)) {
|
|
103
|
+
seen.add(depName);
|
|
104
|
+
result.push(depName);
|
|
105
|
+
queue.push(depName);
|
|
106
|
+
}
|
|
107
|
+
// If this dependency is an AI agent with declared tools, include them as dependencies too
|
|
108
|
+
if (depName === 'ai-agent' && Array.isArray(spec.tools)) {
|
|
109
|
+
for (const toolName of spec.tools) {
|
|
110
|
+
if (seen.has(toolName))
|
|
111
|
+
continue;
|
|
112
|
+
seen.add(toolName);
|
|
113
|
+
result.push(toolName);
|
|
114
|
+
queue.push(toolName);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Fallback to flat dependencies
|
|
121
|
+
const deps = metadata?.bubbleDependencies || [];
|
|
122
|
+
for (const dep of deps) {
|
|
123
|
+
const depName = dep;
|
|
124
|
+
if (seen.has(depName))
|
|
125
|
+
continue;
|
|
126
|
+
seen.add(depName);
|
|
127
|
+
result.push(depName);
|
|
128
|
+
queue.push(depName);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
buildDependencyGraph(bubbleName, bubbleFactory, seen, toolsForThisNode, parentUniqueId = '', ordinalCounters = new Map(), usedVariableIds = new Set(), explicitVariableId, suppressSelfSegment = false, instanceVariableName) {
|
|
135
|
+
// Compute this node's uniqueId and variableId FIRST so even cycle hits have IDs
|
|
136
|
+
const countKey = `${parentUniqueId}|${bubbleName}`;
|
|
137
|
+
const nextOrdinal = (ordinalCounters.get(countKey) || 0) + 1;
|
|
138
|
+
ordinalCounters.set(countKey, nextOrdinal);
|
|
139
|
+
const uniqueId = suppressSelfSegment
|
|
140
|
+
? parentUniqueId
|
|
141
|
+
: parentUniqueId && parentUniqueId.length > 0
|
|
142
|
+
? `${parentUniqueId}.${bubbleName}#${nextOrdinal}`
|
|
143
|
+
: `${bubbleName}#${nextOrdinal}`;
|
|
144
|
+
const variableId = typeof explicitVariableId === 'number'
|
|
145
|
+
? explicitVariableId
|
|
146
|
+
: this.hashUniqueIdToVarId(uniqueId);
|
|
147
|
+
const metadata = bubbleFactory.getMetadata(bubbleName);
|
|
148
|
+
if (seen.has(bubbleName)) {
|
|
149
|
+
return {
|
|
150
|
+
name: bubbleName,
|
|
151
|
+
nodeType: metadata?.type || 'unknown',
|
|
152
|
+
uniqueId,
|
|
153
|
+
variableId,
|
|
154
|
+
variableName: instanceVariableName,
|
|
155
|
+
dependencies: [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const nextSeen = new Set(seen);
|
|
159
|
+
nextSeen.add(bubbleName);
|
|
160
|
+
const children = [];
|
|
161
|
+
const detailed = metadata?.bubbleDependenciesDetailed;
|
|
162
|
+
if (Array.isArray(detailed) && detailed.length > 0) {
|
|
163
|
+
for (const spec of detailed) {
|
|
164
|
+
const childName = spec.name;
|
|
165
|
+
const toolsForChild = childName === 'ai-agent' ? spec.tools : undefined;
|
|
166
|
+
const instancesArr = Array.isArray(spec.instances)
|
|
167
|
+
? spec.instances
|
|
168
|
+
: [];
|
|
169
|
+
const instanceCount = instancesArr.length > 0 ? instancesArr.length : 1;
|
|
170
|
+
const nodeType = bubbleFactory.getMetadata(childName)?.type || 'unknown';
|
|
171
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
172
|
+
const instVarName = instancesArr[i]?.variableName;
|
|
173
|
+
// Special handling: avoid cycles when ai-agent appears again. If seen already has ai-agent
|
|
174
|
+
// but we have tools to display, synthesize a child node with tool dependencies directly.
|
|
175
|
+
if (childName === 'ai-agent' &&
|
|
176
|
+
Array.isArray(toolsForChild) &&
|
|
177
|
+
nextSeen.has('ai-agent')) {
|
|
178
|
+
// Synthesize an ai-agent node under the current uniqueId with its own ordinal
|
|
179
|
+
const aiCountKey = `${uniqueId}|ai-agent`;
|
|
180
|
+
const aiOrdinal = (ordinalCounters.get(aiCountKey) || 0) + 1;
|
|
181
|
+
ordinalCounters.set(aiCountKey, aiOrdinal);
|
|
182
|
+
const aiAgentUniqueId = `${uniqueId}.ai-agent#${aiOrdinal}`;
|
|
183
|
+
const aiAgentVarId = this.hashUniqueIdToVarId(aiAgentUniqueId);
|
|
184
|
+
const toolChildren = [];
|
|
185
|
+
for (const toolName of toolsForChild) {
|
|
186
|
+
toolChildren.push(this.buildDependencyGraph(toolName, bubbleFactory, nextSeen, undefined, aiAgentUniqueId, ordinalCounters, usedVariableIds, undefined, false, toolName));
|
|
187
|
+
}
|
|
188
|
+
children.push({
|
|
189
|
+
name: 'ai-agent',
|
|
190
|
+
uniqueId: aiAgentUniqueId,
|
|
191
|
+
variableId: aiAgentVarId,
|
|
192
|
+
variableName: instVarName,
|
|
193
|
+
dependencies: toolChildren,
|
|
194
|
+
nodeType,
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
children.push(this.buildDependencyGraph(childName, bubbleFactory, nextSeen, toolsForChild, uniqueId, ordinalCounters, usedVariableIds, undefined, false, instVarName));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const directDeps = metadata?.bubbleDependencies || [];
|
|
204
|
+
for (const dep of directDeps) {
|
|
205
|
+
console.warn('No bubble detail dependency', dep);
|
|
206
|
+
children.push(this.buildDependencyGraph(dep, bubbleFactory, nextSeen, undefined, uniqueId, ordinalCounters, usedVariableIds, undefined, false, 'No bubble detail dependency'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Include dynamic tool dependencies for ai-agent at the root node
|
|
210
|
+
if (bubbleName === 'ai-agent' && Array.isArray(toolsForThisNode)) {
|
|
211
|
+
for (const toolName of toolsForThisNode) {
|
|
212
|
+
if (nextSeen.has(toolName))
|
|
213
|
+
continue;
|
|
214
|
+
// No variable name for tool, just use tool name
|
|
215
|
+
children.push(this.buildDependencyGraph(toolName, bubbleFactory, nextSeen, undefined, uniqueId, ordinalCounters, usedVariableIds, undefined, false, toolName));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const nodeObj = {
|
|
219
|
+
name: bubbleName,
|
|
220
|
+
uniqueId,
|
|
221
|
+
variableId,
|
|
222
|
+
variableName: instanceVariableName,
|
|
223
|
+
nodeType: metadata?.type || 'unknown',
|
|
224
|
+
dependencies: children,
|
|
225
|
+
};
|
|
226
|
+
return nodeObj;
|
|
227
|
+
}
|
|
228
|
+
// Deterministic non-negative integer ID from uniqueId string
|
|
229
|
+
hashUniqueIdToVarId(input) {
|
|
230
|
+
let hash = 2166136261; // FNV-1a 32-bit offset basis
|
|
231
|
+
for (let i = 0; i < input.length; i++) {
|
|
232
|
+
hash ^= input.charCodeAt(i);
|
|
233
|
+
hash = (hash * 16777619) >>> 0; // unsigned 32-bit
|
|
234
|
+
}
|
|
235
|
+
// Map to 6-digit range to avoid colliding with small AST ids while readable
|
|
236
|
+
const mapped = 100000 + (hash % 900000);
|
|
237
|
+
return mapped;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Build a JSON Schema object for the payload parameter of the top-level `handle` entrypoint.
|
|
241
|
+
* Supports primitives, arrays, unions (anyOf), intersections (allOf), type literals, and
|
|
242
|
+
* same-file interfaces/type aliases. Interface `extends` are ignored for now.
|
|
243
|
+
*/
|
|
244
|
+
getPayloadJsonSchema(ast) {
|
|
245
|
+
const handleNode = this.findHandleFunctionNode(ast);
|
|
246
|
+
if (!handleNode)
|
|
247
|
+
return null;
|
|
248
|
+
const params = handleNode.type === 'FunctionDeclaration' ||
|
|
249
|
+
handleNode.type === 'FunctionExpression' ||
|
|
250
|
+
handleNode.type === 'ArrowFunctionExpression'
|
|
251
|
+
? handleNode.params
|
|
252
|
+
: [];
|
|
253
|
+
if (!params || params.length === 0)
|
|
254
|
+
return null;
|
|
255
|
+
const firstParam = params[0];
|
|
256
|
+
let typeAnn;
|
|
257
|
+
if (firstParam.type === 'Identifier') {
|
|
258
|
+
typeAnn = firstParam.typeAnnotation || undefined;
|
|
259
|
+
}
|
|
260
|
+
else if (firstParam.type === 'AssignmentPattern' &&
|
|
261
|
+
firstParam.left.type === 'Identifier') {
|
|
262
|
+
typeAnn = firstParam.left.typeAnnotation || undefined;
|
|
263
|
+
}
|
|
264
|
+
else if (firstParam.type === 'RestElement' &&
|
|
265
|
+
firstParam.argument.type === 'Identifier') {
|
|
266
|
+
typeAnn = firstParam.argument.typeAnnotation || undefined;
|
|
267
|
+
}
|
|
268
|
+
if (!typeAnn)
|
|
269
|
+
return {};
|
|
270
|
+
return this.tsTypeToJsonSchema(typeAnn.typeAnnotation, ast) || {};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Find the actual Function/ArrowFunction node corresponding to the handle entrypoint.
|
|
274
|
+
*/
|
|
275
|
+
findHandleFunctionNode(ast) {
|
|
276
|
+
for (const stmt of ast.body) {
|
|
277
|
+
if (stmt.type === 'FunctionDeclaration' && stmt.id?.name === 'handle') {
|
|
278
|
+
return stmt;
|
|
279
|
+
}
|
|
280
|
+
if (stmt.type === 'ExportNamedDeclaration' &&
|
|
281
|
+
stmt.declaration?.type === 'FunctionDeclaration' &&
|
|
282
|
+
stmt.declaration.id?.name === 'handle') {
|
|
283
|
+
return stmt.declaration;
|
|
284
|
+
}
|
|
285
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
286
|
+
for (const d of stmt.declarations) {
|
|
287
|
+
if (d.id.type === 'Identifier' &&
|
|
288
|
+
d.id.name === 'handle' &&
|
|
289
|
+
d.init &&
|
|
290
|
+
(d.init.type === 'ArrowFunctionExpression' ||
|
|
291
|
+
d.init.type === 'FunctionExpression')) {
|
|
292
|
+
return d.init;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (stmt.type === 'ExportNamedDeclaration' &&
|
|
297
|
+
stmt.declaration?.type === 'VariableDeclaration') {
|
|
298
|
+
for (const d of stmt.declaration.declarations) {
|
|
299
|
+
if (d.id.type === 'Identifier' &&
|
|
300
|
+
d.id.name === 'handle' &&
|
|
301
|
+
d.init &&
|
|
302
|
+
(d.init.type === 'ArrowFunctionExpression' ||
|
|
303
|
+
d.init.type === 'FunctionExpression')) {
|
|
304
|
+
return d.init;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (stmt.type === 'ClassDeclaration') {
|
|
309
|
+
const fn = this.findHandleInClass(stmt);
|
|
310
|
+
if (fn)
|
|
311
|
+
return fn;
|
|
312
|
+
}
|
|
313
|
+
if (stmt.type === 'ExportNamedDeclaration' &&
|
|
314
|
+
stmt.declaration?.type === 'ClassDeclaration') {
|
|
315
|
+
const fn = this.findHandleInClass(stmt.declaration);
|
|
316
|
+
if (fn)
|
|
317
|
+
return fn;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
findHandleInClass(cls) {
|
|
323
|
+
for (const member of cls.body.body) {
|
|
324
|
+
if (member.type === 'MethodDefinition' &&
|
|
325
|
+
member.key.type === 'Identifier' &&
|
|
326
|
+
member.key.name === 'handle' &&
|
|
327
|
+
member.value.type === 'FunctionExpression') {
|
|
328
|
+
return member.value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
/** Convert a TS type AST node into a JSON Schema object */
|
|
334
|
+
tsTypeToJsonSchema(typeNode, ast) {
|
|
335
|
+
switch (typeNode.type) {
|
|
336
|
+
case 'TSStringKeyword':
|
|
337
|
+
return { type: 'string' };
|
|
338
|
+
case 'TSNumberKeyword':
|
|
339
|
+
return { type: 'number' };
|
|
340
|
+
case 'TSBooleanKeyword':
|
|
341
|
+
return { type: 'boolean' };
|
|
342
|
+
case 'TSNullKeyword':
|
|
343
|
+
return { type: 'null' };
|
|
344
|
+
case 'TSAnyKeyword':
|
|
345
|
+
case 'TSUnknownKeyword':
|
|
346
|
+
case 'TSUndefinedKeyword':
|
|
347
|
+
return {};
|
|
348
|
+
case 'TSLiteralType': {
|
|
349
|
+
const lit = typeNode.literal;
|
|
350
|
+
if (lit.type === 'Literal') {
|
|
351
|
+
return { const: lit.value };
|
|
352
|
+
}
|
|
353
|
+
return {};
|
|
354
|
+
}
|
|
355
|
+
case 'TSArrayType': {
|
|
356
|
+
const items = this.tsTypeToJsonSchema(typeNode.elementType, ast) || {};
|
|
357
|
+
return { type: 'array', items };
|
|
358
|
+
}
|
|
359
|
+
case 'TSUnionType': {
|
|
360
|
+
const anyOf = typeNode.types.map((t) => this.tsTypeToJsonSchema(t, ast) || {});
|
|
361
|
+
return { anyOf };
|
|
362
|
+
}
|
|
363
|
+
case 'TSIntersectionType': {
|
|
364
|
+
const allOf = typeNode.types.map((t) => this.tsTypeToJsonSchema(t, ast) || {});
|
|
365
|
+
return { allOf };
|
|
366
|
+
}
|
|
367
|
+
case 'TSTypeLiteral': {
|
|
368
|
+
return this.objectTypeToJsonSchema(typeNode, ast);
|
|
369
|
+
}
|
|
370
|
+
case 'TSIndexedAccessType': {
|
|
371
|
+
// Handle BubbleTriggerEventRegistry['event/key'] → specific event schema
|
|
372
|
+
const obj = typeNode.objectType;
|
|
373
|
+
const idx = typeNode.indexType;
|
|
374
|
+
if (obj.type === 'TSTypeReference' &&
|
|
375
|
+
obj.typeName.type === 'Identifier' &&
|
|
376
|
+
obj.typeName.name === 'BubbleTriggerEventRegistry' &&
|
|
377
|
+
idx.type === 'TSLiteralType' &&
|
|
378
|
+
idx.literal.type === 'Literal' &&
|
|
379
|
+
typeof idx.literal.value === 'string') {
|
|
380
|
+
const schema = this.eventKeyToSchema(idx.literal.value);
|
|
381
|
+
if (schema)
|
|
382
|
+
return schema;
|
|
383
|
+
}
|
|
384
|
+
return {};
|
|
385
|
+
}
|
|
386
|
+
case 'TSTypeReference': {
|
|
387
|
+
const name = this.extractTypeReferenceName(typeNode);
|
|
388
|
+
if (!name)
|
|
389
|
+
return {};
|
|
390
|
+
const resolved = this.resolveTypeNameToJson(name, ast);
|
|
391
|
+
return resolved || {};
|
|
392
|
+
}
|
|
393
|
+
default:
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
extractTypeReferenceName(ref) {
|
|
398
|
+
if (ref.typeName.type === 'Identifier')
|
|
399
|
+
return ref.typeName.name;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
objectTypeToJsonSchema(node, ast) {
|
|
403
|
+
const elements = node.type === 'TSTypeLiteral' ? node.members : node.body;
|
|
404
|
+
const properties = {};
|
|
405
|
+
const required = [];
|
|
406
|
+
for (const m of elements) {
|
|
407
|
+
if (m.type !== 'TSPropertySignature')
|
|
408
|
+
continue;
|
|
409
|
+
let keyName = null;
|
|
410
|
+
if (m.key.type === 'Identifier')
|
|
411
|
+
keyName = m.key.name;
|
|
412
|
+
else if (m.key.type === 'Literal' && typeof m.key.value === 'string')
|
|
413
|
+
keyName = m.key.value;
|
|
414
|
+
if (!keyName)
|
|
415
|
+
continue;
|
|
416
|
+
const propSchema = m.typeAnnotation
|
|
417
|
+
? this.tsTypeToJsonSchema(m.typeAnnotation.typeAnnotation, ast)
|
|
418
|
+
: {};
|
|
419
|
+
properties[keyName] = propSchema;
|
|
420
|
+
if (!m.optional)
|
|
421
|
+
required.push(keyName);
|
|
422
|
+
}
|
|
423
|
+
const schema = { type: 'object', properties };
|
|
424
|
+
if (required.length > 0)
|
|
425
|
+
schema.required = required;
|
|
426
|
+
return schema;
|
|
427
|
+
}
|
|
428
|
+
// Minimal mapping for known trigger event keys to JSON Schema shapes
|
|
429
|
+
eventKeyToSchema(eventKey) {
|
|
430
|
+
if (eventKey === 'slack/bot_mentioned') {
|
|
431
|
+
return {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
text: { type: 'string' },
|
|
435
|
+
channel: { type: 'string' },
|
|
436
|
+
thread_ts: { type: 'string' },
|
|
437
|
+
user: { type: 'string' },
|
|
438
|
+
slack_event: { type: 'object' },
|
|
439
|
+
// Allow additional field used in flows
|
|
440
|
+
monthlyLimitError: {},
|
|
441
|
+
},
|
|
442
|
+
required: ['text', 'channel', 'user', 'slack_event'],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (eventKey === 'webhook/http') {
|
|
446
|
+
return {
|
|
447
|
+
type: 'object',
|
|
448
|
+
properties: {
|
|
449
|
+
body: { type: 'object' },
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (eventKey === 'gmail/email_received') {
|
|
454
|
+
return {
|
|
455
|
+
type: 'object',
|
|
456
|
+
properties: {
|
|
457
|
+
email: { type: 'string' },
|
|
458
|
+
},
|
|
459
|
+
required: ['email'],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (eventKey === 'schedule/cron/daily') {
|
|
463
|
+
return {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
cron: { type: 'string' },
|
|
467
|
+
},
|
|
468
|
+
required: ['cron'],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (eventKey === 'slack/message_received') {
|
|
472
|
+
return {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
text: { type: 'string' },
|
|
476
|
+
channel: { type: 'string' },
|
|
477
|
+
user: { type: 'string' },
|
|
478
|
+
channel_type: { type: 'string' },
|
|
479
|
+
slack_event: { type: 'object' },
|
|
480
|
+
},
|
|
481
|
+
required: ['text', 'channel', 'user', 'slack_event'],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
/** Resolve in-file interface/type alias by name to JSON Schema */
|
|
487
|
+
resolveTypeNameToJson(name, ast) {
|
|
488
|
+
for (const stmt of ast.body) {
|
|
489
|
+
if (stmt.type === 'TSInterfaceDeclaration' && stmt.id.name === name) {
|
|
490
|
+
return this.objectTypeToJsonSchema(stmt.body, ast);
|
|
491
|
+
}
|
|
492
|
+
if (stmt.type === 'TSTypeAliasDeclaration' && stmt.id.name === name) {
|
|
493
|
+
return this.tsTypeToJsonSchema(stmt.typeAnnotation, ast) || {};
|
|
494
|
+
}
|
|
495
|
+
if (stmt.type === 'ExportNamedDeclaration' &&
|
|
496
|
+
stmt.declaration?.type === 'TSInterfaceDeclaration' &&
|
|
497
|
+
stmt.declaration.id.name === name) {
|
|
498
|
+
return this.objectTypeToJsonSchema(stmt.declaration.body, ast);
|
|
499
|
+
}
|
|
500
|
+
if (stmt.type === 'ExportNamedDeclaration' &&
|
|
501
|
+
stmt.declaration?.type === 'TSTypeAliasDeclaration' &&
|
|
502
|
+
stmt.declaration.id.name === name) {
|
|
503
|
+
return (this.tsTypeToJsonSchema(stmt.declaration.typeAnnotation, ast) || {});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Find the handle method location in the AST
|
|
510
|
+
*/
|
|
511
|
+
findHandleMethodLocation(ast) {
|
|
512
|
+
for (const statement of ast.body) {
|
|
513
|
+
// Look for function declarations named 'handle'
|
|
514
|
+
if (statement.type === 'FunctionDeclaration' &&
|
|
515
|
+
statement.id?.name === 'handle') {
|
|
516
|
+
return {
|
|
517
|
+
startLine: statement.loc?.start.line || -1,
|
|
518
|
+
endLine: statement.loc?.end.line || -1,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// Look for exported function declarations: export function handle() {}
|
|
522
|
+
if (statement.type === 'ExportNamedDeclaration' &&
|
|
523
|
+
statement.declaration?.type === 'FunctionDeclaration' &&
|
|
524
|
+
statement.declaration.id?.name === 'handle') {
|
|
525
|
+
return {
|
|
526
|
+
startLine: statement.declaration.loc?.start.line || -1,
|
|
527
|
+
endLine: statement.declaration.loc?.end.line || -1,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
// Look for variable declarations with function expressions: const handle = () => {}
|
|
531
|
+
if (statement.type === 'VariableDeclaration') {
|
|
532
|
+
for (const declarator of statement.declarations) {
|
|
533
|
+
if (declarator.type === 'VariableDeclarator' &&
|
|
534
|
+
declarator.id.type === 'Identifier' &&
|
|
535
|
+
declarator.id.name === 'handle' &&
|
|
536
|
+
(declarator.init?.type === 'FunctionExpression' ||
|
|
537
|
+
declarator.init?.type === 'ArrowFunctionExpression')) {
|
|
538
|
+
return {
|
|
539
|
+
startLine: declarator.init.loc?.start.line || -1,
|
|
540
|
+
endLine: declarator.init.loc?.end.line || -1,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Look for exported variable declarations: export const handle = () => {}
|
|
546
|
+
if (statement.type === 'ExportNamedDeclaration' &&
|
|
547
|
+
statement.declaration?.type === 'VariableDeclaration') {
|
|
548
|
+
for (const declarator of statement.declaration.declarations) {
|
|
549
|
+
if (declarator.type === 'VariableDeclarator' &&
|
|
550
|
+
declarator.id.type === 'Identifier' &&
|
|
551
|
+
declarator.id.name === 'handle' &&
|
|
552
|
+
(declarator.init?.type === 'FunctionExpression' ||
|
|
553
|
+
declarator.init?.type === 'ArrowFunctionExpression')) {
|
|
554
|
+
return {
|
|
555
|
+
startLine: declarator.init.loc?.start.line || -1,
|
|
556
|
+
endLine: declarator.init.loc?.end.line || -1,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Look for exported class declarations with handle method
|
|
562
|
+
if (statement.type === 'ExportNamedDeclaration' &&
|
|
563
|
+
statement.declaration?.type === 'ClassDeclaration') {
|
|
564
|
+
const handleMethod = this.findHandleMethodInClass(statement.declaration);
|
|
565
|
+
if (handleMethod) {
|
|
566
|
+
return handleMethod;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Look for class declarations with handle method
|
|
570
|
+
if (statement.type === 'ClassDeclaration') {
|
|
571
|
+
const handleMethod = this.findHandleMethodInClass(statement);
|
|
572
|
+
if (handleMethod) {
|
|
573
|
+
return handleMethod;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return null; // Handle method not found
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Find handle method within a class declaration
|
|
581
|
+
*/
|
|
582
|
+
findHandleMethodInClass(classDeclaration) {
|
|
583
|
+
if (!classDeclaration.body)
|
|
584
|
+
return null;
|
|
585
|
+
for (const member of classDeclaration.body.body) {
|
|
586
|
+
if (member.type === 'MethodDefinition' &&
|
|
587
|
+
member.key.type === 'Identifier' &&
|
|
588
|
+
member.key.name === 'handle' &&
|
|
589
|
+
member.value.type === 'FunctionExpression') {
|
|
590
|
+
return {
|
|
591
|
+
startLine: member.value.loc?.start.line || -1,
|
|
592
|
+
endLine: member.value.loc?.end.line || -1,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Recursively visit AST nodes to find bubble instantiations
|
|
600
|
+
*/
|
|
601
|
+
visitNode(node, nodes, classNameLookup, scopeManager) {
|
|
602
|
+
// Capture variable declarations
|
|
603
|
+
if (node.type === 'VariableDeclaration') {
|
|
604
|
+
for (const declarator of node.declarations) {
|
|
605
|
+
if (declarator.type === 'VariableDeclarator' &&
|
|
606
|
+
declarator.id.type === 'Identifier' &&
|
|
607
|
+
declarator.init) {
|
|
608
|
+
const nameText = declarator.id.name;
|
|
609
|
+
const bubbleNode = this.extractBubbleFromExpression(declarator.init, classNameLookup);
|
|
610
|
+
if (bubbleNode) {
|
|
611
|
+
bubbleNode.variableName = nameText;
|
|
612
|
+
// Find the Variable object for this bubble declaration
|
|
613
|
+
const variable = this.findVariableForBubble(nameText, node, scopeManager);
|
|
614
|
+
if (variable) {
|
|
615
|
+
bubbleNode.variableId = variable.$id;
|
|
616
|
+
// Add variable references to parameters
|
|
617
|
+
bubbleNode.parameters = this.addVariableReferencesToParameters(bubbleNode.parameters, node, scopeManager);
|
|
618
|
+
nodes[variable.$id] = bubbleNode;
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
// Fallback: use variable name as key if Variable not found
|
|
622
|
+
throw new Error(`Variable ${nameText} not found in scope manager`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Anonymous instantiations in expression statements
|
|
629
|
+
if (node.type === 'ExpressionStatement') {
|
|
630
|
+
const bubbleNode = this.extractBubbleFromExpression(node.expression, classNameLookup);
|
|
631
|
+
if (bubbleNode) {
|
|
632
|
+
const synthetic = `_anonymous_${bubbleNode.className}_${Object.keys(nodes).length}`;
|
|
633
|
+
bubbleNode.variableName = synthetic;
|
|
634
|
+
// For anonymous bubbles, use negative synthetic ID (no Variable object exists)
|
|
635
|
+
const syntheticId = -1 * (Object.keys(nodes).length + 1);
|
|
636
|
+
bubbleNode.variableId = syntheticId;
|
|
637
|
+
// Still add variable references to parameters (they can reference other variables)
|
|
638
|
+
bubbleNode.parameters = this.addVariableReferencesToParameters(bubbleNode.parameters, node, scopeManager);
|
|
639
|
+
nodes[syntheticId] = bubbleNode;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Recursively visit child nodes
|
|
643
|
+
for (const key in node) {
|
|
644
|
+
const child = node[key];
|
|
645
|
+
if (Array.isArray(child)) {
|
|
646
|
+
for (const item of child) {
|
|
647
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
648
|
+
this.visitNode(item, nodes, classNameLookup, scopeManager);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
else if (child && typeof child === 'object' && 'type' in child) {
|
|
653
|
+
this.visitNode(child, nodes, classNameLookup, scopeManager);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Find the Variable object corresponding to a bubble declaration
|
|
659
|
+
*/
|
|
660
|
+
findVariableForBubble(variableName, declarationNode, scopeManager) {
|
|
661
|
+
const line = declarationNode.loc?.start.line;
|
|
662
|
+
if (!line)
|
|
663
|
+
return null;
|
|
664
|
+
// Find scopes that contain this line
|
|
665
|
+
for (const scope of scopeManager.scopes) {
|
|
666
|
+
const scopeStart = scope.block.loc?.start.line || 0;
|
|
667
|
+
const scopeEnd = scope.block.loc?.end.line || 0;
|
|
668
|
+
if (line >= scopeStart && line <= scopeEnd) {
|
|
669
|
+
// Look for a variable with this name in this scope
|
|
670
|
+
for (const variable of scope.variables) {
|
|
671
|
+
if (variable.name === variableName) {
|
|
672
|
+
// Check if this variable is declared on or near the same line
|
|
673
|
+
const declLine = variable.defs[0]?.node?.loc?.start?.line;
|
|
674
|
+
if (declLine && Math.abs(declLine - line) <= 2) {
|
|
675
|
+
return variable;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Add variable ID references to parameters that are variables
|
|
685
|
+
*/
|
|
686
|
+
addVariableReferencesToParameters(parameters, contextNode, scopeManager) {
|
|
687
|
+
const contextLine = contextNode.loc?.start.line || 0;
|
|
688
|
+
return parameters.map((param) => {
|
|
689
|
+
if (param.type === 'variable') {
|
|
690
|
+
const baseVariableName = this.extractBaseVariableName(param.value);
|
|
691
|
+
if (baseVariableName) {
|
|
692
|
+
const variableId = this.findVariableIdByName(baseVariableName, contextLine, scopeManager);
|
|
693
|
+
if (variableId !== undefined) {
|
|
694
|
+
return {
|
|
695
|
+
...param,
|
|
696
|
+
variableId,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return param;
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Extract base variable name from expressions like "prompts[i]", "result.data"
|
|
706
|
+
*/
|
|
707
|
+
extractBaseVariableName(expression) {
|
|
708
|
+
const trimmed = expression.trim();
|
|
709
|
+
// Handle array access: "prompts[i]" -> "prompts"
|
|
710
|
+
const arrayMatch = trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\[/);
|
|
711
|
+
if (arrayMatch) {
|
|
712
|
+
return arrayMatch[1];
|
|
713
|
+
}
|
|
714
|
+
// Handle property access: "result.data" -> "result"
|
|
715
|
+
const propertyMatch = trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\./);
|
|
716
|
+
if (propertyMatch) {
|
|
717
|
+
return propertyMatch[1];
|
|
718
|
+
}
|
|
719
|
+
// Handle simple variable: "myVar" -> "myVar"
|
|
720
|
+
const simpleMatch = trimmed.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/);
|
|
721
|
+
if (simpleMatch) {
|
|
722
|
+
return trimmed;
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Find the Variable.$id for a variable name at a specific context line
|
|
728
|
+
*/
|
|
729
|
+
findVariableIdByName(variableName, contextLine, scopeManager) {
|
|
730
|
+
// Find ALL scopes that contain this line (not just the smallest)
|
|
731
|
+
const containingScopes = [];
|
|
732
|
+
for (const scope of scopeManager.scopes) {
|
|
733
|
+
const scopeStart = scope.block.loc?.start.line || 0;
|
|
734
|
+
const scopeEnd = scope.block.loc?.end.line || 0;
|
|
735
|
+
if (contextLine >= scopeStart && contextLine <= scopeEnd) {
|
|
736
|
+
containingScopes.push(scope);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (containingScopes.length === 0) {
|
|
740
|
+
console.warn(`No scopes found containing line ${contextLine} for variable ${variableName}`);
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
// Look through all containing scopes and their parents
|
|
744
|
+
const allScopes = new Set();
|
|
745
|
+
for (const scope of containingScopes) {
|
|
746
|
+
let currentScope = scope;
|
|
747
|
+
while (currentScope) {
|
|
748
|
+
allScopes.add(currentScope);
|
|
749
|
+
if (!currentScope.upper)
|
|
750
|
+
break;
|
|
751
|
+
currentScope = currentScope.upper;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// Search through all accessible scopes
|
|
755
|
+
for (const scope of allScopes) {
|
|
756
|
+
for (const variable of scope.variables) {
|
|
757
|
+
if (variable.name === variableName) {
|
|
758
|
+
// Check if this variable is declared before the context line
|
|
759
|
+
const declLine = variable.defs[0]?.node?.loc?.start?.line;
|
|
760
|
+
if (declLine && declLine <= contextLine) {
|
|
761
|
+
return variable.$id;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
console.warn(`Variable ${variableName} not found or not declared before line ${contextLine}`);
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Extract bubble information from an expression node
|
|
771
|
+
*/
|
|
772
|
+
extractBubbleFromExpression(expr, classNameLookup) {
|
|
773
|
+
// await new X(...)
|
|
774
|
+
if (expr.type === 'AwaitExpression') {
|
|
775
|
+
const inner = this.extractBubbleFromExpression(expr.argument, classNameLookup);
|
|
776
|
+
if (inner)
|
|
777
|
+
inner.hasAwait = true;
|
|
778
|
+
return inner;
|
|
779
|
+
}
|
|
780
|
+
// new X({...})
|
|
781
|
+
if (expr.type === 'NewExpression') {
|
|
782
|
+
return this.extractFromNewExpression(expr, classNameLookup);
|
|
783
|
+
}
|
|
784
|
+
// new X({...}).action() pattern
|
|
785
|
+
if (expr.type === 'CallExpression' &&
|
|
786
|
+
expr.callee.type === 'MemberExpression') {
|
|
787
|
+
const prop = expr.callee;
|
|
788
|
+
if (prop.property.type === 'Identifier' &&
|
|
789
|
+
prop.property.name === 'action' &&
|
|
790
|
+
prop.object.type === 'NewExpression') {
|
|
791
|
+
const node = this.extractFromNewExpression(prop.object, classNameLookup);
|
|
792
|
+
if (node)
|
|
793
|
+
node.hasActionCall = true;
|
|
794
|
+
return node;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Extract bubble information from a NewExpression node
|
|
801
|
+
*/
|
|
802
|
+
extractFromNewExpression(newExpr, classNameLookup) {
|
|
803
|
+
if (!newExpr.callee || newExpr.callee.type !== 'Identifier')
|
|
804
|
+
return null;
|
|
805
|
+
const className = newExpr.callee.name;
|
|
806
|
+
const info = classNameLookup.get(className);
|
|
807
|
+
if (!info)
|
|
808
|
+
return null;
|
|
809
|
+
const parameters = [];
|
|
810
|
+
if (newExpr.arguments && newExpr.arguments.length > 0) {
|
|
811
|
+
const firstArg = newExpr.arguments[0];
|
|
812
|
+
if (firstArg.type === 'ObjectExpression') {
|
|
813
|
+
for (const prop of firstArg.properties) {
|
|
814
|
+
if (prop.type === 'Property') {
|
|
815
|
+
if (prop.key.type === 'Identifier' &&
|
|
816
|
+
'type' in prop.value &&
|
|
817
|
+
prop.value.type !== 'AssignmentPattern') {
|
|
818
|
+
const name = prop.key.name;
|
|
819
|
+
const value = this.extractParameterValue(prop.value);
|
|
820
|
+
// Extract location information for the parameter value
|
|
821
|
+
const valueExpr = prop.value;
|
|
822
|
+
const location = valueExpr.loc
|
|
823
|
+
? {
|
|
824
|
+
startLine: valueExpr.loc.start.line,
|
|
825
|
+
startCol: valueExpr.loc.start.column,
|
|
826
|
+
endLine: valueExpr.loc.end.line,
|
|
827
|
+
endCol: valueExpr.loc.end.column,
|
|
828
|
+
}
|
|
829
|
+
: undefined;
|
|
830
|
+
parameters.push({
|
|
831
|
+
name,
|
|
832
|
+
...value,
|
|
833
|
+
location,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else if (prop.type === 'SpreadElement') {
|
|
838
|
+
// Handle spread properties if needed
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
variableId: -1,
|
|
845
|
+
variableName: '',
|
|
846
|
+
bubbleName: info.bubbleName,
|
|
847
|
+
className: info.className,
|
|
848
|
+
parameters,
|
|
849
|
+
hasAwait: false,
|
|
850
|
+
hasActionCall: false,
|
|
851
|
+
nodeType: info.nodeType,
|
|
852
|
+
location: {
|
|
853
|
+
startLine: newExpr.loc?.start.line || 0,
|
|
854
|
+
startCol: newExpr.loc?.start.column || 0,
|
|
855
|
+
endLine: newExpr.loc?.end.line || 0,
|
|
856
|
+
endCol: newExpr.loc?.end.column || 0,
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Extract parameter value and type from an expression
|
|
862
|
+
*/
|
|
863
|
+
extractParameterValue(expression) {
|
|
864
|
+
const valueText = this.bubbleScript.substring(expression.range[0], expression.range[1]);
|
|
865
|
+
// process.env detection (with or without non-null)
|
|
866
|
+
const isProcessEnv = (text) => text.startsWith('process.env.');
|
|
867
|
+
if (expression.type === 'TSNonNullExpression') {
|
|
868
|
+
const inner = expression.expression;
|
|
869
|
+
if (inner.type === 'MemberExpression') {
|
|
870
|
+
const full = this.bubbleScript.substring(inner.range[0], inner.range[1]);
|
|
871
|
+
if (isProcessEnv(full)) {
|
|
872
|
+
return { value: valueText, type: BubbleParameterType.ENV };
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (expression.type === 'MemberExpression' ||
|
|
877
|
+
expression.type === 'ChainExpression') {
|
|
878
|
+
const full = valueText;
|
|
879
|
+
if (isProcessEnv(full)) {
|
|
880
|
+
return { value: full, type: BubbleParameterType.ENV };
|
|
881
|
+
}
|
|
882
|
+
return { value: full, type: BubbleParameterType.VARIABLE };
|
|
883
|
+
}
|
|
884
|
+
// Identifiers treated as variable references
|
|
885
|
+
if (expression.type === 'Identifier') {
|
|
886
|
+
return { value: valueText, type: BubbleParameterType.VARIABLE };
|
|
887
|
+
}
|
|
888
|
+
// Literals and structured
|
|
889
|
+
if (expression.type === 'Literal') {
|
|
890
|
+
if (typeof expression.value === 'string') {
|
|
891
|
+
return { value: valueText, type: BubbleParameterType.STRING };
|
|
892
|
+
}
|
|
893
|
+
if (typeof expression.value === 'number') {
|
|
894
|
+
return { value: valueText, type: BubbleParameterType.NUMBER };
|
|
895
|
+
}
|
|
896
|
+
if (typeof expression.value === 'boolean') {
|
|
897
|
+
return { value: valueText, type: BubbleParameterType.BOOLEAN };
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (expression.type === 'TemplateLiteral') {
|
|
901
|
+
return { value: valueText, type: BubbleParameterType.STRING };
|
|
902
|
+
}
|
|
903
|
+
if (expression.type === 'ArrayExpression') {
|
|
904
|
+
return { value: valueText, type: BubbleParameterType.ARRAY };
|
|
905
|
+
}
|
|
906
|
+
if (expression.type === 'ObjectExpression') {
|
|
907
|
+
return { value: valueText, type: BubbleParameterType.OBJECT };
|
|
908
|
+
}
|
|
909
|
+
// Check for complex expressions (anything that's not a simple literal or identifier)
|
|
910
|
+
// These are expressions that need to be evaluated rather than treated as literal values
|
|
911
|
+
const simpleTypes = [
|
|
912
|
+
'Literal',
|
|
913
|
+
'Identifier',
|
|
914
|
+
'MemberExpression',
|
|
915
|
+
'TemplateLiteral',
|
|
916
|
+
'ArrayExpression',
|
|
917
|
+
'ObjectExpression',
|
|
918
|
+
];
|
|
919
|
+
if (!simpleTypes.includes(expression.type)) {
|
|
920
|
+
return { value: valueText, type: BubbleParameterType.EXPRESSION };
|
|
921
|
+
}
|
|
922
|
+
// Fallback
|
|
923
|
+
return { value: valueText, type: BubbleParameterType.UNKNOWN };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
//# sourceMappingURL=BubbleParser.js.map
|