@outputai/core 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.
Files changed (114) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/bin/healthcheck.mjs +36 -0
  4. package/bin/healthcheck.spec.js +90 -0
  5. package/bin/worker.sh +26 -0
  6. package/package.json +67 -0
  7. package/src/activity_integration/context.d.ts +27 -0
  8. package/src/activity_integration/context.js +17 -0
  9. package/src/activity_integration/context.spec.js +42 -0
  10. package/src/activity_integration/events.d.ts +7 -0
  11. package/src/activity_integration/events.js +10 -0
  12. package/src/activity_integration/index.d.ts +9 -0
  13. package/src/activity_integration/index.js +3 -0
  14. package/src/activity_integration/tracing.d.ts +32 -0
  15. package/src/activity_integration/tracing.js +37 -0
  16. package/src/async_storage.js +19 -0
  17. package/src/bus.js +3 -0
  18. package/src/consts.js +32 -0
  19. package/src/errors.d.ts +15 -0
  20. package/src/errors.js +14 -0
  21. package/src/hooks/index.d.ts +28 -0
  22. package/src/hooks/index.js +32 -0
  23. package/src/index.d.ts +49 -0
  24. package/src/index.js +4 -0
  25. package/src/interface/evaluation_result.d.ts +173 -0
  26. package/src/interface/evaluation_result.js +215 -0
  27. package/src/interface/evaluator.d.ts +70 -0
  28. package/src/interface/evaluator.js +34 -0
  29. package/src/interface/evaluator.spec.js +565 -0
  30. package/src/interface/index.d.ts +9 -0
  31. package/src/interface/index.js +26 -0
  32. package/src/interface/step.d.ts +138 -0
  33. package/src/interface/step.js +22 -0
  34. package/src/interface/types.d.ts +27 -0
  35. package/src/interface/validations/runtime.js +20 -0
  36. package/src/interface/validations/runtime.spec.js +29 -0
  37. package/src/interface/validations/schema_utils.js +8 -0
  38. package/src/interface/validations/schema_utils.spec.js +67 -0
  39. package/src/interface/validations/static.js +136 -0
  40. package/src/interface/validations/static.spec.js +366 -0
  41. package/src/interface/webhook.d.ts +84 -0
  42. package/src/interface/webhook.js +64 -0
  43. package/src/interface/webhook.spec.js +122 -0
  44. package/src/interface/workflow.d.ts +273 -0
  45. package/src/interface/workflow.js +128 -0
  46. package/src/interface/workflow.spec.js +467 -0
  47. package/src/interface/workflow_context.js +31 -0
  48. package/src/interface/workflow_utils.d.ts +76 -0
  49. package/src/interface/workflow_utils.js +50 -0
  50. package/src/interface/workflow_utils.spec.js +190 -0
  51. package/src/interface/zod_integration.spec.js +646 -0
  52. package/src/internal_activities/index.js +66 -0
  53. package/src/internal_activities/index.spec.js +102 -0
  54. package/src/logger.js +73 -0
  55. package/src/tracing/internal_interface.js +71 -0
  56. package/src/tracing/processors/local/index.js +111 -0
  57. package/src/tracing/processors/local/index.spec.js +149 -0
  58. package/src/tracing/processors/s3/configs.js +31 -0
  59. package/src/tracing/processors/s3/configs.spec.js +64 -0
  60. package/src/tracing/processors/s3/index.js +114 -0
  61. package/src/tracing/processors/s3/index.spec.js +153 -0
  62. package/src/tracing/processors/s3/redis_client.js +62 -0
  63. package/src/tracing/processors/s3/redis_client.spec.js +185 -0
  64. package/src/tracing/processors/s3/s3_client.js +27 -0
  65. package/src/tracing/processors/s3/s3_client.spec.js +62 -0
  66. package/src/tracing/tools/build_trace_tree.js +83 -0
  67. package/src/tracing/tools/build_trace_tree.spec.js +135 -0
  68. package/src/tracing/tools/utils.js +21 -0
  69. package/src/tracing/tools/utils.spec.js +14 -0
  70. package/src/tracing/trace_engine.js +97 -0
  71. package/src/tracing/trace_engine.spec.js +199 -0
  72. package/src/utils/index.d.ts +134 -0
  73. package/src/utils/index.js +2 -0
  74. package/src/utils/resolve_invocation_dir.js +34 -0
  75. package/src/utils/resolve_invocation_dir.spec.js +102 -0
  76. package/src/utils/utils.js +211 -0
  77. package/src/utils/utils.spec.js +448 -0
  78. package/src/worker/bundler_options.js +43 -0
  79. package/src/worker/catalog_workflow/catalog.js +114 -0
  80. package/src/worker/catalog_workflow/index.js +54 -0
  81. package/src/worker/catalog_workflow/index.spec.js +196 -0
  82. package/src/worker/catalog_workflow/workflow.js +24 -0
  83. package/src/worker/configs.js +49 -0
  84. package/src/worker/configs.spec.js +130 -0
  85. package/src/worker/index.js +89 -0
  86. package/src/worker/index.spec.js +177 -0
  87. package/src/worker/interceptors/activity.js +62 -0
  88. package/src/worker/interceptors/activity.spec.js +212 -0
  89. package/src/worker/interceptors/workflow.js +70 -0
  90. package/src/worker/interceptors/workflow.spec.js +167 -0
  91. package/src/worker/interceptors.js +10 -0
  92. package/src/worker/loader.js +151 -0
  93. package/src/worker/loader.spec.js +236 -0
  94. package/src/worker/loader_tools.js +132 -0
  95. package/src/worker/loader_tools.spec.js +156 -0
  96. package/src/worker/log_hooks.js +95 -0
  97. package/src/worker/log_hooks.spec.js +217 -0
  98. package/src/worker/sandboxed_utils.js +18 -0
  99. package/src/worker/shutdown.js +26 -0
  100. package/src/worker/shutdown.spec.js +82 -0
  101. package/src/worker/sinks.js +74 -0
  102. package/src/worker/start_catalog.js +36 -0
  103. package/src/worker/start_catalog.spec.js +118 -0
  104. package/src/worker/webpack_loaders/consts.js +9 -0
  105. package/src/worker/webpack_loaders/tools.js +548 -0
  106. package/src/worker/webpack_loaders/tools.spec.js +330 -0
  107. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
  108. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
  109. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
  110. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
  111. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
  112. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
  113. package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
  114. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +613 -0
