@outputai/core 0.10.1-next.b7b2fbe.0 → 0.10.1-next.bf95a6f.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 (99) hide show
  1. package/package.json +13 -7
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +7 -3
  5. package/src/errors.d.ts +12 -2
  6. package/src/errors.js +17 -3
  7. package/src/errors.spec.js +19 -0
  8. package/src/helpers/activity_options.js +21 -0
  9. package/src/helpers/activity_options.spec.js +55 -0
  10. package/src/helpers/error_serializer.js +236 -0
  11. package/src/helpers/error_serializer.spec.js +413 -0
  12. package/src/helpers/errors.js +23 -14
  13. package/src/helpers/errors.spec.js +31 -75
  14. package/src/helpers/fetch.js +1 -38
  15. package/src/helpers/fetch.spec.js +1 -39
  16. package/src/helpers/function.js +13 -0
  17. package/src/helpers/function.spec.js +23 -1
  18. package/src/helpers/object.js +48 -35
  19. package/src/helpers/object.spec.js +123 -75
  20. package/src/helpers/redact.js +51 -0
  21. package/src/helpers/redact.spec.js +66 -0
  22. package/src/helpers/string.js +10 -0
  23. package/src/helpers/string.spec.js +36 -1
  24. package/src/hooks/index.d.ts +76 -78
  25. package/src/hooks/index.js +57 -49
  26. package/src/hooks/index.spec.js +104 -105
  27. package/src/hooks/pending_hooks.js +24 -0
  28. package/src/hooks/pending_hooks.spec.js +62 -0
  29. package/src/interface/evaluator.d.ts +10 -9
  30. package/src/interface/evaluator.js +1 -6
  31. package/src/interface/evaluator.spec.js +10 -10
  32. package/src/interface/index.js +2 -1
  33. package/src/interface/step.d.ts +16 -15
  34. package/src/interface/step.js +1 -6
  35. package/src/interface/step.spec.js +10 -10
  36. package/src/interface/validations/index.js +33 -53
  37. package/src/interface/validations/index.spec.js +25 -17
  38. package/src/interface/webhook.js +1 -2
  39. package/src/interface/webhook.spec.js +25 -19
  40. package/src/interface/workflow.d.ts +25 -18
  41. package/src/interface/workflow.js +64 -85
  42. package/src/interface/workflow.spec.js +220 -226
  43. package/src/interface/workflow_activity_options.js +2 -5
  44. package/src/interface/workflow_utils.d.ts +15 -0
  45. package/src/interface/workflow_utils.js +19 -0
  46. package/src/interface/workflow_utils.spec.js +82 -1
  47. package/src/sdk/helpers/objects.d.ts +24 -19
  48. package/src/sdk/runtime/events.d.ts +4 -6
  49. package/src/sdk/runtime/events.js +2 -15
  50. package/src/sdk/runtime/events.spec.js +14 -92
  51. package/src/temporal/client.js +47 -0
  52. package/src/temporal/client.spec.js +124 -0
  53. package/src/temporal/index.d.ts +32 -0
  54. package/src/temporal/index.js +1 -0
  55. package/src/tracing/trace_engine.js +9 -11
  56. package/src/tracing/trace_engine.spec.js +67 -28
  57. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  58. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  59. package/src/worker/configs.js +0 -3
  60. package/src/worker/global_functions.js +2 -2
  61. package/src/worker/global_functions.spec.js +3 -3
  62. package/src/worker/index.js +24 -11
  63. package/src/worker/index.spec.js +36 -16
  64. package/src/worker/interceptors/activity.js +34 -21
  65. package/src/worker/interceptors/activity.spec.js +77 -172
  66. package/src/worker/interceptors/workflow.js +58 -16
  67. package/src/worker/interceptors/workflow.spec.js +183 -56
  68. package/src/worker/loader/activities.js +56 -40
  69. package/src/worker/loader/activities.spec.js +21 -22
  70. package/src/worker/loader/workflows.js +16 -3
  71. package/src/worker/loader/workflows.spec.js +36 -5
  72. package/src/worker/log_hooks.js +22 -19
  73. package/src/worker/log_hooks.spec.js +14 -6
  74. package/src/worker/sinks.js +8 -8
  75. package/src/worker/sinks.spec.js +9 -9
  76. package/src/worker/telemetry.js +2 -1
  77. package/src/worker/temporal_logger.js +33 -0
  78. package/src/worker/temporal_logger.spec.js +90 -0
  79. package/src/worker/webpack_loaders/consts.js +6 -0
  80. package/src/worker/webpack_loaders/tools.js +1 -146
  81. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  82. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  83. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  84. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  85. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  86. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  87. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  88. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  89. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  90. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  91. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  92. package/src/helpers/aggregations.js +0 -44
  93. package/src/helpers/aggregations.spec.js +0 -92
  94. package/src/tracing/tools/utils.js +0 -42
  95. package/src/tracing/tools/utils.spec.js +0 -83
  96. package/src/worker/proxy.js +0 -20
  97. package/src/worker/proxy.spec.js +0 -51
  98. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  99. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
