@pikku/inspector 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/add/add-forge-credential.d.ts +8 -0
  3. package/dist/add/add-forge-credential.js +77 -0
  4. package/dist/add/add-forge-node.d.ts +7 -0
  5. package/dist/add/add-forge-node.js +77 -0
  6. package/dist/add/add-functions.js +102 -9
  7. package/dist/add/add-http-route.js +24 -1
  8. package/dist/add/add-rpc-invocations.d.ts +3 -0
  9. package/dist/add/add-rpc-invocations.js +51 -25
  10. package/dist/add/add-workflow-graph.d.ts +6 -0
  11. package/dist/add/add-workflow-graph.js +659 -0
  12. package/dist/add/add-workflow.js +118 -22
  13. package/dist/error-codes.d.ts +3 -1
  14. package/dist/error-codes.js +3 -1
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +2 -0
  17. package/dist/inspector.js +19 -3
  18. package/dist/types.d.ts +26 -0
  19. package/dist/utils/extract-function-name.js +7 -7
  20. package/dist/utils/get-property-value.d.ts +2 -1
  21. package/dist/utils/get-property-value.js +6 -2
  22. package/dist/utils/serialize-inspector-state.d.ts +24 -1
  23. package/dist/utils/serialize-inspector-state.js +24 -0
  24. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  25. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +898 -0
  26. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  27. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +549 -68
  28. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  29. package/dist/utils/workflow/dsl/index.js +7 -0
  30. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  31. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  32. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  33. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  34. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  35. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +316 -0
  36. package/dist/utils/workflow/graph/index.d.ts +6 -0
  37. package/dist/utils/workflow/graph/index.js +6 -0
  38. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +43 -0
  39. package/dist/utils/workflow/graph/serialize-workflow-graph.js +152 -0
  40. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +229 -0
  41. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  42. package/dist/visit.js +6 -0
  43. package/package.json +14 -2
  44. package/src/add/add-forge-credential.ts +119 -0
  45. package/src/add/add-forge-node.ts +132 -0
  46. package/src/add/add-functions.ts +129 -15
  47. package/src/add/add-http-route.ts +25 -1
  48. package/src/add/add-rpc-invocations.ts +61 -31
  49. package/src/add/add-workflow-graph.ts +864 -0
  50. package/src/add/add-workflow.ts +112 -26
  51. package/src/error-codes.ts +3 -1
  52. package/src/index.ts +10 -0
  53. package/src/inspector.ts +20 -4
  54. package/src/types.ts +25 -1
  55. package/src/utils/extract-function-name.ts +7 -7
  56. package/src/utils/get-property-value.ts +9 -2
  57. package/src/utils/serialize-inspector-state.ts +39 -1
  58. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1180 -0
  59. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +654 -81
  60. package/src/utils/workflow/dsl/index.ts +11 -0
  61. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  62. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  63. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +415 -0
  64. package/src/utils/workflow/graph/index.ts +6 -0
  65. package/src/utils/workflow/graph/serialize-workflow-graph.ts +223 -0
  66. package/src/utils/workflow/graph/workflow-graph.types.ts +280 -0
  67. package/src/visit.ts +6 -0
  68. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,132 @@