@@ -0,0 +1,330 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, resolve as resolvePath } from 'node:path';
5
+ import * as t from '@babel/types';
6
+ import {
7
+ toAbsolutePath,
8
+ parse,
9
+ extractTopLevelStringConsts,
10
+ getObjectKeyName,
11
+ getLocalNameFromDestructuredProperty,
12
+ toFunctionExpression,
13
+ isStepsPath,
14
+ isSharedStepsPath,
15
+ isAnyStepsPath,
16
+ isEvaluatorsPath,
17
+ isSharedEvaluatorsPath,
18
+ isWorkflowPath,
19
+ createThisMethodCall,
20
+ resolveNameFromArg,
21
+ resolveNameFromOptions,
22
+ buildStepsNameMap,
23
+ buildSharedStepsNameMap,
24
+ buildWorkflowNameMap,
25
+ buildEvaluatorsNameMap,
26
+ getFileKind
27
+ } from './tools.js';
28
+
29
+ describe( 'workflow_rewriter tools', () => {
30
+ it( 'parse: parses JS with JSX plugin enabled', () => {
31
+ const ast = parse( 'const A = 1; const C = () => <div />', 'file.js' );
32
+ expect( ast?.type ).toBe( 'File' );
33
+ expect( ast.program.body.length ).toBeGreaterThan( 0 );
34
+ } );
35
+
36
+ it( 'toAbsolutePath: resolves relative path against base directory', () => {
37
+ expect( toAbsolutePath( '/base/dir', './file.js' ) ).toBe( resolvePath( '/base/dir', './file.js' ) );
38
+ } );
39
+
40
+ it( 'extractTopLevelStringConsts: returns only const string bindings', () => {
41
+ const ast = parse( [
42
+ 'const A = \"a\"; let B = \"b\"; const C = 3;',
43
+ 'const D = `d`; const E = \"e\"'
44
+ ].join( '\n' ), 'file.js' );
45
+ const map = extractTopLevelStringConsts( ast );
46
+ expect( map.get( 'A' ) ).toBe( 'a' );
47
+ expect( map.has( 'B' ) ).toBe( false );
48
+ expect( map.has( 'C' ) ).toBe( false );
49
+ // Template literal is not a StringLiteral
50
+ expect( map.has( 'D' ) ).toBe( false );
51
+ expect( map.get( 'E' ) ).toBe( 'e' );
52
+ } );
53
+
54
+ it( 'resolveNameFromOptions: returns literal name from options object', () => {
55
+ const opts = t.objectExpression( [ t.objectProperty( t.identifier( 'name' ), t.stringLiteral( 'literal.name' ) ) ] );
56
+ const out = resolveNameFromOptions( opts, new Map(), 'X' );
57
+ expect( out ).toBe( 'literal.name' );
58
+ } );
59
+
60
+ it( 'getObjectKeyName: resolves from Identifier and StringLiteral', () => {
61
+ expect( getObjectKeyName( t.identifier( 'name' ) ) ).toBe( 'name' );
62
+ expect( getObjectKeyName( t.stringLiteral( 'x' ) ) ).toBe( 'x' );
63
+ expect( getObjectKeyName( t.numericLiteral( 1 ) ) ).toBeNull();
64
+ } );
65
+
66
+ it( 'buildStepsNameMap: reads names from steps module and caches result', () => {
67
+ const dir = mkdtempSync( join( tmpdir(), 'tools-steps-' ) );
68
+ const stepsPath = join( dir, 'steps.js' );
69
+ writeFileSync( stepsPath, [
70
+ 'export const StepA = step({ name: "step.a" })',
71
+ 'export const StepB = step({ name: "step.b" })'
72
+ ].join( '\n' ) );
73
+ const cache = new Map();
74
+ const map1 = buildStepsNameMap( stepsPath, cache );
75
+ expect( map1.get( 'StepA' ) ).toBe( 'step.a' );
76
+ expect( map1.get( 'StepB' ) ).toBe( 'step.b' );
77
+ expect( cache.get( stepsPath ) ).toBe( map1 );
78
+ const map2 = buildStepsNameMap( stepsPath, cache );
79
+ expect( map2 ).toBe( map1 );
80
+ rmSync( dir, { recursive: true, force: true } );
81
+ } );
82
+
83
+ it( 'buildEvaluatorsNameMap: reads names from evaluators module and caches result', () => {
84
+ const dir = mkdtempSync( join( tmpdir(), 'tools-evals-' ) );
85
+ const evalsPath = join( dir, 'evaluators.js' );
86
+ writeFileSync( evalsPath, [
87
+ 'export const EvalA = evaluator({ name: "eval.a" })',
88
+ 'export const EvalB = evaluator({ name: "eval.b" })'
89
+ ].join( '\n' ) );
90
+ const cache = new Map();
91
+ const map1 = buildEvaluatorsNameMap( evalsPath, cache );
92
+ expect( map1.get( 'EvalA' ) ).toBe( 'eval.a' );
93
+ expect( map1.get( 'EvalB' ) ).toBe( 'eval.b' );
94
+ expect( cache.get( evalsPath ) ).toBe( map1 );
95
+ const map2 = buildEvaluatorsNameMap( evalsPath, cache );
96
+ expect( map2 ).toBe( map1 );
97
+ rmSync( dir, { recursive: true, force: true } );
98
+ } );
99
+
100
+ it( 'getLocalNameFromDestructuredProperty: handles { a }, { a: b }, { a: b = 1 }', () => {
101
+ // { a }
102
+ const p1 = t.objectProperty( t.identifier( 'a' ), t.identifier( 'a' ), false, true );
103
+ expect( getLocalNameFromDestructuredProperty( p1 ) ).toBe( 'a' );
104
+ // { a: b }
105
+ const p2 = t.objectProperty( t.identifier( 'a' ), t.identifier( 'b' ) );
106
+ expect( getLocalNameFromDestructuredProperty( p2 ) ).toBe( 'b' );
107
+ // { a: b = 1 }
108
+ const p3 = t.objectProperty( t.identifier( 'a' ), t.assignmentPattern( t.identifier( 'b' ), t.numericLiteral( 1 ) ) );
109
+ expect( getLocalNameFromDestructuredProperty( p3 ) ).toBe( 'b' );
110
+ // Unsupported shape
111
+ const p4 = t.objectProperty( t.identifier( 'a' ), t.arrayExpression( [] ) );
112
+ expect( getLocalNameFromDestructuredProperty( p4 ) ).toBeNull();
113
+ } );
114
+
115
+ it( 'buildWorkflowNameMap: reads named and default workflow names and caches', () => {
116
+ const dir = mkdtempSync( join( tmpdir(), 'tools-output-' ) );
117
+ const wfPath = join( dir, 'workflow.js' );
118
+ writeFileSync( wfPath, [
119
+ 'export const FlowA = workflow({ name: "flow.a" })',
120
+ 'export default workflow({ name: "flow.def" })'
121
+ ].join( '\n' ) );
122
+ const cache = new Map();
123
+ const res1 = buildWorkflowNameMap( wfPath, cache );
124
+ expect( res1.named.get( 'FlowA' ) ).toBe( 'flow.a' );
125
+ expect( res1.default ).toBe( 'flow.def' );
126
+ expect( cache.get( wfPath ) ).toBe( res1 );
127
+ const res2 = buildWorkflowNameMap( wfPath, cache );
128
+ expect( res2 ).toBe( res1 );
129
+ rmSync( dir, { recursive: true, force: true } );
130
+ } );
131
+
132
+ it( 'toFunctionExpression: converts arrow, wraps expression bodies', () => {
133
+ const arrowExprBody = t.arrowFunctionExpression( [ t.identifier( 'x' ) ], t.identifier( 'x' ) );
134
+ const arrowBlockBody = t.arrowFunctionExpression( [], t.blockStatement( [ t.returnStatement( t.numericLiteral( 1 ) ) ] ) );
135
+ const fn1 = toFunctionExpression( arrowExprBody );
136
+ const fn2 = toFunctionExpression( arrowBlockBody );
137
+ expect( t.isFunctionExpression( fn1 ) ).toBe( true );
138
+ expect( t.isBlockStatement( fn1.body ) ).toBe( true );
139
+ expect( t.isReturnStatement( fn1.body.body[0] ) ).toBe( true );
140
+ expect( t.isFunctionExpression( fn2 ) ).toBe( true );
141
+ } );
142
+
143
+ it( 'isStepsPath: matches LOCAL steps.js (no path traversal)', () => {
144
+ // Local steps (without ../ or /shared/)
145
+ expect( isStepsPath( 'steps.js' ) ).toBe( true );
146
+ expect( isStepsPath( './steps.js' ) ).toBe( true );
147
+ expect( isStepsPath( '/a/b/steps.js' ) ).toBe( true );
148
+ expect( isStepsPath( './steps/fetch.js' ) ).toBe( true );
149
+ // Shared steps (with ../ or /shared/) should NOT match isStepsPath
150
+ expect( isStepsPath( '../steps.js' ) ).toBe( false );
151
+ expect( isStepsPath( '../../shared/steps/common.js' ) ).toBe( false );
152
+ // Non-steps
153
+ expect( isStepsPath( 'steps.ts' ) ).toBe( false );
154
+ expect( isStepsPath( 'workflow.js' ) ).toBe( false );
155
+ } );
156
+
157
+ it( 'isSharedStepsPath: matches steps imported from outside workflow directory', () => {
158
+ // Shared steps: must have steps pattern AND have path traversal or /shared/
159
+ expect( isSharedStepsPath( '../steps.js' ) ).toBe( true );
160
+ expect( isSharedStepsPath( '../../steps.js' ) ).toBe( true );
161
+ expect( isSharedStepsPath( '../../shared/steps/common.js' ) ).toBe( true );
162
+ expect( isSharedStepsPath( '../other_workflow/steps.js' ) ).toBe( true );
163
+ expect( isSharedStepsPath( '/src/shared/steps/common.js' ) ).toBe( true );
164
+ // Local steps (no traversal, no /shared/) should NOT match
165
+ expect( isSharedStepsPath( './steps.js' ) ).toBe( false );
166
+ expect( isSharedStepsPath( 'steps.js' ) ).toBe( false );
167
+ expect( isSharedStepsPath( './steps/fetch.js' ) ).toBe( false );
168
+ // Non-steps should NOT match
169
+ expect( isSharedStepsPath( '../utils.js' ) ).toBe( false );
170
+ expect( isSharedStepsPath( 'evaluators.js' ) ).toBe( false );
171
+ } );
172
+
173
+ it( 'isAnyStepsPath: matches any steps pattern (local or shared)', () => {
174
+ // Local steps
175
+ expect( isAnyStepsPath( 'steps.js' ) ).toBe( true );
176
+ expect( isAnyStepsPath( './steps.js' ) ).toBe( true );
177
+ expect( isAnyStepsPath( './steps/fetch.js' ) ).toBe( true );
178
+ // Shared steps
179
+ expect( isAnyStepsPath( '../steps.js' ) ).toBe( true );
180
+ expect( isAnyStepsPath( '../../shared/steps/common.js' ) ).toBe( true );
181
+ // Non-steps
182
+ expect( isAnyStepsPath( 'workflow.js' ) ).toBe( false );
183
+ expect( isAnyStepsPath( 'utils.js' ) ).toBe( false );
184
+ } );
185
+
186
+ it( 'isWorkflowPath: matches workflow.js at root or subpath', () => {
187
+ expect( isWorkflowPath( 'workflow.js' ) ).toBe( true );
188
+ expect( isWorkflowPath( './workflow.js' ) ).toBe( true );
189
+ expect( isWorkflowPath( '/a/b/workflow.js' ) ).toBe( true );
190
+ expect( isWorkflowPath( 'workflow.ts' ) ).toBe( false );
191
+ expect( isWorkflowPath( 'steps.js' ) ).toBe( false );
192
+ } );
193
+
194
+ it( 'isEvaluatorsPath: matches local evaluators.js but excludes shared', () => {
195
+ expect( isEvaluatorsPath( 'evaluators.js' ) ).toBe( true );
196
+ expect( isEvaluatorsPath( './evaluators.js' ) ).toBe( true );
197
+ expect( isEvaluatorsPath( '/a/b/evaluators.js' ) ).toBe( true );
198
+ expect( isEvaluatorsPath( './evaluators/quality.js' ) ).toBe( true );
199
+ // Shared evaluators should NOT match (path traversal or /shared/)
200
+ expect( isEvaluatorsPath( '../evaluators.js' ) ).toBe( false );
201
+ expect( isEvaluatorsPath( '../../shared/evaluators/common.js' ) ).toBe( false );
202
+ expect( isEvaluatorsPath( 'evaluators.ts' ) ).toBe( false );
203
+ expect( isEvaluatorsPath( 'steps.js' ) ).toBe( false );
204
+ } );
205
+
206
+ it( 'isSharedEvaluatorsPath: matches evaluators imported from outside workflow directory', () => {
207
+ // Shared evaluators: must have evaluators pattern AND have path traversal or /shared/
208
+ expect( isSharedEvaluatorsPath( '../evaluators.js' ) ).toBe( true );
209
+ expect( isSharedEvaluatorsPath( '../../evaluators.js' ) ).toBe( true );
210
+ expect( isSharedEvaluatorsPath( '../../shared/evaluators/quality.js' ) ).toBe( true );
211
+ expect( isSharedEvaluatorsPath( '../other_workflow/evaluators.js' ) ).toBe( true );
212
+ expect( isSharedEvaluatorsPath( '/src/shared/evaluators/quality.js' ) ).toBe( true );
213
+ // Local evaluators (no traversal, no /shared/) should NOT match
214
+ expect( isSharedEvaluatorsPath( './evaluators.js' ) ).toBe( false );
215
+ expect( isSharedEvaluatorsPath( 'evaluators.js' ) ).toBe( false );
216
+ expect( isSharedEvaluatorsPath( './evaluators/quality.js' ) ).toBe( false );
217
+ // Non-evaluators should NOT match
218
+ expect( isSharedEvaluatorsPath( '../utils.js' ) ).toBe( false );
219
+ expect( isSharedEvaluatorsPath( 'steps.js' ) ).toBe( false );
220
+ } );
221
+
222
+ it( 'createThisMethodCall: builds this.method(\'name\', ...args) call', () => {
223
+ const call = createThisMethodCall( 'invoke', 'n', [ t.numericLiteral( 1 ), t.identifier( 'x' ) ] );
224
+ expect( t.isCallExpression( call ) ).toBe( true );
225
+ expect( t.isMemberExpression( call.callee ) ).toBe( true );
226
+ expect( t.isThisExpression( call.callee.object ) ).toBe( true );
227
+ expect( t.isIdentifier( call.callee.property, { name: 'invoke' } ) ).toBe( true );
228
+ expect( t.isStringLiteral( call.arguments[0], { value: 'n' } ) ).toBe( true );
229
+ expect( call.arguments.length ).toBe( 3 );
230
+ } );
231
+
232
+ it( 'buildSharedStepsNameMap: reads names from shared steps module and caches result', () => {
233
+ const dir = mkdtempSync( join( tmpdir(), 'tools-shared-steps-' ) );
234
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
235
+ const stepsPath = join( dir, 'shared', 'steps', 'common.js' );
236
+ writeFileSync( stepsPath, [
237
+ 'export const StepA = step({ name: "shared.step.a" })',
238
+ 'export const StepB = step({ name: "shared.step.b" })'
239
+ ].join( '\n' ) );
240
+ const cache = new Map();
241
+ const map1 = buildSharedStepsNameMap( stepsPath, cache );
242
+ expect( map1.get( 'StepA' ) ).toBe( 'shared.step.a' );
243
+ expect( map1.get( 'StepB' ) ).toBe( 'shared.step.b' );
244
+ expect( cache.get( stepsPath ) ).toBe( map1 );
245
+ const map2 = buildSharedStepsNameMap( stepsPath, cache );
246
+ expect( map2 ).toBe( map1 );
247
+ rmSync( dir, { recursive: true, force: true } );
248
+ } );
249
+
250
+ it( 'getFileKind: classifies file by its path', () => {
251
+ expect( getFileKind( '/p/workflow.js' ) ).toBe( 'workflow' );
252
+ expect( getFileKind( '/p/steps.js' ) ).toBe( 'steps' );
253
+ // Files in steps folder are steps
254
+ expect( getFileKind( '/p/steps/fetch.js' ) ).toBe( 'steps' );
255
+ expect( getFileKind( '/p/shared/steps/common.js' ) ).toBe( 'steps' );
256
+ expect( getFileKind( '/p/evaluators.js' ) ).toBe( 'evaluators' );
257
+ expect( getFileKind( '/p/evaluators/quality.js' ) ).toBe( 'evaluators' );
258
+ expect( getFileKind( '/p/other.js' ) ).toBe( null );
259
+ expect( getFileKind( '/p/utils.js' ) ).toBe( null );
260
+ expect( getFileKind( '/p/clients/api.js' ) ).toBe( null );
261
+ } );
262
+
263
+ it( 'resolveNameFromArg: resolves string literal directly', () => {
264
+ expect( resolveNameFromArg( t.stringLiteral( 'my_name' ), new Map(), 'X' ) ).toBe( 'my_name' );
265
+ } );
266
+
267
+ it( 'resolveNameFromArg: resolves identifier from consts', () => {
268
+ const consts = new Map( [ [ 'MY_NAME', 'resolved_name' ] ] );
269
+ expect( resolveNameFromArg( t.identifier( 'MY_NAME' ), consts, 'X' ) ).toBe( 'resolved_name' );
270
+ } );
271
+
272
+ it( 'resolveNameFromArg: falls back to resolveNameFromOptions for objects', () => {
273
+ const opts = t.objectExpression( [ t.objectProperty( t.identifier( 'name' ), t.stringLiteral( 'obj_name' ) ) ] );
274
+ expect( resolveNameFromArg( opts, new Map(), 'X' ) ).toBe( 'obj_name' );
275
+ } );
276
+
277
+ it( 'buildEvaluatorsNameMap: reads names from string-arg factory pattern', () => {
278
+ const dir = mkdtempSync( join( tmpdir(), 'tools-verify-evals-' ) );
279
+ const evalsPath = join( dir, 'evaluators.js' );
280
+ writeFileSync( evalsPath, 'export const EvalA = verify( \'eval_a\', async () => {} )' );
281
+ const cache = new Map();
282
+ const map = buildEvaluatorsNameMap( evalsPath, cache );
283
+ expect( map.get( 'EvalA' ) ).toBe( 'eval_a' );
284
+ rmSync( dir, { recursive: true, force: true } );
285
+ } );
286
+
287
+ it( 'buildEvaluatorsNameMap: reads names from object-arg verify pattern', () => {
288
+ const dir = mkdtempSync( join( tmpdir(), 'tools-verify-obj-evals-' ) );
289
+ const evalsPath = join( dir, 'evaluators.js' );
290
+ writeFileSync( evalsPath, 'export const EvalA = verify( { name: \'eval_a\' }, async () => {} )' );
291
+ const cache = new Map();
292
+ const map = buildEvaluatorsNameMap( evalsPath, cache );
293
+ expect( map.get( 'EvalA' ) ).toBe( 'eval_a' );
294
+ rmSync( dir, { recursive: true, force: true } );
295
+ } );
296
+
297
+ it( 'buildEvaluatorsNameMap: reads names from mixed factory patterns', () => {
298
+ const dir = mkdtempSync( join( tmpdir(), 'tools-mixed-evals-' ) );
299
+ const evalsPath = join( dir, 'evaluators.js' );
300
+ writeFileSync( evalsPath, [
301
+ 'export const EvalA = verify( { name: \'eval_a\' }, async () => {} )',
302
+ 'export const EvalB = evaluator( { name: \'eval_b\' } )'
303
+ ].join( '\n' ) );
304
+ const cache = new Map();
305
+ const map = buildEvaluatorsNameMap( evalsPath, cache );
306
+ expect( map.get( 'EvalA' ) ).toBe( 'eval_a' );
307
+ expect( map.get( 'EvalB' ) ).toBe( 'eval_b' );
308
+ rmSync( dir, { recursive: true, force: true } );
309
+ } );
310
+
311
+ it( 'buildStepsNameMap: reads names from string-arg factory pattern', () => {
312
+ const dir = mkdtempSync( join( tmpdir(), 'tools-verify-steps-' ) );
313
+ const stepsPath = join( dir, 'steps.js' );
314
+ writeFileSync( stepsPath, 'export const StepA = myStepHelper( \'step_a\', async () => {} )' );
315
+ const cache = new Map();
316
+ const map = buildStepsNameMap( stepsPath, cache );
317
+ expect( map.get( 'StepA' ) ).toBe( 'step_a' );
318
+ rmSync( dir, { recursive: true, force: true } );
319
+ } );
320
+
321
+ it( 'buildWorkflowNameMap: reads names from string-arg factory pattern', () => {
322
+ const dir = mkdtempSync( join( tmpdir(), 'tools-verify-workflow-' ) );
323
+ const wfPath = join( dir, 'workflow.js' );
324
+ writeFileSync( wfPath, 'export default myWorkflowHelper( { name: \'my_flow\' } )' );
325
+ const cache = new Map();
326
+ const res = buildWorkflowNameMap( wfPath, cache );
327
+ expect( res.default ).toBe( 'my_flow' );
328
+ rmSync( dir, { recursive: true, force: true } );
329
+ } );
330
+ } );
@@ -0,0 +1,221 @@
1
+ import traverseModule from '@babel/traverse';
2
+ import {
3
+ buildWorkflowNameMap,
4
+ getLocalNameFromDestructuredProperty,
5
+ isEvaluatorsPath,
6
+ isSharedEvaluatorsPath,
7
+ isSharedStepsPath,
8
+ isStepsPath,
9
+ isWorkflowPath,
10
+ buildStepsNameMap,
11
+ buildSharedStepsNameMap,
12
+ buildEvaluatorsNameMap,
13
+ buildSharedEvaluatorsNameMap,
14
+ toAbsolutePath
15
+ } from '../tools.js';
16
+ import {
17
+ isCallExpression,
18
+ isIdentifier,
19
+ isImportDefaultSpecifier,
20
+ isImportSpecifier,
21
+ isObjectPattern,
22
+ isObjectProperty,
23
+ isStringLiteral,
24
+ isVariableDeclaration
25
+ } from '@babel/types';
26
+
27
+ // Handle CJS/ESM interop for Babel packages when executed as a webpack loader
28
+ const traverse = traverseModule.default ?? traverseModule;
29
+
30
+ const unresolvedImportError = ( name, fileLabel, filePath ) =>
31
+ new Error(
32
+ `Unresolved import '${name}' from ${fileLabel} file '${filePath}'. ` +
33
+ 'This export may have been defined with the wrong component type. ' +
34
+ 'Use the matching factory function for the file ' +
35
+ '(e.g. step() in steps files, evaluator() in evaluators files, workflow() in workflow files).'
36
+ );
37
+
38
+ const removeRequireDeclarator = path => {
39
+ if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
40
+ path.parentPath.remove();
41
+ } else {
42
+ path.remove();
43
+ }
44
+ };
45
+
46
+ const collectDestructuredRequires = ( path, absolutePath, req, descriptors ) => {
47
+ const propFilter = p => isObjectProperty( p ) && isIdentifier( p.key );
48
+ for ( const { match, buildMap, cache, target, valueKey, label } of descriptors ) {
49
+ if ( !match( req ) ) {
50
+ continue;
51
+ }
52
+ const nameMap = buildMap( absolutePath, cache );
53
+ for ( const prop of path.node.id.properties.filter( propFilter ) ) {
54
+ const importedName = prop.key.name;
55
+ const localName = getLocalNameFromDestructuredProperty( prop );
56
+ if ( localName ) {
57
+ const resolved = nameMap.get( importedName );
58
+ if ( resolved ) {
59
+ target.push( { localName, [valueKey]: resolved } );
60
+ } else {
61
+ throw unresolvedImportError( importedName, label, absolutePath );
62
+ }
63
+ }
64
+ }
65
+ removeRequireDeclarator( path );
66
+ return;
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Collect and strip target imports and requires from an AST, producing
72
+ * step/workflow import mappings for later rewrites.
73
+ *
74
+ * Mutates the AST by removing matching import declarations and require declarators.
75
+ *
76
+ * @param {import('@babel/types').File} ast - Parsed file AST.
77
+ * @param {string} fileDir - Absolute directory of the file represented by `ast`.
78
+ * @param {{ stepsNameCache: Map<string,Map<string,string>>, workflowNameCache: Map<string,{default:(string|null),named:Map<string,string>}> }} caches
79
+ * Resolved-name caches to avoid re-reading same modules.
80
+ * @returns {{ stepImports: Array<{localName:string,stepName:string}>,
81
+ * flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
82
+ */
83
+ export default function collectTargetImports(
84
+ ast, fileDir,
85
+ { stepsNameCache, workflowNameCache, evaluatorsNameCache, sharedStepsNameCache, sharedEvaluatorsNameCache }
86
+ ) {
87
+ const stepImports = [];
88
+ const sharedStepImports = [];
89
+ const flowImports = [];
90
+ const evaluatorImports = [];
91
+ const sharedEvaluatorImports = [];
92
+
93
+ traverse( ast, {
94
+ ImportDeclaration: path => {
95
+ const src = path.node.source.value;
96
+ // Ignore other imports
97
+ const isTargetImport = isStepsPath( src ) || isSharedStepsPath( src ) ||
98
+ isWorkflowPath( src ) || isEvaluatorsPath( src ) || isSharedEvaluatorsPath( src );
99
+ if ( !isTargetImport ) {
100
+ return;
101
+ }
102
+
103
+ const absolutePath = toAbsolutePath( fileDir, src );
104
+ const collectNamedImports = ( match, buildMapFn, cache, targetArr, valueKey, fileLabel ) => {
105
+ if ( !match ) {
106
+ return;
107
+ }
108
+ const nameMap = buildMapFn( absolutePath, cache );
109
+ for ( const s of path.node.specifiers.filter( s => isImportSpecifier( s ) ) ) {
110
+ const importedName = s.imported.name;
111
+ const localName = s.local.name;
112
+ const value = nameMap.get( importedName );
113
+ if ( value ) {
114
+ const entry = { localName };
115
+ entry[valueKey] = value;
116
+ targetArr.push( entry );
117
+ } else {
118
+ throw unresolvedImportError( importedName, fileLabel, absolutePath );
119
+ }
120
+ }
121
+ };
122
+
123
+ collectNamedImports( isStepsPath( src ), buildStepsNameMap, stepsNameCache, stepImports, 'stepName', 'steps' );
124
+ collectNamedImports( isSharedStepsPath( src ), buildSharedStepsNameMap, sharedStepsNameCache, sharedStepImports, 'stepName', 'shared steps' );
125
+ collectNamedImports( isEvaluatorsPath( src ), buildEvaluatorsNameMap, evaluatorsNameCache, evaluatorImports, 'evaluatorName', 'evaluators' );
126
+ collectNamedImports(
127
+ isSharedEvaluatorsPath( src ), buildSharedEvaluatorsNameMap,
128
+ sharedEvaluatorsNameCache, sharedEvaluatorImports, 'evaluatorName', 'shared evaluators'
129
+ );
130
+ if ( isWorkflowPath( src ) ) {
131
+ const { named, default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
132
+ for ( const s of path.node.specifiers ) {
133
+ if ( isImportDefaultSpecifier( s ) ) {
134
+ const localName = s.local.name;
135
+ flowImports.push( { localName, workflowName: defName ?? localName } );
136
+ } else if ( isImportSpecifier( s ) ) {
137
+ const importedName = s.imported.name;
138
+ const localName = s.local.name;
139
+ const workflowName = named.get( importedName );
140
+ if ( workflowName ) {
141
+ flowImports.push( { localName, workflowName } );
142
+ } else {
143
+ throw unresolvedImportError( importedName, 'workflow', absolutePath );
144
+ }
145
+ }
146
+ }
147
+ }
148
+ path.remove();
149
+ },
150
+ VariableDeclarator: path => {
151
+ const init = path.node.init;
152
+ if ( !isCallExpression( init ) ) {
153
+ return;
154
+ }
155
+ if ( !isIdentifier( init.callee, { name: 'require' } ) ) {
156
+ return;
157
+ }
158
+ const firstArgument = init.arguments[0];
159
+ if ( !isStringLiteral( firstArgument ) ) {
160
+ return;
161
+ }
162
+
163
+ const req = firstArgument.value;
164
+ const isTargetRequire = isStepsPath( req ) || isSharedStepsPath( req ) ||
165
+ isWorkflowPath( req ) || isEvaluatorsPath( req ) || isSharedEvaluatorsPath( req );
166
+ if ( !isTargetRequire ) {
167
+ return;
168
+ }
169
+
170
+ const absolutePath = toAbsolutePath( fileDir, req );
171
+
172
+ // Destructured requires: const { X } = require('./steps.js')
173
+ if ( isObjectPattern( path.node.id ) ) {
174
+ const cjsDescriptors = [
175
+ {
176
+ match: isStepsPath, buildMap: buildStepsNameMap,
177
+ cache: stepsNameCache, target: stepImports,
178
+ valueKey: 'stepName', label: 'steps'
179
+ },
180
+ {
181
+ match: isSharedStepsPath, buildMap: buildSharedStepsNameMap,
182
+ cache: sharedStepsNameCache ?? stepsNameCache,
183
+ target: sharedStepImports,
184
+ valueKey: 'stepName', label: 'shared steps'
185
+ },
186
+ {
187
+ match: isEvaluatorsPath, buildMap: buildEvaluatorsNameMap,
188
+ cache: evaluatorsNameCache, target: evaluatorImports,
189
+ valueKey: 'evaluatorName', label: 'evaluators'
190
+ },
191
+ {
192
+ match: isSharedEvaluatorsPath, buildMap: buildSharedEvaluatorsNameMap,
193
+ cache: sharedEvaluatorsNameCache ?? evaluatorsNameCache,
194
+ target: sharedEvaluatorImports,
195
+ valueKey: 'evaluatorName', label: 'shared evaluators'
196
+ },
197
+ {
198
+ match: isWorkflowPath,
199
+ buildMap: ( p, c ) => buildWorkflowNameMap( p, c ).named,
200
+ cache: workflowNameCache, target: flowImports,
201
+ valueKey: 'workflowName', label: 'workflow'
202
+ }
203
+ ];
204
+ collectDestructuredRequires(
205
+ path, absolutePath, req, cjsDescriptors
206
+ );
207
+ return;
208
+ }
209
+
210
+ // Default workflow require: const WF = require('./workflow.js')
211
+ if ( isWorkflowPath( req ) && isIdentifier( path.node.id ) ) {
212
+ const { default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
213
+ const localName = path.node.id.name;
214
+ flowImports.push( { localName, workflowName: defName ?? localName } );
215
+ removeRequireDeclarator( path );
216
+ }
217
+ }
218
+ } );
219
+
220
+ return { stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports };
221
+ };