@@ -12,7 +12,7 @@ import {
12
12
  isStringLiteral
13
13
  } from '@babel/types';
14
14
  import { existsSync, readFileSync } from 'node:fs';
15
- import { parse, isWorkflowPath, buildWorkflowNameMap } from './tools.js';
15
+ import { parse, isWorkflowPath, buildWorkflowNameMap } from '../tools.js';
16
16
 
17
17
  /**
18
18
  * Resolves bare npm imports to workflow runtime names:
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
- import { parse } from './tools.js';
5
+ import { parse } from '../tools.js';
6
6
  import {
7
7
  isBareNpmSpecifier,
8
8
  resolveBareImportSpecifiersAsWorkflows,
@@ -1,44 +0,0 @@
1
- import { Attribute } from '#trace_attribute';
2
- import Decimal from 'decimal.js';
3
-
4
- /**
5
- * @typedef {object} Aggregation
6
- *
7
- * @property {object} cost
8
- * @property {object} cost.total - Total cost
9
- * @property {object} tokens
10
- * @property {object} tokens.total - Total tokens used
11
- * @property {object} tokens.input - Total input tokens used
12
- * @property {object} tokens.input_cached - Total cached input tokens used
13
- * @property {object} tokens.output - Total output tokens used
14
- * @property {object} tokens.reasoning - Total reasoning tokens used
15
- * @property {object} httpRequests
16
- * @property {object} httpRequests.total - Total number of http requests made
17
- */
18
- /**
19
- * Aggregates a collection of Attributes into a object with totals
20
- *
21
- * @param {Attribute} attributes
22
- * @returns {Aggregation} aggregation object
23
- */
24
- export const aggregateAttributes = attributes => ( {
25
- cost: {
26
- total: attributes
27
- .filter( a => [ Attribute.HTTPRequestCost.TYPE, Attribute.LLMUsage.TYPE ].includes( a.type ) )
28
- .reduce( ( sum, a ) => sum.add( a.total ), Decimal( 0 ) ).toNumber()
29
- },
30
- tokens: {
31
- total: attributes
32
- .filter( a => Attribute.LLMUsage.TYPE === a.type )
33
- .reduce( ( sum, a ) => sum.add( a.tokensUsed ), Decimal( 0 ) ).toNumber(),
34
- ...Object.entries( attributes
35
- .filter( a => Attribute.LLMUsage.TYPE === a.type )
36
- .flatMap( a => a.usage )
37
- .reduce( ( obj, a ) => Object.assign( obj, { [a.type]: ( obj[a.type] ?? Decimal( 0 ) ).add( a.amount ) } ), {} ) )
38
- .reduce( ( obj, [ k, v ] ) => Object.assign( obj, { [k]: v.toNumber() } ), {} ) // convert all values to number
39
-
40
- },
41
- httpRequests: {
42
- total: attributes.filter( a => Attribute.HTTPRequestCount.TYPE === a.type ).length
43
- }
44
- } );
@@ -1,92 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Attribute } from '#trace_attribute';
3
- import { aggregateAttributes } from './aggregations.js';
4
-
5
- describe( 'aggregateAttributes', () => {
6
- it( 'returns zeroed aggregations when there are no attributes', () => {
7
- expect( aggregateAttributes( [] ) ).toEqual( {
8
- cost: { total: 0 },
9
- tokens: { total: 0 },
10
- httpRequests: { total: 0 }
11
- } );
12
- } );
13
-
14
- it( 'aggregates costs, token usage, and HTTP request count by attribute type', () => {
15
- const attributes = [
16
- {
17
- type: Attribute.HTTPRequestCount.TYPE,
18
- url: 'https://api.example.test/a',
19
- requestId: 'req-1'
20
- },
21
- {
22
- type: Attribute.HTTPRequestCount.TYPE,
23
- url: 'https://api.example.test/b',
24
- requestId: 'req-2'
25
- },
26
- {
27
- type: Attribute.HTTPRequestCost.TYPE,
28
- url: 'https://api.example.test/a',
29
- requestId: 'req-1',
30
- total: 0.2
31
- },
32
- {
33
- type: Attribute.LLMUsage.TYPE,
34
- modelId: 'gpt-4o',
35
- total: 0.3,
36
- tokensUsed: 120,
37
- usage: [
38
- { type: 'input', ppm: 1, amount: 100, total: 0.1 },
39
- { type: 'output', ppm: 2, amount: 20, total: 0.2 }
40
- ]
41
- },
42
- {
43
- type: Attribute.LLMUsage.TYPE,
44
- modelId: 'gpt-4o-mini',
45
- total: 0.05,
46
- tokensUsed: 30,
47
- usage: [
48
- { type: 'input', ppm: 1, amount: 25, total: 0.025 },
49
- { type: 'reasoning', ppm: 5, amount: 5, total: 0.025 }
50
- ]
51
- },
52
- {
53
- type: 'unrelated',
54
- total: 100,
55
- tokensUsed: 100
56
- }
57
- ];
58
-
59
- expect( aggregateAttributes( attributes ) ).toEqual( {
60
- cost: { total: 0.55 },
61
- tokens: {
62
- total: 150,
63
- input: 125,
64
- output: 20,
65
- reasoning: 5
66
- },
67
- httpRequests: { total: 2 }
68
- } );
69
- } );
70
-
71
- it( 'uses LLMUsage.tokensUsed for total tokens instead of summing usage amounts', () => {
72
- const attributes = [
73
- {
74
- type: Attribute.LLMUsage.TYPE,
75
- modelId: 'provider-model',
76
- total: 0.1,
77
- tokensUsed: 42,
78
- usage: [
79
- { type: 'input', ppm: 1, amount: 10, total: 0.01 },
80
- { type: 'output', ppm: 1, amount: 5, total: 0.005 }
81
- ]
82
- }
83
- ];
84
-
85
- expect( aggregateAttributes( attributes ).tokens ).toEqual( {
86
- total: 42,
87
- input: 10,
88
- output: 5
89
- } );
90
- } );
91
- } );
92
-
@@ -1,42 +0,0 @@
1
- import { inspect } from 'node:util';
2
-
3
- /**
4
- * @typedef {object} SerializedError
5
- * @property {string} name - The error constructor name
6
- * @property {string} message - The error message
7
- * @property {string} stack - The error stack trace
8
- */
9
-
10
- /**
11
- * Recursively Serialize an error object. Navigate using "cause" property.
12
- *
13
- * Goes up 10 levels deep.
14
- *
15
- * @param {Error} error
16
- * @returns {SerializedError}
17
- */
18
- export const serializeError = ( () => {
19
-
20
- const serializeValue = v => {
21
- try {
22
- return JSON.parse( JSON.stringify( v ) );
23
- } catch {
24
- return inspect( v, { depth: 5, breakLength: Infinity, colors: false } );
25
- }
26
- };
27
-
28
- return ( error, depth = 0 ) => {
29
- if ( depth > 10 ) {
30
- return { name: 'Error', message: 'Cause chain too deep' };
31
- }
32
- if ( error instanceof Error ) {
33
- return {
34
- name: error.constructor.name,
35
- message: error.message,
36
- stack: error.stack,
37
- ...( error.cause !== undefined ? { cause: serializeError( error.cause, depth + 1 ) } : {} )
38
- };
39
- }
40
- return serializeValue( error );
41
- };
42
- } )();
@@ -1,83 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { serializeError } from './utils.js';
3
-
4
- describe( 'serializeError', () => {
5
- it( 'serializes basic Error fields', () => {
6
- const error = new Error( 'boom' );
7
-
8
- const result = serializeError( error );
9
-
10
- expect( result ).toMatchObject( {
11
- name: 'Error',
12
- message: 'boom'
13
- } );
14
- expect( typeof result.stack ).toBe( 'string' );
15
- expect( result ).not.toHaveProperty( 'cause' );
16
- } );
17
-
18
- it( 'preserves custom error constructor names', () => {
19
- class CustomError extends Error {}
20
- const error = new CustomError( 'custom boom' );
21
-
22
- expect( serializeError( error ) ).toMatchObject( {
23
- name: 'CustomError',
24
- message: 'custom boom'
25
- } );
26
- } );
27
-
28
- it( 'recursively serializes Error causes', () => {
29
- const root = new TypeError( 'root failure' );
30
- const wrapped = new Error( 'wrapped failure', { cause: root } );
31
-
32
- expect( serializeError( wrapped ) ).toMatchObject( {
33
- name: 'Error',
34
- message: 'wrapped failure',
35
- cause: {
36
- name: 'TypeError',
37
- message: 'root failure'
38
- }
39
- } );
40
- } );
41
-
42
- it( 'preserves JSON-serializable non-Error causes', () => {
43
- const cause = {
44
- code: 'bad_input',
45
- path: [ 'items', 0, 'title' ],
46
- message: 'Expected title'
47
- };
48
- const error = new Error( 'validation failed', { cause } );
49
-
50
- expect( serializeError( error ) ).toMatchObject( {
51
- name: 'Error',
52
- message: 'validation failed',
53
- cause
54
- } );
55
- } );
56
-
57
- it( 'falls back to inspect for non-JSON-serializable causes', () => {
58
- const cause = { name: 'circular' };
59
- cause.self = cause;
60
- const error = new Error( 'failed', { cause } );
61
-
62
- const result = serializeError( error );
63
-
64
- expect( result.cause ).toContain( 'circular' );
65
- expect( result.cause ).toContain( 'Circular' );
66
- } );
67
-
68
- it( 'falls back to inspect for primitive values JSON cannot serialize', () => {
69
- expect( serializeError( 1n ) ).toBe( '1n' );
70
- } );
71
-
72
- it( 'stops serializing cause chains after the depth limit', () => {
73
- const makeErrorChain = depth => depth === 0 ?
74
- new Error( 'leaf' ) :
75
- new Error( `level ${depth}`, { cause: makeErrorChain( depth - 1 ) } );
76
- const getCauseAtDepth = ( error, depth ) => depth === 0 ?
77
- error :
78
- getCauseAtDepth( error.cause, depth - 1 );
79
-
80
- expect( getCauseAtDepth( serializeError( makeErrorChain( 11 ) ), 11 ) )
81
- .toEqual( { name: 'Error', message: 'Cause chain too deep' } );
82
- } );
83
- } );
@@ -1,20 +0,0 @@
1
- import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
2
- import { createChildLogger } from '#logger';
3
-
4
- const log = createChildLogger( 'Proxy' );
5
-
6
- /**
7
- * Routes all `fetch()` calls (including those inside Temporal activities)
8
- * through an HTTP/HTTPS proxy when standard proxy env vars are set.
9
- * No-op when none are set.
10
- */
11
- export const bootstrapFetchProxy = () => {
12
- const httpProxyUrl = process.env.http_proxy ?? process.env.HTTP_PROXY;
13
- const httpsProxyUrl = process.env.https_proxy ?? process.env.HTTPS_PROXY;
14
-
15
- if ( httpProxyUrl?.length > 0 || httpsProxyUrl?.length > 0 ) {
16
- log.info( 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent', { httpProxyUrl, httpsProxyUrl } );
17
- /* Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299 */
18
- setGlobalDispatcher( new EnvHttpProxyAgent( { allowH2: false } ) );
19
- }
20
- };
@@ -1,51 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const mockSetGlobalDispatcher = vi.fn();
4
- const MockEnvHttpProxyAgent = vi.fn();
5
- const mockLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
6
-
7
- vi.mock( 'undici', () => ( {
8
- EnvHttpProxyAgent: MockEnvHttpProxyAgent,
9
- setGlobalDispatcher: mockSetGlobalDispatcher
10
- } ) );
11
-
12
- vi.mock( '#logger', () => ( {
13
- createChildLogger: () => mockLogger
14
- } ) );
15
-
16
- describe( 'worker/proxy', () => {
17
- const proxyEnv = [ 'http_proxy', 'HTTP_PROXY', 'https_proxy', 'HTTPS_PROXY' ];
18
-
19
- beforeEach( () => {
20
- vi.clearAllMocks();
21
- for ( const key of proxyEnv ) {
22
- delete process.env[key];
23
- }
24
- } );
25
-
26
- afterEach( () => {
27
- vi.resetModules();
28
- } );
29
-
30
- it( 'does nothing when no proxy env vars are set', async () => {
31
- const { bootstrapFetchProxy } = await import( './proxy.js' );
32
- bootstrapFetchProxy();
33
-
34
- expect( MockEnvHttpProxyAgent ).not.toHaveBeenCalled();
35
- expect( mockSetGlobalDispatcher ).not.toHaveBeenCalled();
36
- expect( mockLogger.info ).not.toHaveBeenCalled();
37
- } );
38
-
39
- it( 'sets global dispatcher when proxy URL is available', async () => {
40
- process.env.HTTP_PROXY = 'http://proxy:8080/';
41
- const { bootstrapFetchProxy } = await import( './proxy.js' );
42
- bootstrapFetchProxy();
43
-
44
- expect( MockEnvHttpProxyAgent ).toHaveBeenCalledWith( { allowH2: false } );
45
- expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
46
- expect( mockLogger.info ).toHaveBeenCalledWith(
47
- 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent',
48
- { httpProxyUrl: 'http://proxy:8080/', httpsProxyUrl: undefined }
49
- );
50
- } );
51
- } );
@@ -1,193 +0,0 @@
1
- import traverseModule from '@babel/traverse';
2
- import { isArrowFunctionExpression, isIdentifier } from '@babel/types';
3
- import {
4
- toFunctionExpression,
5
- createThisMethodCall,
6
- isFunction,
7
- bindThisAtCallSite,
8
- isFunctionLikeBinding
9
- } from '../tools.js';
10
-
11
- // Handle CJS/ESM interop for Babel packages when executed as a webpack loader
12
- const traverse = traverseModule.default ?? traverseModule;
13
-
14
- /**
15
- * Check whether a CallExpression callee is a simple Identifier.
16
- * Only direct identifier calls are rewritten; member/dynamic calls are skipped.
17
- *
18
- * We only support rewriting `Foo()` calls that refer to imported steps/evaluators
19
- * or local call-chain functions. Calls like `obj.Foo()` or `(getFn())()` are out of scope.
20
- *
21
- * Examples:
22
- * - Supported: `Foo()`
23
- * - Skipped: `obj.Foo()`, `(getFn())()`
24
- *
25
- * @param {import('@babel/traverse').NodePath} cPath - Path to a CallExpression node.
26
- * @returns {boolean} True when callee is an Identifier.
27
- */
28
- const isIdentifierCallee = cPath => isIdentifier( cPath.node.callee );
29
-
30
- /**
31
- * Convert an ArrowFunctionExpression at the given path into a FunctionExpression
32
- * to ensure dynamic `this` semantics inside the function body.
33
- *
34
- * Workflow code relies on `this` to invoke steps/evaluators (e.g., `this.invokeStep(...)`).
35
- * Arrow functions capture `this` lexically, which would break that contract.
36
- *
37
- * If the node is an arrow, it is replaced by an equivalent FunctionExpression and
38
- * the `state.rewrote` flag is set. If not an arrow, this is a no-op.
39
- *
40
- * @param {import('@babel/traverse').NodePath} nodePath - Path to a function node.
41
- * @param {{ rewrote: boolean }} state - Mutation target to indicate a rewrite occurred.
42
- * @returns {void}
43
- */
44
- const normalizeArrowToFunctionPath = ( nodePath, state ) => {
45
- if ( isArrowFunctionExpression( nodePath.node ) ) {
46
- nodePath.replaceWith( toFunctionExpression( nodePath.node ) );
47
- state.rewrote = true;
48
- }
49
- };
50
- /**
51
- * Rewrite calls inside a function body and collect call-chain functions discovered within.
52
- * - Imported calls (steps/shared/evaluators) are rewritten to `this.invokeX`.
53
- * - Local call-chain function calls are rewritten to `fn.call(this, ...)` to bind `this` correctly.
54
- * - Returns a map of call-chain function name -> binding path for further recursive processing.
55
- *
56
- * @param {import('@babel/traverse').NodePath} bodyPath - Path to a function's body node.
57
- * @param {Array<{ list: Array<any>, method: string, key: string }>} descriptors - Import rewrite descriptors.
58
- * @param {{ rewrote: boolean }} state - Mutable state used to flag that edits were performed.
59
- * @returns {Map<string, import('@babel/traverse').NodePath>} Discovered call-chain function bindings.
60
- */
61
- const rewriteCallsInBody = ( bodyPath, descriptors, state ) => {
62
- const callChainFunctions = new Map();
63
- bodyPath.traverse( {
64
- CallExpression: cPath => {
65
- if ( !isIdentifierCallee( cPath ) ) {
66
- return; // Only identifier callees are supported (skip member/dynamic)
67
- }
68
- const callee = cPath.node.callee;
69
-
70
- // Rewrite imported calls (steps/shared/evaluators)
71
- for ( const { list, method, key } of descriptors ) {
72
- const found = list.find( x => x.localName === callee.name );
73
- if ( found ) {
74
- const args = cPath.node.arguments;
75
- cPath.replaceWith( createThisMethodCall( method, found[key], args ) );
76
- state.rewrote = true;
77
- return;
78
- }
79
- }
80
-
81
- // Rewrite local call-chain function calls and track for recursive processing
82
- const binding = cPath.scope.getBinding( callee.name );
83
- if ( !binding ) {
84
- return;
85
- }
86
- if ( !isFunctionLikeBinding( binding.path.node ) ) {
87
- return; // Not a function-like binding
88
- }
89
-
90
- // Queue call-chain function for recursive processing
91
- if ( !callChainFunctions.has( callee.name ) ) {
92
- callChainFunctions.set( callee.name, binding.path );
93
- }
94
-
95
- // Bind `this` at callsite: fn(...) -> fn.call(this, ...)
96
- cPath.replaceWith( bindThisAtCallSite( callee.name, cPath.node.arguments ) );
97
- state.rewrote = true;
98
- }
99
- } );
100
- return callChainFunctions;
101
- };
102
-
103
- /**
104
- * Recursively process a call-chain function:
105
- * - Ensures the function is a FunctionExpression (converts arrow when needed).
106
- * - Rewrites calls inside the function using `rewriteCallsInBody`.
107
- * - Follows nested call-chain functions depth-first while avoiding cycles via `processedFns`.
108
- *
109
- * @param {object} params - Params for processing a call-chain function.
110
- * @param {string} params.name - Local identifier name in the current scope.
111
- * @param {import('@babel/traverse').NodePath} params.bindingPath - Binding path of the function declaration.
112
- * @param {{ rewrote: boolean }} params.state - Mutable state used to flag that edits were performed.
113
- * @param {Array<{ list: Array<any>, method: string, key: string }>} params.descriptors - Import rewrite descriptors.
114
- * @param {Set<string>} [params.processedFns] - Already processed names to avoid cycles.
115
- */
116
- const processFunction = ( { name, bindingPath, state, descriptors, processedFns = new Set() } ) => {
117
- // Avoid infinite loops for recursive/repeated references
118
- if ( processedFns.has( name ) || bindingPath.removed ) {
119
- return;
120
- }
121
- processedFns.add( name );
122
-
123
- if ( bindingPath.isVariableDeclarator() ) {
124
- // Case 1: const foo = <function or arrow>
125
- const initPath = bindingPath.get( 'init' );
126
- // Arrow functions capture `this` lexically; normalize for dynamic `this`
127
- normalizeArrowToFunctionPath( initPath, state );
128
- // Rewrite calls in body; collect nested call-chain functions from this scope
129
- const callChainFunctions = rewriteCallsInBody( initPath.get( 'body' ), descriptors, state );
130
- // DFS: process nested call-chain functions (processedFns prevents cycles)
131
- callChainFunctions.forEach( ( childBindingPath, childName ) => {
132
- processFunction( { name: childName, bindingPath: childBindingPath, state, descriptors, processedFns } );
133
- } );
134
- } else if ( bindingPath.isFunctionDeclaration() ) {
135
- // Case 2: function foo(...) { ... }
136
- // Function declarations already have dynamic `this`; no normalization needed
137
- const callChainFunctions = rewriteCallsInBody( bindingPath.get( 'body' ), descriptors, state );
138
- // Continue DFS into any functions called from this declaration
139
- callChainFunctions.forEach( ( childBindingPath, childName ) => {
140
- processFunction( { name: childName, bindingPath: childBindingPath, state, descriptors, processedFns } );
141
- } );
142
- }
143
- };
144
-
145
- /**
146
- * Rewrite calls to imported steps/evaluators within `fn` object properties.
147
- * Converts arrow fns to functions and replaces `StepX(...)` with
148
- * `this.invokeStep('name', ...)`.
149
- *
150
- * @param {object} params
151
- * @param {import('@babel/types').File} params.ast - Parsed file AST.
152
- * @param {Array<{localName:string,stepName:string}>} params.stepImports - Step imports.
153
- * @param {Array<{localName:string,stepName:string}>} params.sharedStepImports - Shared step imports.
154
- * @param {Array<{localName:string,evaluatorName:string}>} params.evaluatorImports - Evaluator imports.
155
- * @returns {boolean} True if the AST was modified; false otherwise.
156
- */
157
- export default function rewriteFnBodies( { ast, stepImports, sharedStepImports = [], evaluatorImports, sharedEvaluatorImports = [] } ) {
158
- const state = { rewrote: false };
159
- // Build rewrite descriptors once per traversal
160
- const descriptors = [
161
- { list: stepImports, method: 'invokeStep', key: 'stepName' },
162
- { list: sharedStepImports, method: 'invokeSharedStep', key: 'stepName' },
163
- { list: evaluatorImports, method: 'invokeEvaluator', key: 'evaluatorName' },
164
- { list: sharedEvaluatorImports, method: 'invokeSharedEvaluator', key: 'evaluatorName' }
165
- ];
166
- traverse( ast, {
167
- ObjectProperty: path => {
168
- // Only transform object properties named 'fn'
169
- if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) {
170
- return;
171
- }
172
-
173
- const val = path.node.value;
174
-
175
- // Only functions (including arrows) are eligible
176
- if ( !isFunction( val ) && !isArrowFunctionExpression( val ) ) {
177
- return;
178
- }
179
-
180
- // Normalize arrow to function for correct dynamic `this`
181
- normalizeArrowToFunctionPath( path.get( 'value' ), state );
182
-
183
- // Rewrite the main workflow fn body and collect call-chain functions discovered within it
184
- const callChainFunctions = rewriteCallsInBody( path.get( 'value.body' ), descriptors, state );
185
-
186
- // Recursively rewrite call-chain functions and any functions they call
187
- callChainFunctions.forEach( ( bindingPath, name ) => {
188
- processFunction( { name, bindingPath, state, descriptors } );
189
- } );
190
- }
191
- } );
192
- return state.rewrote;
193
- };
@@ -1,123 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import generatorModule from '@babel/generator';
3
- import { parse } from '../tools.js';
4
- import rewriteFnBodies from './rewrite_fn_bodies.js';
5
-
6
- const generate = generatorModule.default ?? generatorModule;
7
-
8
- describe( 'rewrite_fn_bodies', () => {
9
- it( 'converts arrow to function and rewrites step calls without rewriting workflow calls', () => {
10
- const src = `
11
- const obj = {
12
- fn: async x => {
13
- StepA( 1 );
14
- FlowB( 2 );
15
- }
16
- }`;
17
- const ast = parse( src, 'file.js' );
18
- const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
19
-
20
- const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports: [] } );
21
- expect( rewrote ).toBe( true );
22
-
23
- const { code } = generate( ast, { quotes: 'single' } );
24
- expect( code ).toMatch( /this\.invokeStep\(([\"'])step\.a\1,\s*1\)/ );
25
- expect( code ).toMatch( /FlowB\(2\)/ );
26
- } );
27
-
28
- it( 'rewrites evaluator calls to this.invokeEvaluator', () => {
29
- const src = `
30
- const obj = {
31
- fn: async x => {
32
- EvalA(3);
33
- }
34
- };`;
35
- const ast = parse( src, 'file.js' );
36
- const evaluatorImports = [ { localName: 'EvalA', evaluatorName: 'eval.a' } ];
37
- const rewrote = rewriteFnBodies( { ast, stepImports: [], evaluatorImports } );
38
- expect( rewrote ).toBe( true );
39
- } );
40
-
41
- it( 'does nothing when no matching calls are present', () => {
42
- const src = [ 'const obj = { fn: function() { other(); } }' ].join( '\n' );
43
- const ast = parse( src, 'file.js' );
44
- const rewrote = rewriteFnBodies( { ast, stepImports: [], evaluatorImports: [] } );
45
- expect( rewrote ).toBe( false );
46
- } );
47
-
48
- it( 'rewrites helper calls and helper bodies (steps and evaluators)', () => {
49
- const src = `
50
- const foo = async () => {
51
- StepA( 1 );
52
- };
53
-
54
- function bar( x ) {
55
- EvalA( x );
56
- }
57
-
58
- const obj = {
59
- fn: async x => {
60
- foo();
61
- bar( 2 );
62
- }
63
- }`;
64
-
65
- const ast = parse( src, 'file.js' );
66
- const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
67
- const evaluatorImports = [ { localName: 'EvalA', evaluatorName: 'eval.a' } ];
68
-
69
- const rewrote = rewriteFnBodies( { ast, stepImports, sharedStepImports: [], evaluatorImports } );
70
- expect( rewrote ).toBe( true );
71
-
72
- const { code } = generate( ast, { quotes: 'single' } );
73
-
74
- // Helper calls in fn are rewritten to call(this, ...)
75
- expect( code ).toMatch( /foo\.call\(this\)/ );
76
- expect( code ).toMatch( /bar\.call\(this,\s*2\)/ );
77
-
78
- // Inside helpers, calls are rewritten
79
- expect( code ).toMatch( /this\.invokeStep\(([\"'])step\.a\1,\s*1\)/ );
80
- expect( code ).toMatch( /this\.invokeEvaluator\(([\"'])eval\.a\1,\s*x\)/ );
81
-
82
- // Arrow helper converted to function expression to allow dynamic this
83
- expect( code ).toMatch( /const foo = async function/ );
84
- } );
85
-
86
- it( 'rewrites nested helper chains until the step invocation', () => {
87
- const src = `
88
- const foo = () => {
89
- bar();
90
- };
91
-
92
- function bar() {
93
- baz( 42 );
94
- }
95
-
96
- const baz = n => {
97
- StepA( n );
98
- };
99
-
100
- const obj = {
101
- fn: async () => {
102
- foo();
103
- }
104
- }`;
105
-
106
- const ast = parse( src, 'file.js' );
107
- const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
108
- const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports: [] } );
109
- expect( rewrote ).toBe( true );
110
-
111
- const { code } = generate( ast, { quotes: 'single' } );
112
- // Calls along the chain are bound with this
113
- expect( code ).toMatch( /foo\.call\(this\)/ );
114
- expect( code ).toMatch( /bar\.call\(this\)/ );
115
- expect( code ).toMatch( /baz\.call\(this,\s*42\)/ );
116
- // Deep step rewrite in the last helper
117
- expect( code ).toMatch( /this\.invokeStep\(([\"'])step\.a\1,\s*n\)/ );
118
- // Arrow helpers converted to functions
119
- expect( code ).toMatch( /const foo = function/ );
120
- expect( code ).toMatch( /const baz = function/ );
121
- } );
122
- } );
123
-