@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.
Files changed (45) hide show
  1. package/README.md +85 -59
  2. package/package.json +10 -3
  3. package/src/configs.js +1 -1
  4. package/src/consts.js +4 -3
  5. package/src/errors.js +11 -0
  6. package/src/index.d.ts +302 -30
  7. package/src/index.js +3 -2
  8. package/src/interface/metadata.js +3 -3
  9. package/src/interface/step.js +18 -4
  10. package/src/interface/utils.js +41 -4
  11. package/src/interface/utils.spec.js +71 -0
  12. package/src/interface/validations/ajv_provider.js +3 -0
  13. package/src/interface/validations/runtime.js +69 -0
  14. package/src/interface/validations/runtime.spec.js +50 -0
  15. package/src/interface/validations/static.js +67 -0
  16. package/src/interface/validations/static.spec.js +101 -0
  17. package/src/interface/webhook.js +15 -14
  18. package/src/interface/workflow.js +45 -40
  19. package/src/internal_activities/index.js +16 -5
  20. package/src/worker/catalog_workflow/catalog.js +105 -0
  21. package/src/worker/catalog_workflow/index.js +21 -0
  22. package/src/worker/catalog_workflow/index.spec.js +139 -0
  23. package/src/worker/catalog_workflow/workflow.js +13 -0
  24. package/src/worker/index.js +41 -5
  25. package/src/worker/interceptors/activity.js +3 -2
  26. package/src/worker/internal_utils.js +60 -0
  27. package/src/worker/internal_utils.spec.js +134 -0
  28. package/src/worker/loader.js +30 -44
  29. package/src/worker/loader.spec.js +68 -0
  30. package/src/worker/sinks.js +2 -1
  31. package/src/worker/tracer/index.js +35 -3
  32. package/src/worker/tracer/index.test.js +115 -0
  33. package/src/worker/tracer/tracer_tree.js +29 -5
  34. package/src/worker/tracer/tracer_tree.test.js +116 -0
  35. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +133 -0
  36. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +77 -0
  37. package/src/worker/webpack_loaders/workflow_rewriter/consts.js +3 -0
  38. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +58 -0
  39. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +129 -0
  40. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +70 -0
  41. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +33 -0
  42. package/src/worker/webpack_loaders/workflow_rewriter/tools.js +245 -0
  43. package/src/worker/webpack_loaders/workflow_rewriter/tools.spec.js +144 -0
  44. package/src/errors.d.ts +0 -3
  45. 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
+ } );
@@ -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: 'core', ...args } ), { workflowId, workflowType, ...memo } );
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
- export function trace( { lib, event, input, output } ) {
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 ) { return; }
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 ) ) { mkdirSync( outputDir, { recursive: true } ); }
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
- const timestampAscSort = ( a, b ) => a.timestamp > b.timestamp ? -1 : 1;
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
- const coreLib = 'core';
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 === coreLib ) ) {
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 !== coreLib ) ) {
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
+
@@ -0,0 +1,3 @@
1
+ export const NodeType = {
2
+ CONST: 'const'
3
+ };