@output.ai/core 0.5.2 → 0.5.3
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/package.json +1 -1
- package/src/interface/workflow.js +1 -0
- package/src/tracing/tools/build_trace_tree.js +5 -1
- package/src/tracing/tools/build_trace_tree.spec.js +11 -0
- package/src/worker/catalog_workflow/workflow.js +9 -3
- package/src/worker/index.js +7 -39
- package/src/worker/index.spec.js +13 -31
- package/src/worker/interceptors/workflow.js +1 -1
- package/src/worker/shutdown.js +26 -0
- package/src/worker/shutdown.spec.js +82 -0
- package/src/worker/start_catalog.js +36 -0
- package/src/worker/start_catalog.spec.js +116 -0
- package/src/worker/webpack_loaders/tools.js +34 -4
- package/src/worker/webpack_loaders/tools.spec.js +4 -1
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +107 -68
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +251 -1
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -4
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +48 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +2 -1
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +3 -3
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +22 -0
package/package.json
CHANGED
|
@@ -80,6 +80,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
80
80
|
invokeStep: async ( stepName, input, options ) => steps[`${name}#${stepName}`]( input, options ),
|
|
81
81
|
invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
|
|
82
82
|
invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${name}#${evaluatorName}`]( input, options ),
|
|
83
|
+
invokeSharedEvaluator: async ( evaluatorName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${evaluatorName}`]( input, options ),
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Start a child workflow
|
|
@@ -23,7 +23,7 @@ const createEntry = id => ( {
|
|
|
23
23
|
kind: '',
|
|
24
24
|
name: '',
|
|
25
25
|
startedAt: 0,
|
|
26
|
-
endedAt:
|
|
26
|
+
endedAt: null,
|
|
27
27
|
input: undefined,
|
|
28
28
|
output: undefined,
|
|
29
29
|
error: undefined,
|
|
@@ -75,5 +75,9 @@ export default entries => {
|
|
|
75
75
|
if ( !rootNode ) {
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
|
+
if ( !rootNode.endedAt ) {
|
|
79
|
+
rootNode.output = '<<Workflow did not finish yet. If this workflows is supposed to have been completed already, \
|
|
80
|
+
this can indicate it timed out or was interrupted.>>';
|
|
81
|
+
}
|
|
78
82
|
return rootNode;
|
|
79
83
|
};
|
|
@@ -6,6 +6,17 @@ describe( 'build_trace_tree', () => {
|
|
|
6
6
|
expect( buildTraceTree( [] ) ).toBeNull();
|
|
7
7
|
} );
|
|
8
8
|
|
|
9
|
+
it( 'sets root output with a fixed message when workflow has no end/error phase yet', () => {
|
|
10
|
+
const entries = [
|
|
11
|
+
{ kind: 'workflow', id: 'wf', parentId: undefined, phase: 'start', name: 'wf', details: {}, timestamp: 1000 }
|
|
12
|
+
];
|
|
13
|
+
const result = buildTraceTree( entries );
|
|
14
|
+
expect( result ).not.toBeNull();
|
|
15
|
+
expect( result.output ).toBe( '<<Workflow did not finish yet. If this workflows is supposed to have been completed already, \
|
|
16
|
+
this can indicate it timed out or was interrupted.>>' );
|
|
17
|
+
expect( result.endedAt ).toBeNull();
|
|
18
|
+
} );
|
|
19
|
+
|
|
9
20
|
it( 'returns null when there is no root (all entries have parentId)', () => {
|
|
10
21
|
const entries = [
|
|
11
22
|
{ id: 'a', parentId: 'x', phase: 'start', name: 'a', timestamp: 1 },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineQuery, setHandler, condition } from '@temporalio/workflow';
|
|
1
|
+
import { defineQuery, setHandler, condition, defineUpdate } from '@temporalio/workflow';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* This is a special workflow, unique to each worker, which holds the meta information of all other workflows in that worker.
|
|
@@ -8,7 +8,13 @@ import { defineQuery, setHandler, condition } from '@temporalio/workflow';
|
|
|
8
8
|
* @param {object} catalog - The catalog information
|
|
9
9
|
*/
|
|
10
10
|
export default async function catalogWorkflow( catalog ) {
|
|
11
|
+
const state = { canEnd: false };
|
|
12
|
+
|
|
11
13
|
setHandler( defineQuery( 'get' ), () => catalog );
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
// Listen to this update to complete the workflow
|
|
16
|
+
setHandler( defineUpdate( 'complete' ), () => state.canEnd = true );
|
|
17
|
+
|
|
18
|
+
// Wait indefinitely, until the state changes
|
|
19
|
+
await condition( () => state.canEnd );
|
|
14
20
|
};
|
package/src/worker/index.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { Worker, NativeConnection } from '@temporalio/worker';
|
|
2
|
-
import { Client } from '@temporalio/client';
|
|
3
|
-
import { WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
4
2
|
import * as configs from './configs.js';
|
|
5
3
|
import { loadActivities, loadWorkflows, createWorkflowsEntryPoint } from './loader.js';
|
|
6
4
|
import { sinks } from './sinks.js';
|
|
7
5
|
import { createCatalog } from './catalog_workflow/index.js';
|
|
8
6
|
import { init as initTracing } from '#tracing';
|
|
9
|
-
import { WORKFLOW_CATALOG } from '#consts';
|
|
10
7
|
import { webpackConfigHook } from './bundler_options.js';
|
|
11
8
|
import { initInterceptors } from './interceptors.js';
|
|
12
9
|
import { createChildLogger } from '#logger';
|
|
10
|
+
import { registerShutdown } from './shutdown.js';
|
|
11
|
+
import { startCatalog } from './start_catalog.js';
|
|
13
12
|
|
|
14
13
|
const log = createChildLogger( 'Worker' );
|
|
15
14
|
|
|
@@ -22,7 +21,6 @@ const callerDir = process.argv[2];
|
|
|
22
21
|
apiKey,
|
|
23
22
|
namespace,
|
|
24
23
|
taskQueue,
|
|
25
|
-
catalogId,
|
|
26
24
|
maxConcurrentWorkflowTaskExecutions,
|
|
27
25
|
maxConcurrentActivityTaskExecutions,
|
|
28
26
|
maxCachedWorkflows,
|
|
@@ -66,45 +64,15 @@ const callerDir = process.argv[2];
|
|
|
66
64
|
bundlerOptions: { webpackConfigHook }
|
|
67
65
|
} );
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
await new Client( { connection, namespace } ).workflow.start( WORKFLOW_CATALOG, {
|
|
71
|
-
taskQueue,
|
|
72
|
-
workflowId: catalogId, // use the name of the task queue as the catalog name, ensuring uniqueness
|
|
73
|
-
workflowIdConflictPolicy: WorkflowIdConflictPolicy.TERMINATE_EXISTING,
|
|
74
|
-
args: [ catalog ]
|
|
75
|
-
} );
|
|
67
|
+
registerShutdown( { worker, log } );
|
|
76
68
|
|
|
77
69
|
log.info( 'Running worker...' );
|
|
70
|
+
await Promise.all( [ worker.run(), startCatalog( { connection, namespace, catalog } ) ] );
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
// If running output-worker directly with npx, 2 signals are recieved in
|
|
81
|
-
// rapid succession, and users see the force quit message.
|
|
82
|
-
const FORCE_QUIT_GRACE_MS = 1000;
|
|
83
|
-
const state = { isShuttingDown: false, shutdownStartedAt: null };
|
|
84
|
-
|
|
85
|
-
const shutdown = signal => {
|
|
86
|
-
if ( state.isShuttingDown ) {
|
|
87
|
-
const elapsed = Date.now() - state.shutdownStartedAt;
|
|
88
|
-
if ( elapsed < FORCE_QUIT_GRACE_MS ) {
|
|
89
|
-
return; // ignore rapid duplicate signals
|
|
90
|
-
}
|
|
91
|
-
log.warn( 'Force quitting...' );
|
|
92
|
-
process.exit( 1 );
|
|
93
|
-
}
|
|
94
|
-
state.isShuttingDown = true;
|
|
95
|
-
state.shutdownStartedAt = Date.now();
|
|
96
|
-
log.info( 'Shutting down...', { signal } );
|
|
97
|
-
worker.shutdown();
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
process.on( 'SIGTERM', () => shutdown( 'SIGTERM' ) );
|
|
101
|
-
process.on( 'SIGINT', () => shutdown( 'SIGINT' ) );
|
|
102
|
-
|
|
103
|
-
await worker.run();
|
|
104
|
-
log.info( 'Worker stopped.' );
|
|
105
|
-
|
|
72
|
+
log.info( 'Closing connection...' );
|
|
106
73
|
await connection.close();
|
|
107
|
-
|
|
74
|
+
|
|
75
|
+
log.info( 'Bye' );
|
|
108
76
|
|
|
109
77
|
process.exit( 0 );
|
|
110
78
|
} )().catch( error => {
|
package/src/worker/index.spec.js
CHANGED
|
@@ -40,6 +40,12 @@ vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
|
40
40
|
const initInterceptorsMock = vi.fn().mockReturnValue( [] );
|
|
41
41
|
vi.mock( './interceptors.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
42
42
|
|
|
43
|
+
const startCatalogMock = vi.fn().mockResolvedValue( undefined );
|
|
44
|
+
vi.mock( './start_catalog.js', () => ( { startCatalog: startCatalogMock } ) );
|
|
45
|
+
|
|
46
|
+
const registerShutdownMock = vi.fn();
|
|
47
|
+
vi.mock( './shutdown.js', () => ( { registerShutdown: registerShutdownMock } ) );
|
|
48
|
+
|
|
43
49
|
const runState = { resolve: null };
|
|
44
50
|
const runPromise = new Promise( r => {
|
|
45
51
|
runState.resolve = r;
|
|
@@ -53,22 +59,10 @@ vi.mock( '@temporalio/worker', () => ( {
|
|
|
53
59
|
NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) }
|
|
54
60
|
} ) );
|
|
55
61
|
|
|
56
|
-
const workflowStartMock = vi.fn().mockResolvedValue( undefined );
|
|
57
|
-
vi.mock( '@temporalio/client', () => ( {
|
|
58
|
-
Client: vi.fn().mockImplementation( () => ( {
|
|
59
|
-
workflow: { start: workflowStartMock }
|
|
60
|
-
} ) )
|
|
61
|
-
} ) );
|
|
62
|
-
|
|
63
|
-
vi.mock( '@temporalio/common', () => ( {
|
|
64
|
-
WorkflowIdConflictPolicy: { TERMINATE_EXISTING: 'TERMINATE_EXISTING' }
|
|
65
|
-
} ) );
|
|
66
|
-
|
|
67
62
|
describe( 'worker/index', () => {
|
|
68
63
|
const exitMock = vi.fn();
|
|
69
64
|
const originalArgv = process.argv;
|
|
70
65
|
const originalExit = process.exit;
|
|
71
|
-
const originalOn = process.on;
|
|
72
66
|
|
|
73
67
|
beforeEach( () => {
|
|
74
68
|
vi.clearAllMocks();
|
|
@@ -79,13 +73,11 @@ describe( 'worker/index', () => {
|
|
|
79
73
|
afterEach( () => {
|
|
80
74
|
process.argv = originalArgv;
|
|
81
75
|
process.exit = originalExit;
|
|
82
|
-
process.on = originalOn;
|
|
83
76
|
configValues.apiKey = undefined;
|
|
84
77
|
} );
|
|
85
78
|
|
|
86
79
|
it( 'loads configs, workflows, activities and creates worker with correct options', async () => {
|
|
87
80
|
const { Worker, NativeConnection } = await import( '@temporalio/worker' );
|
|
88
|
-
const { Client } = await import( '@temporalio/client' );
|
|
89
81
|
const { init: initTracing } = await import( '#tracing' );
|
|
90
82
|
|
|
91
83
|
import( './index.js' );
|
|
@@ -114,12 +106,11 @@ describe( 'worker/index', () => {
|
|
|
114
106
|
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
115
107
|
} ) );
|
|
116
108
|
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {} } );
|
|
117
|
-
expect(
|
|
118
|
-
expect(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
args: [ { workflows: [], activities: {} } ]
|
|
109
|
+
expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
|
|
110
|
+
expect( startCatalogMock ).toHaveBeenCalledWith( {
|
|
111
|
+
connection: mockConnection,
|
|
112
|
+
namespace: configValues.namespace,
|
|
113
|
+
catalog: { workflows: [], activities: {} }
|
|
123
114
|
} );
|
|
124
115
|
|
|
125
116
|
runState.resolve();
|
|
@@ -145,23 +136,14 @@ describe( 'worker/index', () => {
|
|
|
145
136
|
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
|
|
146
137
|
} );
|
|
147
138
|
|
|
148
|
-
it( '
|
|
149
|
-
const onMock = vi.fn();
|
|
150
|
-
process.on = onMock;
|
|
139
|
+
it( 'calls registerShutdown with worker and log', async () => {
|
|
151
140
|
vi.resetModules();
|
|
152
141
|
|
|
153
142
|
import( './index.js' );
|
|
154
143
|
|
|
155
144
|
await vi.waitFor( () => {
|
|
156
|
-
expect(
|
|
157
|
-
expect( onMock ).toHaveBeenCalledWith( 'SIGINT', expect.any( Function ) );
|
|
145
|
+
expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
|
|
158
146
|
} );
|
|
159
|
-
|
|
160
|
-
const sigtermHandler = onMock.mock.calls.find( c => c[0] === 'SIGTERM' )?.[1];
|
|
161
|
-
expect( sigtermHandler ).toBeDefined();
|
|
162
|
-
sigtermHandler();
|
|
163
|
-
expect( shutdownMock ).toHaveBeenCalled();
|
|
164
|
-
|
|
165
147
|
runState.resolve();
|
|
166
148
|
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
|
|
167
149
|
} );
|
|
@@ -49,7 +49,7 @@ class WorkflowExecutionInterceptor {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
sinks.trace.addWorkflowEventError( error );
|
|
52
|
-
const failure = new ApplicationFailure( error.message, error.constructor.name );
|
|
52
|
+
const failure = new ApplicationFailure( error.message, error.constructor.name, undefined, undefined, error );
|
|
53
53
|
|
|
54
54
|
/*
|
|
55
55
|
* If intercepted error has metadata, set it to .details property of Temporal's ApplicationFailure instance.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const FORCE_QUIT_GRACE_MS = 1000;
|
|
2
|
+
|
|
3
|
+
export const registerShutdown = ( { worker, log } ) => {
|
|
4
|
+
const state = { isShuttingDown: false, shutdownStartedAt: null };
|
|
5
|
+
|
|
6
|
+
const shutdown = signal => {
|
|
7
|
+
if ( state.isShuttingDown ) {
|
|
8
|
+
const elapsed = Date.now() - state.shutdownStartedAt;
|
|
9
|
+
|
|
10
|
+
// If running with npx, 2 kill signals are received in rapid succession,
|
|
11
|
+
// this ignores the second interruption when it is right after the first.
|
|
12
|
+
if ( elapsed < FORCE_QUIT_GRACE_MS ) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
log.warn( 'Force quitting...' );
|
|
16
|
+
process.exit( 1 );
|
|
17
|
+
}
|
|
18
|
+
state.isShuttingDown = true;
|
|
19
|
+
state.shutdownStartedAt = Date.now();
|
|
20
|
+
log.info( 'Shutting down...', { signal } );
|
|
21
|
+
worker.shutdown();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
process.on( 'SIGTERM', () => shutdown( 'SIGTERM' ) );
|
|
25
|
+
process.on( 'SIGINT', () => shutdown( 'SIGINT' ) );
|
|
26
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { registerShutdown } from './shutdown.js';
|
|
3
|
+
|
|
4
|
+
describe( 'worker/shutdown', () => {
|
|
5
|
+
const mockLog = { info: vi.fn(), warn: vi.fn() };
|
|
6
|
+
const shutdownMock = vi.fn();
|
|
7
|
+
const mockWorker = { shutdown: shutdownMock };
|
|
8
|
+
const onHandlers = {};
|
|
9
|
+
const exitMock = vi.fn();
|
|
10
|
+
const originalOn = process.on;
|
|
11
|
+
const originalExit = process.exit;
|
|
12
|
+
|
|
13
|
+
beforeEach( () => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
Object.keys( onHandlers ).forEach( k => delete onHandlers[k] );
|
|
16
|
+
process.on = vi.fn( ( event, handler ) => {
|
|
17
|
+
onHandlers[event] = handler;
|
|
18
|
+
} );
|
|
19
|
+
process.exit = exitMock;
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
afterEach( () => {
|
|
23
|
+
process.on = originalOn;
|
|
24
|
+
process.exit = originalExit;
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'registers SIGTERM and SIGINT handlers', () => {
|
|
28
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
29
|
+
|
|
30
|
+
expect( process.on ).toHaveBeenCalledWith( 'SIGTERM', expect.any( Function ) );
|
|
31
|
+
expect( process.on ).toHaveBeenCalledWith( 'SIGINT', expect.any( Function ) );
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'on first signal: logs, calls worker.shutdown(), does not exit', () => {
|
|
35
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
36
|
+
|
|
37
|
+
onHandlers.SIGTERM();
|
|
38
|
+
|
|
39
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Shutting down...', { signal: 'SIGTERM' } );
|
|
40
|
+
expect( shutdownMock ).toHaveBeenCalledTimes( 1 );
|
|
41
|
+
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
42
|
+
expect( exitMock ).not.toHaveBeenCalled();
|
|
43
|
+
} );
|
|
44
|
+
|
|
45
|
+
it( 'on first SIGINT: logs with SIGINT', () => {
|
|
46
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
47
|
+
|
|
48
|
+
onHandlers.SIGINT();
|
|
49
|
+
|
|
50
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Shutting down...', { signal: 'SIGINT' } );
|
|
51
|
+
expect( shutdownMock ).toHaveBeenCalledTimes( 1 );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'on second signal within grace period: ignores (no force quit)', () => {
|
|
55
|
+
vi.useFakeTimers();
|
|
56
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
57
|
+
|
|
58
|
+
onHandlers.SIGTERM();
|
|
59
|
+
onHandlers.SIGINT();
|
|
60
|
+
|
|
61
|
+
expect( mockLog.info ).toHaveBeenCalledTimes( 1 );
|
|
62
|
+
expect( shutdownMock ).toHaveBeenCalledTimes( 1 );
|
|
63
|
+
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
64
|
+
expect( exitMock ).not.toHaveBeenCalled();
|
|
65
|
+
|
|
66
|
+
vi.useRealTimers();
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
it( 'on second signal after grace period: logs force quit and exits with 1', () => {
|
|
70
|
+
vi.useFakeTimers();
|
|
71
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
72
|
+
|
|
73
|
+
onHandlers.SIGTERM();
|
|
74
|
+
vi.advanceTimersByTime( 1001 );
|
|
75
|
+
onHandlers.SIGINT();
|
|
76
|
+
|
|
77
|
+
expect( mockLog.warn ).toHaveBeenCalledWith( 'Force quitting...' );
|
|
78
|
+
expect( exitMock ).toHaveBeenCalledWith( 1 );
|
|
79
|
+
|
|
80
|
+
vi.useRealTimers();
|
|
81
|
+
} );
|
|
82
|
+
} );
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Client, WorkflowNotFoundError } from '@temporalio/client';
|
|
2
|
+
import { WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
3
|
+
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
|
+
import { catalogId, taskQueue } from './configs.js';
|
|
5
|
+
import { createChildLogger } from '#logger';
|
|
6
|
+
|
|
7
|
+
const log = createChildLogger( 'Catalog' );
|
|
8
|
+
|
|
9
|
+
export const startCatalog = async ( { connection, namespace, catalog } ) => {
|
|
10
|
+
const client = new Client( { connection, namespace } );
|
|
11
|
+
const catalogWorkflowHandle = client.workflow.getHandle( catalogId );
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const catalogWorkflowDescription = await catalogWorkflowHandle.describe();
|
|
15
|
+
if ( !catalogWorkflowDescription.closeTime ) {
|
|
16
|
+
log.info( 'Completing previous catalog workflow...' );
|
|
17
|
+
await catalogWorkflowHandle.executeUpdate( 'complete', { args: [] } );
|
|
18
|
+
}
|
|
19
|
+
} catch ( error ) {
|
|
20
|
+
// When "not found", it's either a cold start or the catalog was already stopped/terminated, ignore it.
|
|
21
|
+
// Otherwise, create a log and try the next operation:
|
|
22
|
+
// A. If the workflow is still running, the start() will fail and throw;
|
|
23
|
+
// B. If the workflow is no running, the start() will succeed, and the error was transient;
|
|
24
|
+
if ( !( error instanceof WorkflowNotFoundError ) ) {
|
|
25
|
+
log.warn( 'Error interacting with previous catalog workflow', { error } );
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
log.info( 'Starting catalog workflow...' );
|
|
30
|
+
await client.workflow.start( WORKFLOW_CATALOG, {
|
|
31
|
+
taskQueue,
|
|
32
|
+
workflowId: catalogId, // use the name of the task queue as the catalog name, ensuring uniqueness
|
|
33
|
+
workflowIdConflictPolicy: WorkflowIdConflictPolicy.FAIL,
|
|
34
|
+
args: [ catalog ]
|
|
35
|
+
} );
|
|
36
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { WorkflowNotFoundError } from '@temporalio/client';
|
|
3
|
+
|
|
4
|
+
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
5
|
+
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
6
|
+
|
|
7
|
+
vi.mock( '#consts', () => ( { WORKFLOW_CATALOG: 'catalog' } ) );
|
|
8
|
+
|
|
9
|
+
const catalogId = 'test-catalog';
|
|
10
|
+
const taskQueue = 'test-queue';
|
|
11
|
+
vi.mock( './configs.js', () => ( { catalogId, taskQueue } ) );
|
|
12
|
+
|
|
13
|
+
const describeMock = vi.fn();
|
|
14
|
+
const executeUpdateMock = vi.fn();
|
|
15
|
+
const workflowStartMock = vi.fn().mockResolvedValue( undefined );
|
|
16
|
+
vi.mock( '@temporalio/client', async importOriginal => {
|
|
17
|
+
const actual = await importOriginal();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
Client: vi.fn().mockImplementation( () => ( {
|
|
21
|
+
workflow: {
|
|
22
|
+
start: workflowStartMock,
|
|
23
|
+
getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
|
|
24
|
+
}
|
|
25
|
+
} ) )
|
|
26
|
+
};
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
vi.mock( '@temporalio/common', () => ( { WorkflowIdConflictPolicy: { FAIL: 'FAIL' } } ) );
|
|
30
|
+
|
|
31
|
+
describe( 'worker/start_catalog', () => {
|
|
32
|
+
const mockConnection = {};
|
|
33
|
+
const namespace = 'default';
|
|
34
|
+
const catalog = { workflows: [], activities: {} };
|
|
35
|
+
|
|
36
|
+
beforeEach( () => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
workflowStartMock.mockResolvedValue( undefined );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'when previous catalog still running: completes it then starts catalog workflow', async () => {
|
|
42
|
+
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
43
|
+
executeUpdateMock.mockResolvedValue( undefined );
|
|
44
|
+
|
|
45
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
46
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
47
|
+
|
|
48
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
49
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
50
|
+
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
51
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
52
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
53
|
+
taskQueue,
|
|
54
|
+
workflowId: catalogId,
|
|
55
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
56
|
+
args: [ catalog ]
|
|
57
|
+
} );
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
it( 'when no previous catalog: ignores and starts catalog workflow', async () => {
|
|
61
|
+
describeMock.mockRejectedValue( new WorkflowNotFoundError( 'not found' ) );
|
|
62
|
+
|
|
63
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
64
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
65
|
+
|
|
66
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
67
|
+
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
68
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
69
|
+
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
70
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
71
|
+
taskQueue,
|
|
72
|
+
workflowId: catalogId,
|
|
73
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
74
|
+
args: [ catalog ]
|
|
75
|
+
} );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'when previous catalog already closed: skips complete and starts catalog workflow', async () => {
|
|
79
|
+
describeMock.mockResolvedValue( { closeTime: '2024-01-01T00:00:00Z' } );
|
|
80
|
+
|
|
81
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
82
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
83
|
+
|
|
84
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
85
|
+
expect( mockLog.info ).not.toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
86
|
+
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
87
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
88
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
89
|
+
taskQueue,
|
|
90
|
+
workflowId: catalogId,
|
|
91
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
92
|
+
args: [ catalog ]
|
|
93
|
+
} );
|
|
94
|
+
} );
|
|
95
|
+
|
|
96
|
+
it( 'when describe or complete fails with other error: logs warn and still starts catalog workflow', async () => {
|
|
97
|
+
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
98
|
+
executeUpdateMock.mockRejectedValue( new Error( 'Connection refused' ) );
|
|
99
|
+
|
|
100
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
101
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
102
|
+
|
|
103
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
104
|
+
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
105
|
+
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error interacting with previous catalog workflow', {
|
|
106
|
+
error: expect.any( Error )
|
|
107
|
+
} );
|
|
108
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
109
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
110
|
+
taskQueue,
|
|
111
|
+
workflowId: catalogId,
|
|
112
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
113
|
+
args: [ catalog ]
|
|
114
|
+
} );
|
|
115
|
+
} );
|
|
116
|
+
} );
|
|
@@ -156,8 +156,13 @@ export const isAnyStepsPath = value =>
|
|
|
156
156
|
* @param {string} value - Module path or request string.
|
|
157
157
|
* @returns {boolean} True if it matches an evaluators path.
|
|
158
158
|
*/
|
|
159
|
-
export const isEvaluatorsPath = value =>
|
|
160
|
-
|
|
159
|
+
export const isEvaluatorsPath = value => {
|
|
160
|
+
// Exclude shared evaluators (paths with ../ or containing /shared/)
|
|
161
|
+
if ( PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value ) ) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return EVALUATORS_FILE_REGEX.test( value ) || EVALUATORS_FOLDER_REGEX.test( value );
|
|
165
|
+
};
|
|
161
166
|
|
|
162
167
|
/**
|
|
163
168
|
* Check if a module specifier or request string points to shared evaluators.
|
|
@@ -174,6 +179,15 @@ export const isSharedEvaluatorsPath = value => {
|
|
|
174
179
|
return PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value );
|
|
175
180
|
};
|
|
176
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Check if a path matches any evaluators pattern (local or shared).
|
|
184
|
+
* Used for validation purposes.
|
|
185
|
+
* @param {string} value - Module path or request string.
|
|
186
|
+
* @returns {boolean} True if it matches any evaluators path pattern.
|
|
187
|
+
*/
|
|
188
|
+
export const isAnyEvaluatorsPath = value =>
|
|
189
|
+
EVALUATORS_FILE_REGEX.test( value ) || EVALUATORS_FOLDER_REGEX.test( value );
|
|
190
|
+
|
|
177
191
|
/**
|
|
178
192
|
* Check if a module specifier or request string points to workflow.js.
|
|
179
193
|
* @param {string} value - Module path or request string.
|
|
@@ -187,7 +201,7 @@ export const isWorkflowPath = value => /(^|\/)workflow\.js$/.test( value );
|
|
|
187
201
|
* @returns {boolean} True if it matches any component file path.
|
|
188
202
|
*/
|
|
189
203
|
export const isComponentFile = value =>
|
|
190
|
-
isAnyStepsPath( value ) ||
|
|
204
|
+
isAnyStepsPath( value ) || isAnyEvaluatorsPath( value ) || isWorkflowPath( value );
|
|
191
205
|
|
|
192
206
|
/**
|
|
193
207
|
* Determine file kind based on its path.
|
|
@@ -199,7 +213,7 @@ export const getFileKind = path => {
|
|
|
199
213
|
if ( isAnyStepsPath( path ) ) {
|
|
200
214
|
return ComponentFile.STEPS;
|
|
201
215
|
}
|
|
202
|
-
if (
|
|
216
|
+
if ( isAnyEvaluatorsPath( path ) ) {
|
|
203
217
|
return ComponentFile.EVALUATORS;
|
|
204
218
|
}
|
|
205
219
|
if ( isWorkflowPath( path ) ) {
|
|
@@ -357,6 +371,22 @@ export const buildEvaluatorsNameMap = ( path, cache ) => buildComponentNameMap(
|
|
|
357
371
|
invalidMessagePrefix: 'Invalid evaluator name in'
|
|
358
372
|
} );
|
|
359
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Build a map from exported shared evaluator identifier to declared evaluator name.
|
|
376
|
+
* Same as buildEvaluatorsNameMap but for shared evaluators.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} path - Absolute path to the shared evaluators module file.
|
|
379
|
+
* @param {Map<string, Map<string,string>>} cache - Cache of computed evaluator name maps.
|
|
380
|
+
* @returns {Map<string,string>} Exported identifier -> evaluator name.
|
|
381
|
+
* @throws {Error} When an evaluator name is invalid (non-static or missing).
|
|
382
|
+
*/
|
|
383
|
+
export const buildSharedEvaluatorsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
384
|
+
path,
|
|
385
|
+
cache,
|
|
386
|
+
calleeName: 'evaluator',
|
|
387
|
+
invalidMessagePrefix: 'Invalid shared evaluator name in'
|
|
388
|
+
} );
|
|
389
|
+
|
|
360
390
|
/**
|
|
361
391
|
* Build a structure with default and named workflow names from a workflow module.
|
|
362
392
|
* Extracts names from `workflow({ name: '...' })` calls.
|
|
@@ -190,11 +190,14 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
190
190
|
expect( isWorkflowPath( 'steps.js' ) ).toBe( false );
|
|
191
191
|
} );
|
|
192
192
|
|
|
193
|
-
it( 'isEvaluatorsPath: matches evaluators.js
|
|
193
|
+
it( 'isEvaluatorsPath: matches local evaluators.js but excludes shared', () => {
|
|
194
194
|
expect( isEvaluatorsPath( 'evaluators.js' ) ).toBe( true );
|
|
195
195
|
expect( isEvaluatorsPath( './evaluators.js' ) ).toBe( true );
|
|
196
196
|
expect( isEvaluatorsPath( '/a/b/evaluators.js' ) ).toBe( true );
|
|
197
197
|
expect( isEvaluatorsPath( './evaluators/quality.js' ) ).toBe( true );
|
|
198
|
+
// Shared evaluators should NOT match (path traversal or /shared/)
|
|
199
|
+
expect( isEvaluatorsPath( '../evaluators.js' ) ).toBe( false );
|
|
200
|
+
expect( isEvaluatorsPath( '../../shared/evaluators/common.js' ) ).toBe( false );
|
|
198
201
|
expect( isEvaluatorsPath( 'evaluators.ts' ) ).toBe( false );
|
|
199
202
|
expect( isEvaluatorsPath( 'steps.js' ) ).toBe( false );
|
|
200
203
|
} );
|
|
@@ -3,12 +3,14 @@ import {
|
|
|
3
3
|
buildWorkflowNameMap,
|
|
4
4
|
getLocalNameFromDestructuredProperty,
|
|
5
5
|
isEvaluatorsPath,
|
|
6
|
+
isSharedEvaluatorsPath,
|
|
6
7
|
isSharedStepsPath,
|
|
7
8
|
isStepsPath,
|
|
8
9
|
isWorkflowPath,
|
|
9
10
|
buildStepsNameMap,
|
|
10
11
|
buildSharedStepsNameMap,
|
|
11
12
|
buildEvaluatorsNameMap,
|
|
13
|
+
buildSharedEvaluatorsNameMap,
|
|
12
14
|
toAbsolutePath
|
|
13
15
|
} from '../tools.js';
|
|
14
16
|
import {
|
|
@@ -25,6 +27,46 @@ import {
|
|
|
25
27
|
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
26
28
|
const traverse = traverseModule.default ?? traverseModule;
|
|
27
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
|
+
|
|
28
70
|
/**
|
|
29
71
|
* Collect and strip target imports and requires from an AST, producing
|
|
30
72
|
* step/workflow import mappings for later rewrites.
|
|
@@ -38,22 +80,28 @@ const traverse = traverseModule.default ?? traverseModule;
|
|
|
38
80
|
* @returns {{ stepImports: Array<{localName:string,stepName:string}>,
|
|
39
81
|
* flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
|
|
40
82
|
*/
|
|
41
|
-
export default function collectTargetImports(
|
|
83
|
+
export default function collectTargetImports(
|
|
84
|
+
ast, fileDir,
|
|
85
|
+
{ stepsNameCache, workflowNameCache, evaluatorsNameCache, sharedStepsNameCache, sharedEvaluatorsNameCache }
|
|
86
|
+
) {
|
|
42
87
|
const stepImports = [];
|
|
43
88
|
const sharedStepImports = [];
|
|
44
89
|
const flowImports = [];
|
|
45
90
|
const evaluatorImports = [];
|
|
91
|
+
const sharedEvaluatorImports = [];
|
|
46
92
|
|
|
47
93
|
traverse( ast, {
|
|
48
94
|
ImportDeclaration: path => {
|
|
49
95
|
const src = path.node.source.value;
|
|
50
96
|
// Ignore other imports
|
|
51
|
-
|
|
97
|
+
const isTargetImport = isStepsPath( src ) || isSharedStepsPath( src ) ||
|
|
98
|
+
isWorkflowPath( src ) || isEvaluatorsPath( src ) || isSharedEvaluatorsPath( src );
|
|
99
|
+
if ( !isTargetImport ) {
|
|
52
100
|
return;
|
|
53
101
|
}
|
|
54
102
|
|
|
55
103
|
const absolutePath = toAbsolutePath( fileDir, src );
|
|
56
|
-
const collectNamedImports = ( match, buildMapFn, cache, targetArr, valueKey ) => {
|
|
104
|
+
const collectNamedImports = ( match, buildMapFn, cache, targetArr, valueKey, fileLabel ) => {
|
|
57
105
|
if ( !match ) {
|
|
58
106
|
return;
|
|
59
107
|
}
|
|
@@ -66,13 +114,19 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
66
114
|
const entry = { localName };
|
|
67
115
|
entry[valueKey] = value;
|
|
68
116
|
targetArr.push( entry );
|
|
117
|
+
} else {
|
|
118
|
+
throw unresolvedImportError( importedName, fileLabel, absolutePath );
|
|
69
119
|
}
|
|
70
120
|
}
|
|
71
121
|
};
|
|
72
122
|
|
|
73
|
-
collectNamedImports( isStepsPath( src ), buildStepsNameMap, stepsNameCache, stepImports, 'stepName' );
|
|
74
|
-
collectNamedImports( isSharedStepsPath( src ), buildSharedStepsNameMap, sharedStepsNameCache, sharedStepImports, 'stepName' );
|
|
75
|
-
collectNamedImports( isEvaluatorsPath( src ), buildEvaluatorsNameMap, evaluatorsNameCache, evaluatorImports, 'evaluatorName' );
|
|
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
|
+
);
|
|
76
130
|
if ( isWorkflowPath( src ) ) {
|
|
77
131
|
const { named, default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
78
132
|
for ( const s of path.node.specifiers ) {
|
|
@@ -85,6 +139,8 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
85
139
|
const workflowName = named.get( importedName );
|
|
86
140
|
if ( workflowName ) {
|
|
87
141
|
flowImports.push( { localName, workflowName } );
|
|
142
|
+
} else {
|
|
143
|
+
throw unresolvedImportError( importedName, 'workflow', absolutePath );
|
|
88
144
|
}
|
|
89
145
|
}
|
|
90
146
|
}
|
|
@@ -93,90 +149,73 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
93
149
|
},
|
|
94
150
|
VariableDeclarator: path => {
|
|
95
151
|
const init = path.node.init;
|
|
96
|
-
// Not a require call
|
|
97
152
|
if ( !isCallExpression( init ) ) {
|
|
98
153
|
return;
|
|
99
154
|
}
|
|
100
|
-
// Different callee
|
|
101
155
|
if ( !isIdentifier( init.callee, { name: 'require' } ) ) {
|
|
102
156
|
return;
|
|
103
157
|
}
|
|
104
158
|
const firstArgument = init.arguments[0];
|
|
105
|
-
// Dynamic require is not supported
|
|
106
159
|
if ( !isStringLiteral( firstArgument ) ) {
|
|
107
160
|
return;
|
|
108
161
|
}
|
|
109
162
|
|
|
110
163
|
const req = firstArgument.value;
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
const isTargetRequire = isStepsPath( req ) || isSharedStepsPath( req ) ||
|
|
165
|
+
isWorkflowPath( req ) || isEvaluatorsPath( req ) || isSharedEvaluatorsPath( req );
|
|
166
|
+
if ( !isTargetRequire ) {
|
|
113
167
|
return;
|
|
114
168
|
}
|
|
115
169
|
|
|
116
170
|
const absolutePath = toAbsolutePath( fileDir, req );
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} else {
|
|
149
|
-
path.remove();
|
|
150
|
-
}
|
|
151
|
-
} else if ( isEvaluatorsPath( req ) && isObjectPattern( path.node.id ) ) {
|
|
152
|
-
const nameMap = buildEvaluatorsNameMap( absolutePath, evaluatorsNameCache );
|
|
153
|
-
for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
|
|
154
|
-
const importedName = prop.key.name;
|
|
155
|
-
const localName = getLocalNameFromDestructuredProperty( prop );
|
|
156
|
-
if ( localName ) {
|
|
157
|
-
const evaluatorName = nameMap.get( importedName );
|
|
158
|
-
if ( evaluatorName ) {
|
|
159
|
-
evaluatorImports.push( { localName, evaluatorName } );
|
|
160
|
-
}
|
|
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'
|
|
161
202
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
path
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 ) ) {
|
|
169
212
|
const { default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
170
213
|
const localName = path.node.id.name;
|
|
171
214
|
flowImports.push( { localName, workflowName: defName ?? localName } );
|
|
172
|
-
|
|
173
|
-
path.parentPath.remove();
|
|
174
|
-
} else {
|
|
175
|
-
path.remove();
|
|
176
|
-
}
|
|
215
|
+
removeRequireDeclarator( path );
|
|
177
216
|
}
|
|
178
217
|
}
|
|
179
218
|
} );
|
|
180
219
|
|
|
181
|
-
return { stepImports, sharedStepImports, evaluatorImports, flowImports };
|
|
220
|
+
return { stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports };
|
|
182
221
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { parse } from '../tools.js';
|
|
@@ -75,5 +75,255 @@ const obj = {};`;
|
|
|
75
75
|
|
|
76
76
|
rmSync( dir, { recursive: true, force: true } );
|
|
77
77
|
} );
|
|
78
|
+
|
|
79
|
+
it( 'throws when ESM import from evaluators.js uses step() instead of evaluator()', () => {
|
|
80
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-esm-mismatch-eval-' ) );
|
|
81
|
+
writeFileSync( join( dir, 'evaluators.js' ), 'export const BadExport = step({ name: \'bad\' });' );
|
|
82
|
+
|
|
83
|
+
const source = 'import { BadExport } from \'./evaluators.js\';';
|
|
84
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
85
|
+
|
|
86
|
+
expect( () => collectTargetImports(
|
|
87
|
+
ast,
|
|
88
|
+
dir,
|
|
89
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
90
|
+
) ).toThrow( /Unresolved import 'BadExport' from evaluators file/ );
|
|
91
|
+
|
|
92
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'throws when ESM import from steps.js uses evaluator() instead of step()', () => {
|
|
96
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-esm-mismatch-step-' ) );
|
|
97
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const BadExport = evaluator({ name: \'bad\' });' );
|
|
98
|
+
|
|
99
|
+
const source = 'import { BadExport } from \'./steps.js\';';
|
|
100
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
101
|
+
|
|
102
|
+
expect( () => collectTargetImports(
|
|
103
|
+
ast,
|
|
104
|
+
dir,
|
|
105
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
106
|
+
) ).toThrow( /Unresolved import 'BadExport' from steps file/ );
|
|
107
|
+
|
|
108
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
it( 'throws when CJS require from evaluators.js uses step() instead of evaluator()', () => {
|
|
112
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-mismatch-eval-' ) );
|
|
113
|
+
writeFileSync( join( dir, 'evaluators.js' ), 'export const BadExport = step({ name: \'bad\' });' );
|
|
114
|
+
|
|
115
|
+
const source = 'const { BadExport } = require( \'./evaluators.js\' );';
|
|
116
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
117
|
+
|
|
118
|
+
expect( () => collectTargetImports(
|
|
119
|
+
ast,
|
|
120
|
+
dir,
|
|
121
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
122
|
+
) ).toThrow( /Unresolved import 'BadExport' from evaluators file/ );
|
|
123
|
+
|
|
124
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
125
|
+
} );
|
|
126
|
+
|
|
127
|
+
it( 'throws when CJS require from steps.js uses evaluator() instead of step()', () => {
|
|
128
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-mismatch-step-' ) );
|
|
129
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const BadExport = evaluator({ name: \'bad\' });' );
|
|
130
|
+
|
|
131
|
+
const source = 'const { BadExport } = require( \'./steps.js\' );';
|
|
132
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
133
|
+
|
|
134
|
+
expect( () => collectTargetImports(
|
|
135
|
+
ast,
|
|
136
|
+
dir,
|
|
137
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
138
|
+
) ).toThrow( /Unresolved import 'BadExport' from steps file/ );
|
|
139
|
+
|
|
140
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
it( 'throws when ESM import from workflow.js has non-workflow export', () => {
|
|
144
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-esm-mismatch-wf-' ) );
|
|
145
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export const helper = () => 42;' );
|
|
146
|
+
|
|
147
|
+
const source = 'import { helper } from \'./workflow.js\';';
|
|
148
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
149
|
+
|
|
150
|
+
expect( () => collectTargetImports(
|
|
151
|
+
ast,
|
|
152
|
+
dir,
|
|
153
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
154
|
+
) ).toThrow( /Unresolved import 'helper' from workflow file/ );
|
|
155
|
+
|
|
156
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
it( 'throws when CJS destructured require from workflow.js has non-workflow export', () => {
|
|
160
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-mismatch-wf-' ) );
|
|
161
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export const helper = () => 42;' );
|
|
162
|
+
|
|
163
|
+
const source = 'const { helper } = require( \'./workflow.js\' );';
|
|
164
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
165
|
+
|
|
166
|
+
expect( () => collectTargetImports(
|
|
167
|
+
ast,
|
|
168
|
+
dir,
|
|
169
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
170
|
+
) ).toThrow( /Unresolved import 'helper' from workflow file/ );
|
|
171
|
+
|
|
172
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
it( 'collects CJS destructured require from workflow.js', () => {
|
|
176
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-wf-destruct-' ) );
|
|
177
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export const FlowX = workflow({ name: \'flow.x\' });' );
|
|
178
|
+
|
|
179
|
+
const source = 'const { FlowX } = require( \'./workflow.js\' );\nconst obj = {};';
|
|
180
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
181
|
+
|
|
182
|
+
const { flowImports } = collectTargetImports(
|
|
183
|
+
ast,
|
|
184
|
+
dir,
|
|
185
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
186
|
+
);
|
|
187
|
+
expect( flowImports ).toEqual( [ { localName: 'FlowX', workflowName: 'flow.x' } ] );
|
|
188
|
+
|
|
189
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
190
|
+
} );
|
|
191
|
+
|
|
192
|
+
it( 'collects ESM shared evaluator imports', () => {
|
|
193
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-esm-shared-eval-' ) );
|
|
194
|
+
mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
|
|
195
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
196
|
+
writeFileSync(
|
|
197
|
+
join( dir, 'shared', 'evaluators', 'common.js' ),
|
|
198
|
+
'export const SharedEval = evaluator({ name: \'shared.eval\' });'
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const source = 'import { SharedEval } from \'../../shared/evaluators/common.js\';';
|
|
202
|
+
const ast = makeAst( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
203
|
+
|
|
204
|
+
const { sharedEvaluatorImports } = collectTargetImports(
|
|
205
|
+
ast,
|
|
206
|
+
join( dir, 'workflows', 'my_workflow' ),
|
|
207
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), sharedEvaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
208
|
+
);
|
|
209
|
+
expect( sharedEvaluatorImports ).toEqual( [ { localName: 'SharedEval', evaluatorName: 'shared.eval' } ] );
|
|
210
|
+
|
|
211
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
212
|
+
} );
|
|
213
|
+
|
|
214
|
+
it( 'collects CJS shared evaluator requires', () => {
|
|
215
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-shared-eval-' ) );
|
|
216
|
+
mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
|
|
217
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
218
|
+
writeFileSync(
|
|
219
|
+
join( dir, 'shared', 'evaluators', 'common.js' ),
|
|
220
|
+
'export const SharedEval = evaluator({ name: \'shared.eval\' });'
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const source = 'const { SharedEval } = require( \'../../shared/evaluators/common.js\' );';
|
|
224
|
+
const ast = makeAst( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
225
|
+
|
|
226
|
+
const { sharedEvaluatorImports } = collectTargetImports(
|
|
227
|
+
ast,
|
|
228
|
+
join( dir, 'workflows', 'my_workflow' ),
|
|
229
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), sharedEvaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
230
|
+
);
|
|
231
|
+
expect( sharedEvaluatorImports ).toEqual( [ { localName: 'SharedEval', evaluatorName: 'shared.eval' } ] );
|
|
232
|
+
|
|
233
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
234
|
+
} );
|
|
235
|
+
|
|
236
|
+
it( 'collects CJS shared steps requires', () => {
|
|
237
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-shared-step-' ) );
|
|
238
|
+
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
239
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
240
|
+
writeFileSync(
|
|
241
|
+
join( dir, 'shared', 'steps', 'common.js' ),
|
|
242
|
+
'export const SharedA = step({ name: \'shared.a\' });'
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const source = 'const { SharedA } = require( \'../../shared/steps/common.js\' );';
|
|
246
|
+
const ast = makeAst( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
247
|
+
|
|
248
|
+
const { sharedStepImports } = collectTargetImports(
|
|
249
|
+
ast,
|
|
250
|
+
join( dir, 'workflows', 'my_workflow' ),
|
|
251
|
+
{
|
|
252
|
+
stepsNameCache: new Map(), sharedStepsNameCache: new Map(),
|
|
253
|
+
evaluatorsNameCache: new Map(), sharedEvaluatorsNameCache: new Map(),
|
|
254
|
+
workflowNameCache: new Map()
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
expect( sharedStepImports ).toEqual( [ { localName: 'SharedA', stepName: 'shared.a' } ] );
|
|
258
|
+
|
|
259
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
it( 'throws when CJS shared steps require uses evaluator() instead of step()', () => {
|
|
263
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-shared-step-mismatch-' ) );
|
|
264
|
+
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
265
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
266
|
+
writeFileSync(
|
|
267
|
+
join( dir, 'shared', 'steps', 'common.js' ),
|
|
268
|
+
'export const BadExport = evaluator({ name: \'bad\' });'
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const source = 'const { BadExport } = require( \'../../shared/steps/common.js\' );';
|
|
272
|
+
const ast = makeAst( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
273
|
+
|
|
274
|
+
expect( () => collectTargetImports(
|
|
275
|
+
ast,
|
|
276
|
+
join( dir, 'workflows', 'my_workflow' ),
|
|
277
|
+
{
|
|
278
|
+
stepsNameCache: new Map(), sharedStepsNameCache: new Map(),
|
|
279
|
+
evaluatorsNameCache: new Map(), sharedEvaluatorsNameCache: new Map(),
|
|
280
|
+
workflowNameCache: new Map()
|
|
281
|
+
}
|
|
282
|
+
) ).toThrow( /Unresolved import 'BadExport' from shared steps file/ );
|
|
283
|
+
|
|
284
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
it( 'throws when CJS shared evaluator require uses step() instead of evaluator()', () => {
|
|
288
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-shared-eval-mismatch-' ) );
|
|
289
|
+
mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
|
|
290
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
291
|
+
writeFileSync(
|
|
292
|
+
join( dir, 'shared', 'evaluators', 'common.js' ),
|
|
293
|
+
'export const BadExport = step({ name: \'bad\' });'
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const source = 'const { BadExport } = require( \'../../shared/evaluators/common.js\' );';
|
|
297
|
+
const ast = makeAst( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
298
|
+
|
|
299
|
+
expect( () => collectTargetImports(
|
|
300
|
+
ast,
|
|
301
|
+
join( dir, 'workflows', 'my_workflow' ),
|
|
302
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), sharedEvaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
303
|
+
) ).toThrow( /Unresolved import 'BadExport' from shared evaluators file/ );
|
|
304
|
+
|
|
305
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
306
|
+
} );
|
|
307
|
+
|
|
308
|
+
it( 'throws when ESM shared evaluator import uses step() instead of evaluator()', () => {
|
|
309
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-esm-shared-eval-mismatch-' ) );
|
|
310
|
+
mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
|
|
311
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
312
|
+
writeFileSync(
|
|
313
|
+
join( dir, 'shared', 'evaluators', 'common.js' ),
|
|
314
|
+
'export const BadExport = step({ name: \'bad\' });'
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const source = 'import { BadExport } from \'../../shared/evaluators/common.js\';';
|
|
318
|
+
const ast = makeAst( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
319
|
+
|
|
320
|
+
expect( () => collectTargetImports(
|
|
321
|
+
ast,
|
|
322
|
+
join( dir, 'workflows', 'my_workflow' ),
|
|
323
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), sharedEvaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
324
|
+
) ).toThrow( /Unresolved import 'BadExport' from shared evaluators file/ );
|
|
325
|
+
|
|
326
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
327
|
+
} );
|
|
78
328
|
} );
|
|
79
329
|
|
|
@@ -12,6 +12,7 @@ const generate = generatorModule.default ?? generatorModule;
|
|
|
12
12
|
const stepsNameCache = new Map(); // path -> Map<exported, stepName>
|
|
13
13
|
const sharedStepsNameCache = new Map(); // path -> Map<exported, stepName> (shared)
|
|
14
14
|
const evaluatorsNameCache = new Map(); // path -> Map<exported, evaluatorName>
|
|
15
|
+
const sharedEvaluatorsNameCache = new Map(); // path -> Map<exported, evaluatorName> (shared)
|
|
15
16
|
const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exported, flowName> }
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -27,20 +28,20 @@ const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exp
|
|
|
27
28
|
export default function stepImportRewriterAstLoader( source, inputMap ) {
|
|
28
29
|
this.cacheable?.( true );
|
|
29
30
|
const callback = this.async?.() ?? this.callback;
|
|
30
|
-
const cache = { stepsNameCache, sharedStepsNameCache, evaluatorsNameCache, workflowNameCache };
|
|
31
|
+
const cache = { stepsNameCache, sharedStepsNameCache, evaluatorsNameCache, sharedEvaluatorsNameCache, workflowNameCache };
|
|
31
32
|
|
|
32
33
|
try {
|
|
33
34
|
const filename = this.resourcePath;
|
|
34
35
|
const ast = parse( String( source ), filename );
|
|
35
36
|
const fileDir = dirname( filename );
|
|
36
|
-
const { stepImports, sharedStepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
37
|
+
const { stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
37
38
|
|
|
38
39
|
// No imports
|
|
39
|
-
if ( [].concat( stepImports, sharedStepImports, evaluatorImports, flowImports ).length === 0 ) {
|
|
40
|
+
if ( [].concat( stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports ).length === 0 ) {
|
|
40
41
|
return callback( null, source, inputMap );
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const rewrote = rewriteFnBodies( { ast, stepImports, sharedStepImports, evaluatorImports, flowImports } );
|
|
44
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports } );
|
|
44
45
|
// No edits performed
|
|
45
46
|
if ( !rewrote ) {
|
|
46
47
|
return callback( null, source, inputMap );
|
|
@@ -148,6 +148,54 @@ const obj = { fn: async () => { StepC(); FlowC(); FlowDef(); } }`;
|
|
|
148
148
|
rmSync( dir, { recursive: true, force: true } );
|
|
149
149
|
} );
|
|
150
150
|
|
|
151
|
+
it( 'rewrites ESM shared evaluator imports to invokeSharedEvaluator', async () => {
|
|
152
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-eval-' ) );
|
|
153
|
+
mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
|
|
154
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
155
|
+
writeFileSync( join( dir, 'shared', 'evaluators', 'common.js' ), 'export const SharedEval = evaluator({ name: \'shared.eval\' });' );
|
|
156
|
+
|
|
157
|
+
const source = `
|
|
158
|
+
import { SharedEval } from '../../shared/evaluators/common.js';
|
|
159
|
+
|
|
160
|
+
const obj = {
|
|
161
|
+
fn: async (x) => {
|
|
162
|
+
SharedEval(1);
|
|
163
|
+
}
|
|
164
|
+
}`;
|
|
165
|
+
|
|
166
|
+
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
167
|
+
|
|
168
|
+
expect( code ).not.toMatch( /from '\.\.\/\.\.\/shared\/evaluators\/common\.js'/ );
|
|
169
|
+
expect( code ).toMatch( /fn:\s*async function \(x\)/ );
|
|
170
|
+
expect( code ).toMatch( /this\.invokeSharedEvaluator\('shared\.eval',\s*1\)/ );
|
|
171
|
+
|
|
172
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
it( 'rewrites CJS shared evaluator requires to invokeSharedEvaluator', async () => {
|
|
176
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-eval-' ) );
|
|
177
|
+
mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
|
|
178
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
179
|
+
writeFileSync( join( dir, 'shared', 'evaluators', 'common.js' ), 'export const SharedEval = evaluator({ name: \'shared.eval\' });' );
|
|
180
|
+
|
|
181
|
+
const source = `
|
|
182
|
+
const { SharedEval } = require( '../../shared/evaluators/common.js' );
|
|
183
|
+
|
|
184
|
+
const obj = {
|
|
185
|
+
fn: async (y) => {
|
|
186
|
+
SharedEval();
|
|
187
|
+
}
|
|
188
|
+
}`;
|
|
189
|
+
|
|
190
|
+
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
191
|
+
|
|
192
|
+
expect( code ).not.toMatch( /require\('\.\.\/\.\.\/shared\/evaluators\/common\.js'\)/ );
|
|
193
|
+
expect( code ).toMatch( /fn:\s*async function \(y\)/ );
|
|
194
|
+
expect( code ).toMatch( /this\.invokeSharedEvaluator\('shared\.eval'\)/ );
|
|
195
|
+
|
|
196
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
197
|
+
} );
|
|
198
|
+
|
|
151
199
|
it( 'throws on non-static name', async () => {
|
|
152
200
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-error-' ) );
|
|
153
201
|
writeFileSync( join( dir, 'steps.js' ), `
|
|
@@ -156,13 +156,14 @@ const processFunction = ( { name, bindingPath, state, descriptors, processedFns
|
|
|
156
156
|
* @param {Array<{localName:string,workflowName:string}>} params.flowImports - Workflow imports.
|
|
157
157
|
* @returns {boolean} True if the AST was modified; false otherwise.
|
|
158
158
|
*/
|
|
159
|
-
export default function rewriteFnBodies( { ast, stepImports, sharedStepImports = [], evaluatorImports, flowImports } ) {
|
|
159
|
+
export default function rewriteFnBodies( { ast, stepImports, sharedStepImports = [], evaluatorImports, sharedEvaluatorImports = [], flowImports } ) {
|
|
160
160
|
const state = { rewrote: false };
|
|
161
161
|
// Build rewrite descriptors once per traversal
|
|
162
162
|
const descriptors = [
|
|
163
163
|
{ list: stepImports, method: 'invokeStep', key: 'stepName' },
|
|
164
164
|
{ list: sharedStepImports, method: 'invokeSharedStep', key: 'stepName' },
|
|
165
165
|
{ list: evaluatorImports, method: 'invokeEvaluator', key: 'evaluatorName' },
|
|
166
|
+
{ list: sharedEvaluatorImports, method: 'invokeSharedEvaluator', key: 'evaluatorName' },
|
|
166
167
|
{ list: flowImports, method: 'startWorkflow', key: 'workflowName' }
|
|
167
168
|
];
|
|
168
169
|
traverse( ast, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import traverseModule from '@babel/traverse';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { parse, toAbsolutePath, getFileKind, isAnyStepsPath,
|
|
3
|
+
import { parse, toAbsolutePath, getFileKind, isAnyStepsPath, isAnyEvaluatorsPath, isWorkflowPath } from '../tools.js';
|
|
4
4
|
import { ComponentFile } from '../consts.js';
|
|
5
5
|
import {
|
|
6
6
|
isCallExpression,
|
|
@@ -27,7 +27,7 @@ const getFileKindLabel = filename => {
|
|
|
27
27
|
if ( isAnyStepsPath( filename ) ) {
|
|
28
28
|
return 'steps.js';
|
|
29
29
|
}
|
|
30
|
-
if (
|
|
30
|
+
if ( isAnyEvaluatorsPath( filename ) ) {
|
|
31
31
|
return 'evaluators.js';
|
|
32
32
|
}
|
|
33
33
|
if ( /workflow\.js$/.test( filename ) ) {
|
|
@@ -48,7 +48,7 @@ const validateInstantiationLocation = ( calleeName, filename ) => {
|
|
|
48
48
|
if ( calleeName === 'step' && !isAnyStepsPath( filename ) ) {
|
|
49
49
|
throw new Error( `Invalid instantiation location: step() can only be called in files with 'steps' in the path. Found in: ${filename}` );
|
|
50
50
|
}
|
|
51
|
-
if ( calleeName === 'evaluator' && !
|
|
51
|
+
if ( calleeName === 'evaluator' && !isAnyEvaluatorsPath( filename ) ) {
|
|
52
52
|
throw new Error( `Invalid instantiation location: evaluator() can only be called in files with 'evaluators' in the path. Found in: ${filename}` );
|
|
53
53
|
}
|
|
54
54
|
if ( calleeName === 'workflow' && !isWorkflowPath( filename ) ) {
|
|
@@ -565,6 +565,28 @@ describe( 'workflow_validator loader', () => {
|
|
|
565
565
|
rmSync( dir, { recursive: true, force: true } );
|
|
566
566
|
} );
|
|
567
567
|
|
|
568
|
+
it( 'step() called in evaluators.js is blocked by validator', async () => {
|
|
569
|
+
const dir = mkdtempSync( join( tmpdir(), 'step-evals-fail-' ) );
|
|
570
|
+
const src = [
|
|
571
|
+
'import { step } from "@output.ai/core";',
|
|
572
|
+
'export const badStep = step({ name: "bad", fn: async () => ({}) });'
|
|
573
|
+
].join( '\n' );
|
|
574
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), src ) )
|
|
575
|
+
.rejects.toThrow( /Invalid instantiation location.*step\(\).*steps/ );
|
|
576
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
577
|
+
} );
|
|
578
|
+
|
|
579
|
+
it( 'evaluator() called in steps.js is blocked by validator', async () => {
|
|
580
|
+
const dir = mkdtempSync( join( tmpdir(), 'eval-steps-fail-' ) );
|
|
581
|
+
const src = [
|
|
582
|
+
'import { evaluator } from "@output.ai/core";',
|
|
583
|
+
'export const badEval = evaluator({ name: "bad", fn: async () => ({ value: 1 }) });'
|
|
584
|
+
].join( '\n' );
|
|
585
|
+
await expect( runLoader( join( dir, 'steps.js' ), src ) )
|
|
586
|
+
.rejects.toThrow( /Invalid instantiation location.*evaluator\(\).*evaluators/ );
|
|
587
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
588
|
+
} );
|
|
589
|
+
|
|
568
590
|
it( 'workflow() called in shared/common.js is blocked by validator', async () => {
|
|
569
591
|
const dir = mkdtempSync( join( tmpdir(), 'wf-shared-fail-' ) );
|
|
570
592
|
mkdirSync( join( dir, 'shared' ) );
|