1
+ import * as ts from 'typescript'
2
+ import {
3
+ getPropertyValue,
4
+ getCommonWireMetaData,
5
+ } from '../utils/get-property-value.js'
6
+ import { AddWiring } from '../types.js'
7
+ import { ErrorCode } from '../error-codes.js'
8
+ import type { ForgeNodeType } from '@pikku/core/forge-node'
9
+
10
+ /**
11
+ * Inspector for wireForgeNode calls.
12
+ * Extracts metadata for Forge workflow builder nodes.
13
+ * Note: wireForgeNode is metadata-only - no runtime behavior.
14
+ */
15
+ export const addForgeNode: AddWiring = (
16
+ logger,
17
+ node,
18
+ checker,
19
+ state,
20
+ _options
21
+ ) => {
22
+ if (!ts.isCallExpression(node)) {
23
+ return
24
+ }
25
+
26
+ const args = node.arguments
27
+ const firstArg = args[0]
28
+ const expression = node.expression
29
+
30
+ // Check if the call is to wireForgeNode
31
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireForgeNode') {
32
+ return
33
+ }
34
+
35
+ if (!firstArg) {
36
+ return
37
+ }
38
+
39
+ if (ts.isObjectLiteralExpression(firstArg)) {
40
+ const obj = firstArg
41
+
42
+ const nameValue = getPropertyValue(obj, 'name') as string | null
43
+ const displayNameValue = getPropertyValue(obj, 'displayName') as
44
+ | string
45
+ | null
46
+ const categoryValue = getPropertyValue(obj, 'category') as string | null
47
+ const typeValue = getPropertyValue(obj, 'type') as ForgeNodeType | null
48
+ const rpcValue = getPropertyValue(obj, 'rpc') as string | null
49
+ const errorOutputValue = getPropertyValue(obj, 'errorOutput') as
50
+ | boolean
51
+ | null
52
+
53
+ const { tags, description } = getCommonWireMetaData(
54
+ obj,
55
+ 'Forge node',
56
+ nameValue,
57
+ logger
58
+ )
59
+
60
+ // Validate required fields
61
+ if (!nameValue) {
62
+ logger.critical(
63
+ ErrorCode.MISSING_NAME,
64
+ "Forge node is missing the required 'name' property."
65
+ )
66
+ return
67
+ }
68
+
69
+ if (!displayNameValue) {
70
+ logger.critical(
71
+ ErrorCode.MISSING_NAME,
72
+ `Forge node '${nameValue}' is missing the required 'displayName' property.`
73
+ )
74
+ return
75
+ }
76
+
77
+ if (!categoryValue) {
78
+ logger.critical(
79
+ ErrorCode.MISSING_NAME,
80
+ `Forge node '${nameValue}' is missing the required 'category' property.`
81
+ )
82
+ return
83
+ }
84
+
85
+ if (!typeValue) {
86
+ logger.critical(
87
+ ErrorCode.MISSING_NAME,
88
+ `Forge node '${nameValue}' is missing the required 'type' property.`
89
+ )
90
+ return
91
+ }
92
+
93
+ if (!['trigger', 'action', 'end'].includes(typeValue)) {
94
+ logger.critical(
95
+ ErrorCode.INVALID_VALUE,
96
+ `Forge node '${nameValue}' has invalid type '${typeValue}'. Must be 'trigger', 'action', or 'end'.`
97
+ )
98
+ return
99
+ }
100
+
101
+ if (!rpcValue) {
102
+ logger.critical(
103
+ ErrorCode.MISSING_NAME,
104
+ `Forge node '${nameValue}' is missing the required 'rpc' property.`
105
+ )
106
+ return
107
+ }
108
+
109
+ // Get function metadata for input/output schemas
110
+ const fnMeta = state.functions.meta[rpcValue]
111
+ const inputSchemaName = fnMeta?.inputs?.[0] || null
112
+ const outputSchemaName = fnMeta?.outputs?.[0] || null
113
+
114
+ // Note: Category validation against forge.node.categories config
115
+ // is done at CLI build time, not during inspection
116
+
117
+ state.forgeNodes.files.add(node.getSourceFile().fileName)
118
+
119
+ state.forgeNodes.meta[nameValue] = {
120
+ name: nameValue,
121
+ displayName: displayNameValue,
122
+ category: categoryValue,
123
+ type: typeValue,
124
+ rpc: rpcValue,
125
+ description,
126
+ errorOutput: errorOutputValue ?? false,
127
+ inputSchemaName,
128
+ outputSchemaName,
129
+ tags,
130
+ }
131
+ }
132
+ }
@@ -9,6 +9,8 @@ import {
9
9
  getCommonWireMetaData,
10
10
  } from '../utils/get-property-value.js'
11
11
  import { resolveMiddleware } from '../utils/middleware.js'
12
+ import { resolvePermissions } from '../utils/permissions.js'
13
+ import { ErrorCode } from '../error-codes.js'
12
14
 
