@output.ai/core 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -59
- package/package.json +10 -3
- package/src/configs.js +1 -1
- package/src/consts.js +4 -3
- package/src/errors.js +11 -0
- package/src/index.d.ts +302 -30
- package/src/index.js +3 -2
- package/src/interface/metadata.js +3 -3
- package/src/interface/step.js +18 -4
- package/src/interface/utils.js +41 -4
- package/src/interface/utils.spec.js +71 -0
- package/src/interface/validations/ajv_provider.js +3 -0
- package/src/interface/validations/runtime.js +69 -0
- package/src/interface/validations/runtime.spec.js +50 -0
- package/src/interface/validations/static.js +67 -0
- package/src/interface/validations/static.spec.js +101 -0
- package/src/interface/webhook.js +15 -14
- package/src/interface/workflow.js +45 -40
- package/src/internal_activities/index.js +16 -5
- package/src/worker/catalog_workflow/catalog.js +105 -0
- package/src/worker/catalog_workflow/index.js +21 -0
- package/src/worker/catalog_workflow/index.spec.js +139 -0
- package/src/worker/catalog_workflow/workflow.js +13 -0
- package/src/worker/index.js +41 -5
- package/src/worker/interceptors/activity.js +3 -2
- package/src/worker/internal_utils.js +60 -0
- package/src/worker/internal_utils.spec.js +134 -0
- package/src/worker/loader.js +30 -44
- package/src/worker/loader.spec.js +68 -0
- package/src/worker/sinks.js +2 -1
- package/src/worker/tracer/index.js +35 -3
- package/src/worker/tracer/index.test.js +115 -0
- package/src/worker/tracer/tracer_tree.js +29 -5
- package/src/worker/tracer/tracer_tree.test.js +116 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +133 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +77 -0
- package/src/worker/webpack_loaders/workflow_rewriter/consts.js +3 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +58 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +129 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +70 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +33 -0
- package/src/worker/webpack_loaders/workflow_rewriter/tools.js +245 -0
- package/src/worker/webpack_loaders/workflow_rewriter/tools.spec.js +144 -0
- package/src/errors.d.ts +0 -3
- package/src/worker/temp/__workflows_entrypoint.js +0 -6
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
4
|
+
|
|
5
|
+
vi.mock( '#consts', () => ( {
|
|
6
|
+
SEND_WEBHOOK_ACTIVITY_NAME: '__internal#sendWebhookPost',
|
|
7
|
+
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
8
|
+
METADATA_ACCESS_SYMBOL
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
const sendWebhookPostMock = vi.fn();
|
|
12
|
+
vi.mock( '#internal_activities', () => ( {
|
|
13
|
+
sendWebhookPost: sendWebhookPostMock
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
16
|
+
// Mock internal_utils to control filesystem-independent behavior
|
|
17
|
+
const iteratorMock = vi.fn();
|
|
18
|
+
const recursiveMock = vi.fn();
|
|
19
|
+
const writeFileMock = vi.fn();
|
|
20
|
+
vi.mock( './internal_utils.js', () => ( {
|
|
21
|
+
iteratorOverImportedComponents: iteratorMock,
|
|
22
|
+
recursiveNavigateWhileCollecting: recursiveMock,
|
|
23
|
+
writeFileOnLocationSync: writeFileMock
|
|
24
|
+
} ) );
|
|
25
|
+
|
|
26
|
+
describe( 'worker/loader', () => {
|
|
27
|
+
beforeEach( () => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
it( 'loadActivities returns map including system activity', async () => {
|
|
32
|
+
const { loadActivities } = await import( './loader.js' );
|
|
33
|
+
|
|
34
|
+
recursiveMock.mockReturnValue( [ { pathname: '/a/steps.js', path: '/a', url: 'file:///a/steps.js' } ] );
|
|
35
|
+
iteratorMock.mockImplementation( async function *() {
|
|
36
|
+
yield { component: () => {}, metadata: { name: 'Act1' }, pathname: '/a/steps.js', path: '/a' };
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
const activities = await loadActivities( '/root' );
|
|
40
|
+
expect( activities['/a#Act1'] ).toBeTypeOf( 'function' );
|
|
41
|
+
expect( activities['__internal#sendWebhookPost'] ).toBe( sendWebhookPostMock );
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
it( 'loadWorkflows returns array of workflows with metadata', async () => {
|
|
45
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
46
|
+
|
|
47
|
+
recursiveMock.mockReturnValue( [ { pathname: '/b/workflow.js', path: '/b', url: 'file:///b/workflow.js' } ] );
|
|
48
|
+
iteratorMock.mockImplementation( async function *() {
|
|
49
|
+
yield { metadata: { name: 'Flow1', description: 'd' }, pathname: '/b/workflow.js', path: '/b' };
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
const workflows = await loadWorkflows( '/root' );
|
|
53
|
+
expect( workflows ).toEqual( [ { name: 'Flow1', description: 'd', pathname: '/b/workflow.js', path: '/b' } ] );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'createWorkflowsEntryPoint writes index and returns its path', async () => {
|
|
57
|
+
const { createWorkflowsEntryPoint } = await import( './loader.js' );
|
|
58
|
+
|
|
59
|
+
const workflows = [ { name: 'W', pathname: '/abs/wf.js' } ];
|
|
60
|
+
const entry = createWorkflowsEntryPoint( workflows );
|
|
61
|
+
|
|
62
|
+
expect( writeFileMock ).toHaveBeenCalledTimes( 1 );
|
|
63
|
+
const [ writtenPath, contents ] = writeFileMock.mock.calls[0];
|
|
64
|
+
expect( entry ).toBe( writtenPath );
|
|
65
|
+
expect( contents ).toContain( 'export { default as W } from \'/abs/wf.js\';' );
|
|
66
|
+
expect( contents ).toContain( 'export { default as catalog }' );
|
|
67
|
+
} );
|
|
68
|
+
} );
|
package/src/worker/sinks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Storage } from './async_storage.js';
|
|
2
2
|
import { trace } from './tracer/index.js';
|
|
3
|
+
import { THIS_LIB_NAME } from '#consts';
|
|
3
4
|
|
|
4
5
|
export const sinks = {
|
|
5
6
|
// This sink allow for sandbox Temporal environment to send trace logs back to the main thread.
|
|
@@ -7,7 +8,7 @@ export const sinks = {
|
|
|
7
8
|
trace: {
|
|
8
9
|
fn( workflowInfo, args ) {
|
|
9
10
|
const { workflowId, workflowType, memo } = workflowInfo;
|
|
10
|
-
Storage.runWithContext( _ => trace( { lib:
|
|
11
|
+
Storage.runWithContext( _ => trace( { lib: THIS_LIB_NAME, ...args } ), { workflowId, workflowType, ...memo } );
|
|
11
12
|
},
|
|
12
13
|
callDuringReplay: false
|
|
13
14
|
}
|
|
@@ -7,12 +7,35 @@ import { tracing as tracingConfig } from '#configs';
|
|
|
7
7
|
|
|
8
8
|
const callerDir = process.argv[2];
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Appends new information to a file
|
|
12
|
+
*
|
|
13
|
+
* Information has to be a JSON
|
|
14
|
+
*
|
|
15
|
+
* File is encoded in utf-8
|
|
16
|
+
*
|
|
17
|
+
* @param {string} path - The full filename
|
|
18
|
+
* @param {object} json - The content
|
|
19
|
+
*/
|
|
10
20
|
const flushEntry = ( path, json ) => appendFileSync( path, JSON.stringify( json ) + EOL, 'utf-8' );
|
|
11
21
|
|
|
12
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Add an event to the execution trace file.
|
|
24
|
+
*
|
|
25
|
+
* Events normally are the result of an operation, either a function call or an IO.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} options
|
|
28
|
+
* @param {string} options.lib - The macro part of the platform that triggered the event
|
|
29
|
+
* @param {string} options.event - The name of the event
|
|
30
|
+
* @param {any} [options.input] - The input of the operation
|
|
31
|
+
* @param {any} [options.output] - The output of the operation
|
|
32
|
+
*/
|
|
33
|
+
export function trace( { lib, event, input = undefined, output = undefined } ) {
|
|
13
34
|
const now = Date.now();
|
|
14
35
|
|
|
15
|
-
if ( !tracingConfig.enabled ) {
|
|
36
|
+
if ( !tracingConfig.enabled ) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
16
39
|
|
|
17
40
|
const {
|
|
18
41
|
activityId: stepId,
|
|
@@ -29,7 +52,9 @@ export function trace( { lib, event, input, output } ) {
|
|
|
29
52
|
|
|
30
53
|
// test for rootWorkflow to append to the same file as the parent/grandparent
|
|
31
54
|
const outputDir = join( callerDir, 'logs', 'runs', rootWorkflowType ?? workflowType );
|
|
32
|
-
if ( !existsSync( outputDir ) ) {
|
|
55
|
+
if ( !existsSync( outputDir ) ) {
|
|
56
|
+
mkdirSync( outputDir, { recursive: true } );
|
|
57
|
+
}
|
|
33
58
|
|
|
34
59
|
const suffix = `-${rootWorkflowId ?? workflowId}.raw`;
|
|
35
60
|
const logFile = readdirSync( outputDir ).find( f => f.endsWith( suffix ) ) ?? `${new Date( now ).toISOString()}-${suffix}`;
|
|
@@ -39,5 +64,12 @@ export function trace( { lib, event, input, output } ) {
|
|
|
39
64
|
buildLogTree( logPath );
|
|
40
65
|
};
|
|
41
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Setup the global tracer function, so it is available to be used by other libraries
|
|
69
|
+
*
|
|
70
|
+
* It will be situated in the global object, under Symbol.for('__trace')
|
|
71
|
+
*
|
|
72
|
+
* @returns {object} The assigned globalThis
|
|
73
|
+
*/
|
|
42
74
|
export const setupGlobalTracer = () =>
|
|
43
75
|
Object.defineProperty( globalThis, Symbol.for( '__trace' ), { value: trace, writable: false, enumerable: false, configurable: false } );
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir, EOL } from 'node:os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { THIS_LIB_NAME } from '#consts';
|
|
6
|
+
|
|
7
|
+
const createTempDir = () => mkdtempSync( join( tmpdir(), 'flow-sdk-trace-' ) );
|
|
8
|
+
|
|
9
|
+
describe( 'tracer/index', () => {
|
|
10
|
+
beforeEach( () => {
|
|
11
|
+
vi.resetModules();
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
vi.useFakeTimers();
|
|
14
|
+
vi.setSystemTime( new Date( '2020-01-01T00:00:00.000Z' ) );
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
afterEach( () => {
|
|
18
|
+
vi.useRealTimers();
|
|
19
|
+
} );
|
|
20
|
+
|
|
21
|
+
it( 'writes a raw log entry and calls buildLogTree (mocked)', async () => {
|
|
22
|
+
const originalArgv2 = process.argv[2];
|
|
23
|
+
const tmp = createTempDir();
|
|
24
|
+
process.argv[2] = tmp;
|
|
25
|
+
|
|
26
|
+
const prevTracing = process.env.TRACING_ENABLED;
|
|
27
|
+
process.env.TRACING_ENABLED = 'true';
|
|
28
|
+
vi.mock( '../async_storage.js', () => ( {
|
|
29
|
+
Storage: {
|
|
30
|
+
load: () => ( {
|
|
31
|
+
activityId: 's1',
|
|
32
|
+
activityType: 'Step 1',
|
|
33
|
+
workflowId: 'wf1',
|
|
34
|
+
workflowType: 'prompt',
|
|
35
|
+
workflowPath: '/workflows/prompt.js',
|
|
36
|
+
parentWorkflowId: undefined,
|
|
37
|
+
rootWorkflowId: undefined,
|
|
38
|
+
rootWorkflowType: undefined
|
|
39
|
+
} )
|
|
40
|
+
}
|
|
41
|
+
} ) );
|
|
42
|
+
vi.mock( './tracer_tree.js', () => ( { buildLogTree: vi.fn() } ) );
|
|
43
|
+
const { trace } = await import( './index.js' );
|
|
44
|
+
|
|
45
|
+
const input = { foo: 1 };
|
|
46
|
+
trace( { lib: THIS_LIB_NAME, event: 'workflow_start', input, output: null } );
|
|
47
|
+
|
|
48
|
+
const { buildLogTree } = await import( './tracer_tree.js' );
|
|
49
|
+
expect( buildLogTree ).toHaveBeenCalledTimes( 1 );
|
|
50
|
+
const logPath = buildLogTree.mock.calls[0][0];
|
|
51
|
+
|
|
52
|
+
const raw = readFileSync( logPath, 'utf-8' );
|
|
53
|
+
const [ firstLine ] = raw.split( EOL );
|
|
54
|
+
const entry = JSON.parse( firstLine );
|
|
55
|
+
|
|
56
|
+
expect( entry ).toMatchObject( {
|
|
57
|
+
lib: THIS_LIB_NAME,
|
|
58
|
+
event: 'workflow_start',
|
|
59
|
+
input,
|
|
60
|
+
output: null,
|
|
61
|
+
stepId: 's1',
|
|
62
|
+
stepName: 'Step 1',
|
|
63
|
+
workflowId: 'wf1',
|
|
64
|
+
workflowType: 'prompt',
|
|
65
|
+
workflowPath: '/workflows/prompt.js'
|
|
66
|
+
} );
|
|
67
|
+
expect( typeof entry.timestamp ).toBe( 'number' );
|
|
68
|
+
|
|
69
|
+
rmSync( tmp, { recursive: true, force: true } );
|
|
70
|
+
process.env.TRACING_ENABLED = prevTracing;
|
|
71
|
+
process.argv[2] = originalArgv2;
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
it( 'does nothing when tracing is disabled', async () => {
|
|
75
|
+
const originalArgv2 = process.argv[2];
|
|
76
|
+
const tmp = createTempDir();
|
|
77
|
+
process.argv[2] = tmp;
|
|
78
|
+
|
|
79
|
+
const prevTracing = process.env.TRACING_ENABLED;
|
|
80
|
+
process.env.TRACING_ENABLED = 'false';
|
|
81
|
+
vi.mock( '../async_storage.js', () => ( {
|
|
82
|
+
Storage: {
|
|
83
|
+
load: () => ( {
|
|
84
|
+
activityId: 's1',
|
|
85
|
+
activityType: 'Step 1',
|
|
86
|
+
workflowId: 'wf1',
|
|
87
|
+
workflowType: 'prompt',
|
|
88
|
+
workflowPath: '/workflows/prompt.js'
|
|
89
|
+
} )
|
|
90
|
+
}
|
|
91
|
+
} ) );
|
|
92
|
+
vi.mock( './tracer_tree.js', () => ( { buildLogTree: vi.fn() } ) );
|
|
93
|
+
const { trace } = await import( './index.js' );
|
|
94
|
+
|
|
95
|
+
trace( { lib: THIS_LIB_NAME, event: 'workflow_start', input: {}, output: null } );
|
|
96
|
+
|
|
97
|
+
const { buildLogTree } = await import( './tracer_tree.js' );
|
|
98
|
+
expect( buildLogTree ).not.toHaveBeenCalled();
|
|
99
|
+
|
|
100
|
+
rmSync( tmp, { recursive: true, force: true } );
|
|
101
|
+
process.env.TRACING_ENABLED = prevTracing;
|
|
102
|
+
process.argv[2] = originalArgv2;
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
it( 'setupGlobalTracer installs global symbol', async () => {
|
|
106
|
+
const prevTracing = process.env.TRACING_ENABLED;
|
|
107
|
+
process.env.TRACING_ENABLED = 'false';
|
|
108
|
+
const { setupGlobalTracer } = await import( './index.js' );
|
|
109
|
+
setupGlobalTracer();
|
|
110
|
+
const sym = Symbol.for( '__trace' );
|
|
111
|
+
expect( typeof globalThis[sym] ).toBe( 'function' );
|
|
112
|
+
process.env.TRACING_ENABLED = prevTracing;
|
|
113
|
+
} );
|
|
114
|
+
} );
|
|
115
|
+
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { EOL } from 'os';
|
|
3
3
|
import { TraceEvent } from './types.js';
|
|
4
|
+
import { THIS_LIB_NAME } from '#consts';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Sorting function that compares two objects and ASC sort them by either .startedAt or, if not present, .timestamp
|
|
8
|
+
*
|
|
9
|
+
* @param {object} a
|
|
10
|
+
* @param {object} b
|
|
11
|
+
* @returns {number} The sorting result [1,-1]
|
|
12
|
+
*/
|
|
13
|
+
const timestampAscSort = ( a, b ) => {
|
|
14
|
+
if ( a.startedAt ) {
|
|
15
|
+
return a.startedAt > b.startedAt ? 1 : 1;
|
|
16
|
+
}
|
|
17
|
+
return a.timestamp > b.timestamp ? 1 : -1;
|
|
18
|
+
};
|
|
6
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Add a member to an array an sort it. It is a mutating method.
|
|
22
|
+
*
|
|
23
|
+
* @param {array} arr - The arr to be changed
|
|
24
|
+
* @param {any} entry - The entry to be added
|
|
25
|
+
* @param {Function} sorter - The sort function to be used (within .filter)
|
|
26
|
+
*/
|
|
7
27
|
const pushSort = ( arr, entry, sorter ) => {
|
|
8
28
|
arr.push( entry );
|
|
9
29
|
arr.sort( sorter );
|
|
10
30
|
};
|
|
11
31
|
|
|
12
|
-
|
|
13
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Transform the trace file into a tree of events, where nested events are represented as children of parent events.
|
|
34
|
+
* And the events STEP_START/STEP_END and WORKFLOW_START/WORKFLOW_END are combined into single events with start and end timestamps.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} src - The trace src filename
|
|
37
|
+
*/
|
|
14
38
|
export const buildLogTree = src => {
|
|
15
39
|
const content = readFileSync( src, 'utf-8' );
|
|
16
40
|
const entries = content.split( EOL ).slice( 0, -1 ).map( c => JSON.parse( c ) );
|
|
@@ -19,7 +43,7 @@ export const buildLogTree = src => {
|
|
|
19
43
|
const workflowsMap = new Map();
|
|
20
44
|
|
|
21
45
|
// close steps/workflows
|
|
22
|
-
for ( const entry of entries.filter( e => e.lib ===
|
|
46
|
+
for ( const entry of entries.filter( e => e.lib === THIS_LIB_NAME ) ) {
|
|
23
47
|
const { event, workflowId, workflowType, workflowPath, parentWorkflowId, stepId, stepName, input, output, timestamp } = entry;
|
|
24
48
|
|
|
25
49
|
const baseEntry = { children: [], startedAt: timestamp, workflowId };
|
|
@@ -41,7 +65,7 @@ export const buildLogTree = src => {
|
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
// insert operations inside steps
|
|
44
|
-
for ( const entry of entries.filter( e => e.lib !==
|
|
68
|
+
for ( const entry of entries.filter( e => e.lib !== THIS_LIB_NAME ) ) {
|
|
45
69
|
pushSort( stepsMap.get( `${entry.workflowId}:${entry.stepId}` ).children, entry, timestampAscSort );
|
|
46
70
|
}
|
|
47
71
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { mkdtempSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { EOL } from 'os';
|
|
7
|
+
import { buildLogTree } from './tracer_tree.js';
|
|
8
|
+
import { TraceEvent } from './types.js';
|
|
9
|
+
import { THIS_LIB_NAME } from '#consts';
|
|
10
|
+
|
|
11
|
+
const createTempDir = () => mkdtempSync( join( tmpdir(), 'flow-sdk-trace-tree-' ) );
|
|
12
|
+
|
|
13
|
+
describe( 'tracer/tracer_tree', () => {
|
|
14
|
+
it( 'builds a tree JSON from a raw log file', () => {
|
|
15
|
+
const tmp = createTempDir();
|
|
16
|
+
const rawPath = join( tmp, 'run-123.raw' );
|
|
17
|
+
|
|
18
|
+
const entries = [
|
|
19
|
+
// root workflow start
|
|
20
|
+
{
|
|
21
|
+
lib: THIS_LIB_NAME,
|
|
22
|
+
event: TraceEvent.WORKFLOW_START,
|
|
23
|
+
input: { a: 1 },
|
|
24
|
+
output: null,
|
|
25
|
+
timestamp: 1000,
|
|
26
|
+
stepId: undefined,
|
|
27
|
+
stepName: undefined,
|
|
28
|
+
workflowId: 'wf1',
|
|
29
|
+
workflowType: 'prompt',
|
|
30
|
+
workflowPath: '/workflows/prompt.js',
|
|
31
|
+
parentWorkflowId: undefined
|
|
32
|
+
},
|
|
33
|
+
// step start
|
|
34
|
+
{
|
|
35
|
+
lib: THIS_LIB_NAME,
|
|
36
|
+
event: TraceEvent.STEP_START,
|
|
37
|
+
input: { x: 1 },
|
|
38
|
+
output: null,
|
|
39
|
+
timestamp: 2000,
|
|
40
|
+
stepId: 's1',
|
|
41
|
+
stepName: 'Step 1',
|
|
42
|
+
workflowId: 'wf1',
|
|
43
|
+
workflowType: 'prompt',
|
|
44
|
+
workflowPath: '/workflows/prompt.js',
|
|
45
|
+
parentWorkflowId: undefined
|
|
46
|
+
},
|
|
47
|
+
// non-core operation within step
|
|
48
|
+
{
|
|
49
|
+
lib: 'tool',
|
|
50
|
+
event: 'call',
|
|
51
|
+
input: { y: 2 },
|
|
52
|
+
output: { y: 3 },
|
|
53
|
+
timestamp: 3000,
|
|
54
|
+
stepId: 's1',
|
|
55
|
+
stepName: 'Step 1',
|
|
56
|
+
workflowId: 'wf1'
|
|
57
|
+
},
|
|
58
|
+
// step end
|
|
59
|
+
{
|
|
60
|
+
lib: THIS_LIB_NAME,
|
|
61
|
+
event: TraceEvent.STEP_END,
|
|
62
|
+
input: null,
|
|
63
|
+
output: { done: true },
|
|
64
|
+
timestamp: 4000,
|
|
65
|
+
stepId: 's1',
|
|
66
|
+
stepName: 'Step 1',
|
|
67
|
+
workflowId: 'wf1',
|
|
68
|
+
workflowType: 'prompt',
|
|
69
|
+
workflowPath: '/workflows/prompt.js',
|
|
70
|
+
parentWorkflowId: undefined
|
|
71
|
+
},
|
|
72
|
+
// workflow end
|
|
73
|
+
{
|
|
74
|
+
lib: THIS_LIB_NAME,
|
|
75
|
+
event: TraceEvent.WORKFLOW_END,
|
|
76
|
+
input: null,
|
|
77
|
+
output: { ok: true },
|
|
78
|
+
timestamp: 5000,
|
|
79
|
+
stepId: undefined,
|
|
80
|
+
stepName: undefined,
|
|
81
|
+
workflowId: 'wf1',
|
|
82
|
+
workflowType: 'prompt',
|
|
83
|
+
workflowPath: '/workflows/prompt.js',
|
|
84
|
+
parentWorkflowId: undefined
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
writeFileSync( rawPath, entries.map( e => JSON.stringify( e ) ).join( EOL ) + EOL, 'utf-8' );
|
|
89
|
+
|
|
90
|
+
buildLogTree( rawPath );
|
|
91
|
+
|
|
92
|
+
const tree = JSON.parse( readFileSync( rawPath.replace( /.raw$/, '.json' ), 'utf-8' ) );
|
|
93
|
+
|
|
94
|
+
expect( tree.event ).toBe( 'workflow' );
|
|
95
|
+
expect( tree.workflowId ).toBe( 'wf1' );
|
|
96
|
+
expect( tree.workflowType ).toBe( 'prompt' );
|
|
97
|
+
expect( tree.startedAt ).toBe( 1000 );
|
|
98
|
+
expect( tree.endedAt ).toBe( 5000 );
|
|
99
|
+
expect( tree.output ).toEqual( { ok: true } );
|
|
100
|
+
expect( Array.isArray( tree.children ) ).toBe( true );
|
|
101
|
+
expect( tree.children.length ).toBe( 1 );
|
|
102
|
+
|
|
103
|
+
const step = tree.children[0];
|
|
104
|
+
expect( step.event ).toBe( 'step' );
|
|
105
|
+
expect( step.stepId ).toBe( 's1' );
|
|
106
|
+
expect( step.startedAt ).toBe( 2000 );
|
|
107
|
+
expect( step.endedAt ).toBe( 4000 );
|
|
108
|
+
expect( step.output ).toEqual( { done: true } );
|
|
109
|
+
expect( step.children.length ).toBe( 1 );
|
|
110
|
+
expect( step.children[0].lib ).toBe( 'tool' );
|
|
111
|
+
expect( step.children[0].timestamp ).toBe( 3000 );
|
|
112
|
+
|
|
113
|
+
rmSync( tmp, { recursive: true, force: true } );
|
|
114
|
+
} );
|
|
115
|
+
} );
|
|
116
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import traverseModule from '@babel/traverse';
|
|
2
|
+
import {
|
|
3
|
+
buildWorkflowNameMap,
|
|
4
|
+
getLocalNameFromDestructuredProperty,
|
|
5
|
+
isStepsPath,
|
|
6
|
+
isWorkflowPath,
|
|
7
|
+
buildStepsNameMap,
|
|
8
|
+
toAbsolutePath
|
|
9
|
+
} from './tools.js';
|
|
10
|
+
import {
|
|
11
|
+
isCallExpression,
|
|
12
|
+
isIdentifier,
|
|
13
|
+
isImportDefaultSpecifier,
|
|
14
|
+
isImportSpecifier,
|
|
15
|
+
isObjectPattern,
|
|
16
|
+
isObjectProperty,
|
|
17
|
+
isStringLiteral,
|
|
18
|
+
isVariableDeclaration
|
|
19
|
+
} from '@babel/types';
|
|
20
|
+
|
|
21
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
22
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Collect and strip target imports and requires from an AST, producing
|
|
26
|
+
* step/workflow import mappings for later rewrites.
|
|
27
|
+
*
|
|
28
|
+
* Mutates the AST by removing matching import declarations and require declarators.
|
|
29
|
+
*
|
|
30
|
+
* @param {import('@babel/types').File} ast - Parsed file AST.
|
|
31
|
+
* @param {string} fileDir - Absolute directory of the file represented by `ast`.
|
|
32
|
+
* @param {{ stepsNameCache: Map<string,Map<string,string>>, workflowNameCache: Map<string,{default:(string|null),named:Map<string,string>}> }} caches
|
|
33
|
+
* Resolved-name caches to avoid re-reading same modules.
|
|
34
|
+
* @returns {{ stepImports: Array<{localName:string,stepName:string}>,
|
|
35
|
+
* flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
|
|
36
|
+
*/
|
|
37
|
+
export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache } ) {
|
|
38
|
+
const stepImports = [];
|
|
39
|
+
const flowImports = [];
|
|
40
|
+
|
|
41
|
+
traverse( ast, {
|
|
42
|
+
ImportDeclaration: path => {
|
|
43
|
+
const src = path.node.source.value;
|
|
44
|
+
// Ignore other imports
|
|
45
|
+
if ( !isStepsPath( src ) && !isWorkflowPath( src ) ) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const absolutePath = toAbsolutePath( fileDir, src );
|
|
50
|
+
if ( isStepsPath( src ) ) {
|
|
51
|
+
const nameMap = buildStepsNameMap( absolutePath, stepsNameCache );
|
|
52
|
+
for ( const s of path.node.specifiers.filter( s => isImportSpecifier( s ) ) ) {
|
|
53
|
+
const importedName = s.imported.name;
|
|
54
|
+
const localName = s.local.name;
|
|
55
|
+
const stepName = nameMap.get( importedName );
|
|
56
|
+
if ( stepName ) {
|
|
57
|
+
stepImports.push( { localName, stepName } );
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if ( isWorkflowPath( src ) ) {
|
|
62
|
+
const { named, default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
63
|
+
for ( const s of path.node.specifiers ) {
|
|
64
|
+
if ( isImportDefaultSpecifier( s ) ) {
|
|
65
|
+
const localName = s.local.name;
|
|
66
|
+
flowImports.push( { localName, workflowName: defName ?? localName } );
|
|
67
|
+
} else if ( isImportSpecifier( s ) ) {
|
|
68
|
+
const importedName = s.imported.name;
|
|
69
|
+
const localName = s.local.name;
|
|
70
|
+
const workflowName = named.get( importedName );
|
|
71
|
+
if ( workflowName ) {
|
|
72
|
+
flowImports.push( { localName, workflowName } );
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
path.remove();
|
|
78
|
+
},
|
|
79
|
+
VariableDeclarator: path => {
|
|
80
|
+
const init = path.node.init;
|
|
81
|
+
// Not a require call
|
|
82
|
+
if ( !isCallExpression( init ) ) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Different callee
|
|
86
|
+
if ( !isIdentifier( init.callee, { name: 'require' } ) ) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const firstArgument = init.arguments[0];
|
|
90
|
+
// Dynamic require is not supported
|
|
91
|
+
if ( !isStringLiteral( firstArgument ) ) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const req = firstArgument.value;
|
|
96
|
+
// Must be steps/workflows module
|
|
97
|
+
if ( !isStepsPath( req ) && !isWorkflowPath( req ) ) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const absolutePath = toAbsolutePath( fileDir, req );
|
|
102
|
+
if ( isStepsPath( req ) && isObjectPattern( path.node.id ) ) {
|
|
103
|
+
const nameMap = buildStepsNameMap( absolutePath, stepsNameCache );
|
|
104
|
+
for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
|
|
105
|
+
const importedName = prop.key.name;
|
|
106
|
+
const localName = getLocalNameFromDestructuredProperty( prop );
|
|
107
|
+
if ( localName ) {
|
|
108
|
+
const stepName = nameMap.get( importedName );
|
|
109
|
+
if ( stepName ) {
|
|
110
|
+
stepImports.push( { localName, stepName } );
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
|
|
115
|
+
path.parentPath.remove();
|
|
116
|
+
} else {
|
|
117
|
+
path.remove();
|
|
118
|
+
}
|
|
119
|
+
} else if ( isWorkflowPath( req ) && isIdentifier( path.node.id ) ) {
|
|
120
|
+
const { default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
121
|
+
const localName = path.node.id.name;
|
|
122
|
+
flowImports.push( { localName, workflowName: defName ?? localName } );
|
|
123
|
+
if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
|
|
124
|
+
path.parentPath.remove();
|
|
125
|
+
} else {
|
|
126
|
+
path.remove();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
return { stepImports, flowImports };
|
|
133
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { parse } from './tools.js';
|
|
6
|
+
import collectTargetImports from './collect_target_imports.js';
|
|
7
|
+
|
|
8
|
+
function makeAst( source, filename ) {
|
|
9
|
+
return parse( source, filename );
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe( 'collect_target_imports', () => {
|
|
13
|
+
it( 'collects ESM imports for steps and workflows and flags changes', () => {
|
|
14
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-esm-' ) );
|
|
15
|
+
writeFileSync( join( dir, 'steps.js' ), [
|
|
16
|
+
'export const StepA = step({ name: "step.a" })',
|
|
17
|
+
'export const StepB = step({ name: "step.b" })'
|
|
18
|
+
].join( '\n' ) );
|
|
19
|
+
writeFileSync( join( dir, 'workflow.js' ), [
|
|
20
|
+
'export const FlowA = workflow({ name: "flow.a" })',
|
|
21
|
+
'export default workflow({ name: "flow.def" })'
|
|
22
|
+
].join( '\n' ) );
|
|
23
|
+
|
|
24
|
+
const source = [
|
|
25
|
+
'import { StepA } from "./steps.js";',
|
|
26
|
+
'import WF, { FlowA } from "./workflow.js";',
|
|
27
|
+
'const x = 1;'
|
|
28
|
+
].join( '\n' );
|
|
29
|
+
|
|
30
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
31
|
+
const { stepImports, flowImports } = collectTargetImports(
|
|
32
|
+
ast,
|
|
33
|
+
dir,
|
|
34
|
+
{ stepsNameCache: new Map(), workflowNameCache: new Map() }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect( stepImports ).toEqual( [ { localName: 'StepA', stepName: 'step.a' } ] );
|
|
38
|
+
expect( flowImports ).toEqual( [
|
|
39
|
+
{ localName: 'WF', workflowName: 'flow.def' },
|
|
40
|
+
{ localName: 'FlowA', workflowName: 'flow.a' }
|
|
41
|
+
] );
|
|
42
|
+
// Import declarations should have been removed
|
|
43
|
+
expect( ast.program.body.find( n => n.type === 'ImportDeclaration' ) ).toBeUndefined();
|
|
44
|
+
|
|
45
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'collects CJS requires and removes declarators (steps + default workflow)', () => {
|
|
49
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-' ) );
|
|
50
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: "step.b" })\n' );
|
|
51
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: "flow.c" })\n' );
|
|
52
|
+
|
|
53
|
+
const source = [
|
|
54
|
+
'const { StepB } = require("./steps.js");',
|
|
55
|
+
'const WF = require("./workflow.js");',
|
|
56
|
+
'const obj = {};'
|
|
57
|
+
].join( '\n' );
|
|
58
|
+
|
|
59
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
60
|
+
const { stepImports, flowImports } = collectTargetImports(
|
|
61
|
+
ast,
|
|
62
|
+
dir,
|
|
63
|
+
{ stepsNameCache: new Map(), workflowNameCache: new Map() }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect( stepImports ).toEqual( [ { localName: 'StepB', stepName: 'step.b' } ] );
|
|
67
|
+
expect( flowImports ).toEqual( [ { localName: 'WF', workflowName: 'flow.c' } ] );
|
|
68
|
+
// All require-based declarators should have been removed (only non-require decls may remain)
|
|
69
|
+
const hasRequireDecl = ast.program.body.some( n =>
|
|
70
|
+
n.type === 'VariableDeclaration' && n.declarations.some( d => d.init && d.init.type === 'CallExpression' )
|
|
71
|
+
);
|
|
72
|
+
expect( hasRequireDecl ).toBe( false );
|
|
73
|
+
|
|
74
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
75
|
+
} );
|
|
76
|
+
} );
|
|
77
|
+
|