13
15
  const isValidVariableName = (name: string) => {
14
16
  const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
@@ -152,14 +154,18 @@ const resolveUnionTypes = (
152
154
  // Check if it's a union type AND not part of an intersection
153
155
  if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
154
156
  for (const t of type.types) {
155
- const name = nullifyTypes(checker.typeToString(t))
157
+ const name = nullifyTypes(
158
+ checker.typeToString(t, undefined, ts.TypeFormatFlags.NoTruncation)
159
+ )
156
160
  if (name) {
157
161
  types.push(t)
158
162
  names.push(name)
159
163
  }
160
164
  }
161
165
  } else {
162
- const name = nullifyTypes(checker.typeToString(type))
166
+ const name = nullifyTypes(
167
+ checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)
168
+ )
163
169
  if (name) {
164
170
  types.push(type)
165
171
  names.push(name)
@@ -306,6 +312,7 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
306
312
  const { pikkuFuncName, name, explicitName, exportedName } =
307
313
  extractFunctionName(node, checker, state.rootDir)
308
314
 
315
+ let title: string | undefined
309
316
  let tags: string[] | undefined
310
317
  let summary: string | undefined
311
318
  let description: string | undefined
@@ -322,16 +329,85 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
322
329
  isDirectFunction,
323
330
  } = extractFunctionNode(firstArg, checker)
324
331
 
332
+ // Variables to hold zod schema references if provided
333
+ let inputZodSchemaRef: { variableName: string; sourceFile: string } | null =
334
+ null
335
+ let outputZodSchemaRef: { variableName: string; sourceFile: string } | null =
336
+ null
337
+
338
+ // Helper to resolve schema identifier to its actual source file
339
+ const resolveSchemaSourceFile = (
340
+ identifier: ts.Identifier
341
+ ): { variableName: string; sourceFile: string } | null => {
342
+ const symbol = checker.getSymbolAtLocation(identifier)
343
+ if (!symbol) return null
344
+
345
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0]
346
+ if (!decl) return null
347
+
348
+ // If it's an import specifier, resolve the aliased symbol to get the actual source
349
+ if (ts.isImportSpecifier(decl)) {
350
+ const aliasedSymbol = checker.getAliasedSymbol(symbol)
351
+ if (aliasedSymbol) {
352
+ const aliasedDecl =
353
+ aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
354
+ if (aliasedDecl) {
355
+ return {
356
+ variableName: identifier.text,
357
+ sourceFile: aliasedDecl.getSourceFile().fileName,
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+ // Not an import - use the current source file
364
+ return {
365
+ variableName: identifier.text,
366
+ sourceFile: decl.getSourceFile().fileName,
367
+ }
368
+ }
369
+
325
370
  // Extract config properties if using object form
326
371
  if (ts.isObjectLiteralExpression(firstArg)) {
327
372
  objectNode = firstArg
328
373
  const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger)
374
+ title = metadata.title
329
375
  tags = metadata.tags
330
376
  summary = metadata.summary
331
377
  description = metadata.description
332
378
  errors = metadata.errors
333
379
  expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
334
380
  internal = getPropertyValue(firstArg, 'internal') as boolean | undefined
381
+
382
+ // Extract zod schema variable names from input/output properties
383
+ for (const prop of firstArg.properties) {
384
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
385
+ const propName = prop.name.text
386
+ if (propName === 'input' || propName === 'output') {
387
+ if (ts.isIdentifier(prop.initializer)) {
388
+ // Good - it's a variable reference, resolve its actual source file
389
+ const ref = resolveSchemaSourceFile(prop.initializer)
390
+ if (ref) {
391
+ if (propName === 'input') {
392
+ inputZodSchemaRef = ref
393
+ } else {
394
+ outputZodSchemaRef = ref
395
+ }
396
+ }
397
+ } else if (ts.isCallExpression(prop.initializer)) {
398
+ // Bad - it's an inline expression
399
+ const schemaName = `${name.charAt(0).toUpperCase() + name.slice(1)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
400
+ logger.critical(
401
+ ErrorCode.INLINE_ZOD_SCHEMA,
402
+ `Inline Zod schemas are not supported for '${propName}' in '${name}'.\n` +
403
+ ` Extract to an exported variable:\n` +
404
+ ` export const ${schemaName} = ${prop.initializer.getText()}\n` +
405
+ ` Then use: ${propName}: ${schemaName}`
406
+ )
407
+ }
408
+ }
409
+ }
410
+ }
335
411
  }
336
412
 
337
413
  // Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
@@ -404,23 +480,54 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
404
480
  .map((tn) => checker.getTypeFromTypeNode(tn))
405
481
  .map((t) => unwrapPromise(checker, t))
406
482
 
483
+ const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1)
484
+
407
485
  // --- Input Extraction ---
408
- let { names: inputNames, types: inputTypes } = getNamesAndTypes(
409
- checker,
410
- state.functions.typesMap,
411
- 'Input',
412
- name,
413
- genericTypes[0]
414
- )
415
- // if (inputTypes.length === 0) {
416
- // logger.debug(
417
- // `\x1b[31m• Unknown input type for '${name}', assuming void.\x1b[0m`
418
- // )
419
- // }
486
+ let inputNames: string[] = []
487
+ let inputTypes: ts.Type[] = []
488
+
489
+ if (inputZodSchemaRef) {
490
+ const schemaName = `${capitalizedName}Input`
491
+ inputNames = [schemaName]
492
+ state.zodLookup.set(schemaName, inputZodSchemaRef)
493
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
494
+ } else if (genericTypes.length >= 1 && genericTypes[0]) {
495
+ // Fall back to extracting from generic type arguments
496
+ const result = getNamesAndTypes(
497
+ checker,
498
+ state.functions.typesMap,
499
+ 'Input',
500
+ name,
501
+ genericTypes[0]
502
+ )
503
+ inputNames = result.names
504
+ inputTypes = result.types
505
+ } else {
506
+ // Fall back to extracting from the function's second parameter type
507
+ const secondParam = handler.parameters[1]
508
+ if (secondParam) {
509
+ const paramType = checker.getTypeAtLocation(secondParam)
510
+ const result = getNamesAndTypes(
511
+ checker,
512
+ state.functions.typesMap,
513
+ 'Input',
514
+ pikkuFuncName,
515
+ paramType
516
+ )
517
+ inputNames = result.names
518
+ inputTypes = result.types
519
+ }
520
+ }
420
521
 
421
522
  // --- Output Extraction ---
422
523
  let outputNames: string[] = []
423
- if (genericTypes.length >= 2) {
524
+
525
+ if (outputZodSchemaRef) {
526
+ const schemaName = `${capitalizedName}Output`
527
+ outputNames = [schemaName]
528
+ state.zodLookup.set(schemaName, outputZodSchemaRef)
529
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
530
+ } else if (genericTypes.length >= 2) {
424
531
  outputNames = getNamesAndTypes(
425
532
  checker,
426
533
  state.functions.typesMap,
@@ -459,6 +566,11 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
459
566
  ? resolveMiddleware(state, objectNode, tags, checker)
460
567
  : undefined
461
568
 
569
+ // --- resolve permissions ---
570
+ const permissions = objectNode
571
+ ? resolvePermissions(state, objectNode, tags, checker)
572
+ : undefined
573
+
462
574
  state.functions.meta[pikkuFuncName] = {
463
575
  pikkuFuncName,
464
576
  name,
@@ -470,11 +582,13 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
470
582
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
471
583
  expose: expose || undefined,
472
584
  internal: internal || undefined,
585
+ title,
473
586
  tags: tags || undefined,
474
587
  summary,
475
588
  description,
476
589
  errors,
477
590
  middleware,
591
+ permissions,
478
592
  isDirectFunction,
479
593
  }
480
594
 
@@ -70,7 +70,7 @@ export const addHTTPRoute: AddWiring = (
70
70
 
71
71
  const method =
72
72
  (getPropertyValue(obj, 'method') as string)?.toLowerCase() || 'get'
73
- const { tags, summary, description, errors } = getCommonWireMetaData(
73
+ const { title, tags, summary, description, errors } = getCommonWireMetaData(
74
74
  obj,
75
75
  'HTTP route',
76
76
  route,
@@ -78,6 +78,29 @@ export const addHTTPRoute: AddWiring = (
78
78
  )
79
79
  const query = (getPropertyValue(obj, 'query') as string[]) || []
80
80
 
81
+ // Check if this is a workflow trigger (workflow: true)
82
+ const isWorkflowTrigger = getPropertyValue(obj, 'workflow') === true
83
+ if (isWorkflowTrigger) {
84
+ // Workflow triggers don't need func - they're handled by workflow-utils
85
+ // Just record the route for HTTP meta but skip function processing
86
+ state.http.files.add(node.getSourceFile().fileName)
87
+ state.http.meta[method][route] = {
88
+ pikkuFuncName: '', // No function - workflow handles it
89
+ route,
90
+ method: method as HTTPMethod,
91
+ params: params.length > 0 ? params : undefined,
92
+ query: query.length > 0 ? query : undefined,
93
+ inputTypes: undefined,
94
+ title,
95
+ summary,
96
+ description,
97
+ errors,
98
+ tags,
99
+ workflow: true,
100
+ }
101
+ return
102
+ }
103
+
81
104
  // --- find the referenced function name first for filtering ---
82
105
  const funcInitializer = getPropertyAssignmentInitializer(
83
106
  obj,
@@ -158,6 +181,7 @@ export const addHTTPRoute: AddWiring = (
158
181
  params: params.length > 0 ? params : undefined,
159
182
  query: query.length > 0 ? query : undefined,
160
183
  inputTypes,
184
+ title,
161
185
  summary,
162
186
  description,
163
187
  errors,
@@ -1,48 +1,78 @@
1
1
  import * as ts from 'typescript'
2
2
  import { InspectorState, InspectorLogger } from '../types.js'
3
3
 
4
+ /**
5
+ * Helper to extract namespace from a namespaced function reference like 'ext:hello'
6
+ */
7
+ function extractNamespace(functionRef: string): string | null {
8
+ const colonIndex = functionRef.indexOf(':')
9
+ if (colonIndex !== -1) {
10
+ return functionRef.substring(0, colonIndex)
11
+ }
12
+ return null
13
+ }
14
+
4
15
  /**
5
16
  * Scan for rpc.invoke() calls to track which functions are actually being invoked
17
+ * Also detects external package usage via:
18
+ * - Namespaced calls: rpc.invoke('namespace:function')
19
+ * - External helper: external('namespace:function')
6
20
  */
7
21
  export function addRPCInvocations(
8
22
  node: ts.Node,
9
23
  state: InspectorState,
10
24
  logger: InspectorLogger
11
25
  ) {
12
- // Look for property access expressions: rpc.invoke
13
- if (ts.isPropertyAccessExpression(node)) {
14
- const { expression, name } = node
26
+ // Look for call expressions: external('ext:hello') or rpc.invoke('...')
27
+ if (ts.isCallExpression(node)) {
28
+ const { expression, arguments: args } = node
29
+
30
+ // Check for external('namespace:function') calls
31
+ if (ts.isIdentifier(expression) && expression.text === 'external') {
32
+ const [firstArg] = args
33
+ if (firstArg && ts.isStringLiteral(firstArg)) {
34
+ const functionRef = firstArg.text
35
+ logger.debug(`• Found external() call: ${functionRef}`)
36
+ state.rpc.invokedFunctions.add(functionRef)
15
37
 
16
- // Check if this is accessing 'invoke' property
17
- if (name.text === 'invoke') {
18
- // Check if the object is 'rpc' (or a variable containing rpc)
19
- if (ts.isIdentifier(expression) && expression.text === 'rpc') {
20
- // This is rpc.invoke - now we need to find the parent call expression
21
- const parent = node.parent
22
- if (ts.isCallExpression(parent) && parent.expression === node) {
23
- // This is rpc.invoke('function-name')
24
- const [firstArg] = parent.arguments
25
- if (firstArg) {
26
- // Extract the function name from string literal
27
- if (ts.isStringLiteral(firstArg)) {
28
- const functionName = firstArg.text
29
- logger.debug(`• Found RPC invocation: ${functionName}`)
30
- state.rpc.invokedFunctions.add(functionName)
31
- }
32
- // Handle template literals like `function-${name}`
33
- else if (
34
- ts.isTemplateExpression(firstArg) ||
35
- ts.isNoSubstitutionTemplateLiteral(firstArg)
36
- ) {
37
- logger.warn(
38
- `• Found dynamic RPC invocation: ${firstArg.getText()}`
39
- )
40
- logger.warn(
41
- `\tYou can only use string literals for RPC function names, with ' or " and not \``
42
- )
43
- }
38
+ const namespace = extractNamespace(functionRef)
39
+ if (namespace) {
40
+ logger.debug(` → External package detected: ${namespace}`)
41
+ state.rpc.usedExternalPackages.add(namespace)
42
+ }
43
+ }
44
+ }
45
+
46
+ // Check for rpc.invoke('...') calls
47
+ if (
48
+ ts.isPropertyAccessExpression(expression) &&
49
+ expression.name.text === 'invoke' &&
50
+ ts.isIdentifier(expression.expression) &&
51
+ expression.expression.text === 'rpc'
52
+ ) {
53
+ const [firstArg] = args
54
+ if (firstArg) {
55
+ if (ts.isStringLiteral(firstArg)) {
56
+ const functionRef = firstArg.text
57
+ logger.debug(`• Found RPC invocation: ${functionRef}`)
58
+ state.rpc.invokedFunctions.add(functionRef)
59
+
60
+ const namespace = extractNamespace(functionRef)
61
+ if (namespace) {
62
+ logger.debug(` → External package detected: ${namespace}`)
63
+ state.rpc.usedExternalPackages.add(namespace)
44
64
  }
45
65
  }
66
+ // Handle template literals like `function-${name}`
67
+ else if (
68
+ ts.isTemplateExpression(firstArg) ||
69
+ ts.isNoSubstitutionTemplateLiteral(firstArg)
70
+ ) {
71
+ logger.warn(`• Found dynamic RPC invocation: ${firstArg.getText()}`)
72
+ logger.warn(
73
+ `\tYou can only use string literals for RPC function names, with ' or " and not \``
74
+ )
75
+ }
46
76
  }
47
77
  }
48
78
  }