@output.ai/core 0.2.4 → 0.3.0-dev.pr263-8f8e94a
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/consts.js +1 -1
- package/src/interface/workflow.js +3 -4
- package/src/worker/index.js +1 -1
- package/src/worker/loader.js +58 -17
- package/src/worker/loader.spec.js +106 -5
- package/src/worker/loader_tools.js +80 -11
- package/src/worker/loader_tools.spec.js +33 -4
- package/src/worker/webpack_loaders/consts.js +0 -1
- package/src/worker/webpack_loaders/tools.js +70 -21
- package/src/worker/webpack_loaders/tools.spec.js +65 -14
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +17 -11
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +109 -27
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +429 -62
|
@@ -26,6 +26,14 @@ import {
|
|
|
26
26
|
} from '@babel/types';
|
|
27
27
|
import { ComponentFile, NodeType } from './consts.js';
|
|
28
28
|
|
|
29
|
+
// Path pattern regexes - shared across multiple helper functions
|
|
30
|
+
const STEPS_FILE_REGEX = /(^|\/)steps\.js$/;
|
|
31
|
+
const STEPS_FOLDER_REGEX = /\/steps\/[^/]+\.js$/;
|
|
32
|
+
const EVALUATORS_FILE_REGEX = /(^|\/)evaluators\.js$/;
|
|
33
|
+
const EVALUATORS_FOLDER_REGEX = /\/evaluators\/[^/]+\.js$/;
|
|
34
|
+
const PATH_TRAVERSAL_REGEX = /\.\.\//;
|
|
35
|
+
const SHARED_PATH_REGEX = /\/shared\//;
|
|
36
|
+
|
|
29
37
|
/**
|
|
30
38
|
* Resolve a relative module specifier against a base directory.
|
|
31
39
|
* @param {string} fileDir - Base directory to resolve from.
|
|
@@ -104,25 +112,67 @@ export const toFunctionExpression = arrow => {
|
|
|
104
112
|
};
|
|
105
113
|
|
|
106
114
|
/**
|
|
107
|
-
* Check if a module specifier or request string points to steps.js.
|
|
115
|
+
* Check if a module specifier or request string points to steps.js or is in a steps folder.
|
|
116
|
+
* Matches: steps.js, /steps.js, /steps/*.js
|
|
117
|
+
* This matches LOCAL steps only (no path traversal).
|
|
118
|
+
* @param {string} value - Module path or request string.
|
|
119
|
+
* @returns {boolean} True if it matches a local steps path.
|
|
120
|
+
*/
|
|
121
|
+
export const isStepsPath = value => {
|
|
122
|
+
// Exclude shared steps (paths with ../ or containing /shared/)
|
|
123
|
+
if ( PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value ) ) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return STEPS_FILE_REGEX.test( value ) || STEPS_FOLDER_REGEX.test( value );
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a module specifier or request string points to shared steps.
|
|
131
|
+
* Shared steps are steps imported from outside the current workflow directory.
|
|
132
|
+
* Matches paths with ../ traversal or /shared/ and containing steps pattern.
|
|
133
|
+
* @param {string} value - Module path or request string.
|
|
134
|
+
* @returns {boolean} True if it matches a shared steps path.
|
|
135
|
+
*/
|
|
136
|
+
export const isSharedStepsPath = value => {
|
|
137
|
+
const hasStepsPattern = STEPS_FILE_REGEX.test( value ) || STEPS_FOLDER_REGEX.test( value );
|
|
138
|
+
if ( !hasStepsPattern ) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value );
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if a path matches any steps pattern (local or shared).
|
|
146
|
+
* Used for validation purposes.
|
|
108
147
|
* @param {string} value - Module path or request string.
|
|
109
|
-
* @returns {boolean} True if it matches steps.
|
|
148
|
+
* @returns {boolean} True if it matches any steps path pattern.
|
|
110
149
|
*/
|
|
111
|
-
export const
|
|
150
|
+
export const isAnyStepsPath = value =>
|
|
151
|
+
STEPS_FILE_REGEX.test( value ) || STEPS_FOLDER_REGEX.test( value );
|
|
112
152
|
|
|
113
153
|
/**
|
|
114
|
-
* Check if a module specifier or request string points to
|
|
154
|
+
* Check if a module specifier or request string points to evaluators.js or is in an evaluators folder.
|
|
155
|
+
* Matches: evaluators.js, /evaluators.js, /evaluators/*.js
|
|
115
156
|
* @param {string} value - Module path or request string.
|
|
116
|
-
* @returns {boolean} True if it matches
|
|
157
|
+
* @returns {boolean} True if it matches an evaluators path.
|
|
117
158
|
*/
|
|
118
|
-
export const
|
|
159
|
+
export const isEvaluatorsPath = value =>
|
|
160
|
+
EVALUATORS_FILE_REGEX.test( value ) || EVALUATORS_FOLDER_REGEX.test( value );
|
|
119
161
|
|
|
120
162
|
/**
|
|
121
|
-
* Check if a module specifier or request string points to evaluators.
|
|
163
|
+
* Check if a module specifier or request string points to shared evaluators.
|
|
164
|
+
* Shared evaluators are evaluators imported from outside the current workflow directory.
|
|
165
|
+
* Matches paths with ../ traversal or /shared/ and containing evaluators pattern.
|
|
122
166
|
* @param {string} value - Module path or request string.
|
|
123
|
-
* @returns {boolean} True if it matches evaluators.
|
|
167
|
+
* @returns {boolean} True if it matches a shared evaluators path.
|
|
124
168
|
*/
|
|
125
|
-
export const
|
|
169
|
+
export const isSharedEvaluatorsPath = value => {
|
|
170
|
+
const hasEvaluatorsPattern = EVALUATORS_FILE_REGEX.test( value ) || EVALUATORS_FOLDER_REGEX.test( value );
|
|
171
|
+
if ( !hasEvaluatorsPattern ) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value );
|
|
175
|
+
};
|
|
126
176
|
|
|
127
177
|
/**
|
|
128
178
|
* Check if a module specifier or request string points to workflow.js.
|
|
@@ -132,24 +182,23 @@ export const isEvaluatorsPath = value => /(^|\/)evaluators\.js$/.test( value );
|
|
|
132
182
|
export const isWorkflowPath = value => /(^|\/)workflow\.js$/.test( value );
|
|
133
183
|
|
|
134
184
|
/**
|
|
135
|
-
* Check if a
|
|
185
|
+
* Check if a path is a component file (steps, evaluators, or workflow).
|
|
136
186
|
* @param {string} value - Module path or request string.
|
|
137
|
-
* @returns {boolean} True if it matches
|
|
187
|
+
* @returns {boolean} True if it matches any component file path.
|
|
138
188
|
*/
|
|
139
|
-
export const
|
|
189
|
+
export const isComponentFile = value =>
|
|
190
|
+
isAnyStepsPath( value ) || isEvaluatorsPath( value ) || isWorkflowPath( value );
|
|
140
191
|
|
|
141
192
|
/**
|
|
142
193
|
* Determine file kind based on its path.
|
|
143
|
-
*
|
|
144
|
-
* @
|
|
194
|
+
* Returns the component type if it's a component file, null otherwise.
|
|
195
|
+
* @param {string} path
|
|
196
|
+
* @returns {'workflow'|'steps'|'evaluators'|null}
|
|
145
197
|
*/
|
|
146
198
|
export const getFileKind = path => {
|
|
147
|
-
if (
|
|
199
|
+
if ( isAnyStepsPath( path ) ) {
|
|
148
200
|
return ComponentFile.STEPS;
|
|
149
201
|
}
|
|
150
|
-
if ( isSharedStepsPath( path ) ) {
|
|
151
|
-
return ComponentFile.SHARED_STEPS;
|
|
152
|
-
}
|
|
153
202
|
if ( isEvaluatorsPath( path ) ) {
|
|
154
203
|
return ComponentFile.EVALUATORS;
|
|
155
204
|
}
|
|
@@ -278,12 +327,12 @@ export const buildStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
|
278
327
|
|
|
279
328
|
/**
|
|
280
329
|
* Build a map from exported shared step identifier to declared step name.
|
|
281
|
-
*
|
|
282
|
-
* Uses the same factory as regular steps.
|
|
330
|
+
* Same as buildStepsNameMap but for shared steps.
|
|
283
331
|
*
|
|
284
332
|
* @param {string} path - Absolute path to the shared steps module file.
|
|
285
|
-
* @param {Map<string, Map<string,string>>} cache - Cache of computed name maps.
|
|
333
|
+
* @param {Map<string, Map<string,string>>} cache - Cache of computed step name maps.
|
|
286
334
|
* @returns {Map<string,string>} Exported identifier -> step name.
|
|
335
|
+
* @throws {Error} When a step name is invalid (non-static or missing).
|
|
287
336
|
*/
|
|
288
337
|
export const buildSharedStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
289
338
|
path,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join, resolve as resolvePath } from 'node:path';
|
|
5
5
|
import * as t from '@babel/types';
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
toFunctionExpression,
|
|
13
13
|
isStepsPath,
|
|
14
14
|
isSharedStepsPath,
|
|
15
|
+
isAnyStepsPath,
|
|
15
16
|
isEvaluatorsPath,
|
|
17
|
+
isSharedEvaluatorsPath,
|
|
16
18
|
isWorkflowPath,
|
|
17
19
|
createThisMethodCall,
|
|
18
20
|
resolveNameFromOptions,
|
|
@@ -137,14 +139,49 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
137
139
|
expect( t.isFunctionExpression( fn2 ) ).toBe( true );
|
|
138
140
|
} );
|
|
139
141
|
|
|
140
|
-
it( 'isStepsPath: matches steps.js
|
|
142
|
+
it( 'isStepsPath: matches LOCAL steps.js (no path traversal)', () => {
|
|
143
|
+
// Local steps (without ../ or /shared/)
|
|
141
144
|
expect( isStepsPath( 'steps.js' ) ).toBe( true );
|
|
142
145
|
expect( isStepsPath( './steps.js' ) ).toBe( true );
|
|
143
146
|
expect( isStepsPath( '/a/b/steps.js' ) ).toBe( true );
|
|
147
|
+
expect( isStepsPath( './steps/fetch.js' ) ).toBe( true );
|
|
148
|
+
// Shared steps (with ../ or /shared/) should NOT match isStepsPath
|
|
149
|
+
expect( isStepsPath( '../steps.js' ) ).toBe( false );
|
|
150
|
+
expect( isStepsPath( '../../shared/steps/common.js' ) ).toBe( false );
|
|
151
|
+
// Non-steps
|
|
144
152
|
expect( isStepsPath( 'steps.ts' ) ).toBe( false );
|
|
145
153
|
expect( isStepsPath( 'workflow.js' ) ).toBe( false );
|
|
146
154
|
} );
|
|
147
155
|
|
|
156
|
+
it( 'isSharedStepsPath: matches steps imported from outside workflow directory', () => {
|
|
157
|
+
// Shared steps: must have steps pattern AND have path traversal or /shared/
|
|
158
|
+
expect( isSharedStepsPath( '../steps.js' ) ).toBe( true );
|
|
159
|
+
expect( isSharedStepsPath( '../../steps.js' ) ).toBe( true );
|
|
160
|
+
expect( isSharedStepsPath( '../../shared/steps/common.js' ) ).toBe( true );
|
|
161
|
+
expect( isSharedStepsPath( '../other_workflow/steps.js' ) ).toBe( true );
|
|
162
|
+
expect( isSharedStepsPath( '/src/shared/steps/common.js' ) ).toBe( true );
|
|
163
|
+
// Local steps (no traversal, no /shared/) should NOT match
|
|
164
|
+
expect( isSharedStepsPath( './steps.js' ) ).toBe( false );
|
|
165
|
+
expect( isSharedStepsPath( 'steps.js' ) ).toBe( false );
|
|
166
|
+
expect( isSharedStepsPath( './steps/fetch.js' ) ).toBe( false );
|
|
167
|
+
// Non-steps should NOT match
|
|
168
|
+
expect( isSharedStepsPath( '../utils.js' ) ).toBe( false );
|
|
169
|
+
expect( isSharedStepsPath( 'evaluators.js' ) ).toBe( false );
|
|
170
|
+
} );
|
|
171
|
+
|
|
172
|
+
it( 'isAnyStepsPath: matches any steps pattern (local or shared)', () => {
|
|
173
|
+
// Local steps
|
|
174
|
+
expect( isAnyStepsPath( 'steps.js' ) ).toBe( true );
|
|
175
|
+
expect( isAnyStepsPath( './steps.js' ) ).toBe( true );
|
|
176
|
+
expect( isAnyStepsPath( './steps/fetch.js' ) ).toBe( true );
|
|
177
|
+
// Shared steps
|
|
178
|
+
expect( isAnyStepsPath( '../steps.js' ) ).toBe( true );
|
|
179
|
+
expect( isAnyStepsPath( '../../shared/steps/common.js' ) ).toBe( true );
|
|
180
|
+
// Non-steps
|
|
181
|
+
expect( isAnyStepsPath( 'workflow.js' ) ).toBe( false );
|
|
182
|
+
expect( isAnyStepsPath( 'utils.js' ) ).toBe( false );
|
|
183
|
+
} );
|
|
184
|
+
|
|
148
185
|
it( 'isWorkflowPath: matches workflow.js at root or subpath', () => {
|
|
149
186
|
expect( isWorkflowPath( 'workflow.js' ) ).toBe( true );
|
|
150
187
|
expect( isWorkflowPath( './workflow.js' ) ).toBe( true );
|
|
@@ -153,22 +190,31 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
153
190
|
expect( isWorkflowPath( 'steps.js' ) ).toBe( false );
|
|
154
191
|
} );
|
|
155
192
|
|
|
156
|
-
it( 'isSharedStepsPath: matches shared_steps.js at root or subpath', () => {
|
|
157
|
-
expect( isSharedStepsPath( 'shared_steps.js' ) ).toBe( true );
|
|
158
|
-
expect( isSharedStepsPath( './shared_steps.js' ) ).toBe( true );
|
|
159
|
-
expect( isSharedStepsPath( '/a/b/shared_steps.js' ) ).toBe( true );
|
|
160
|
-
expect( isSharedStepsPath( 'shared_steps.ts' ) ).toBe( false );
|
|
161
|
-
expect( isSharedStepsPath( 'evaluators.js' ) ).toBe( false );
|
|
162
|
-
} );
|
|
163
|
-
|
|
164
193
|
it( 'isEvaluatorsPath: matches evaluators.js at root or subpath', () => {
|
|
165
194
|
expect( isEvaluatorsPath( 'evaluators.js' ) ).toBe( true );
|
|
166
195
|
expect( isEvaluatorsPath( './evaluators.js' ) ).toBe( true );
|
|
167
196
|
expect( isEvaluatorsPath( '/a/b/evaluators.js' ) ).toBe( true );
|
|
197
|
+
expect( isEvaluatorsPath( './evaluators/quality.js' ) ).toBe( true );
|
|
168
198
|
expect( isEvaluatorsPath( 'evaluators.ts' ) ).toBe( false );
|
|
169
199
|
expect( isEvaluatorsPath( 'steps.js' ) ).toBe( false );
|
|
170
200
|
} );
|
|
171
201
|
|
|
202
|
+
it( 'isSharedEvaluatorsPath: matches evaluators imported from outside workflow directory', () => {
|
|
203
|
+
// Shared evaluators: must have evaluators pattern AND have path traversal or /shared/
|
|
204
|
+
expect( isSharedEvaluatorsPath( '../evaluators.js' ) ).toBe( true );
|
|
205
|
+
expect( isSharedEvaluatorsPath( '../../evaluators.js' ) ).toBe( true );
|
|
206
|
+
expect( isSharedEvaluatorsPath( '../../shared/evaluators/quality.js' ) ).toBe( true );
|
|
207
|
+
expect( isSharedEvaluatorsPath( '../other_workflow/evaluators.js' ) ).toBe( true );
|
|
208
|
+
expect( isSharedEvaluatorsPath( '/src/shared/evaluators/quality.js' ) ).toBe( true );
|
|
209
|
+
// Local evaluators (no traversal, no /shared/) should NOT match
|
|
210
|
+
expect( isSharedEvaluatorsPath( './evaluators.js' ) ).toBe( false );
|
|
211
|
+
expect( isSharedEvaluatorsPath( 'evaluators.js' ) ).toBe( false );
|
|
212
|
+
expect( isSharedEvaluatorsPath( './evaluators/quality.js' ) ).toBe( false );
|
|
213
|
+
// Non-evaluators should NOT match
|
|
214
|
+
expect( isSharedEvaluatorsPath( '../utils.js' ) ).toBe( false );
|
|
215
|
+
expect( isSharedEvaluatorsPath( 'steps.js' ) ).toBe( false );
|
|
216
|
+
} );
|
|
217
|
+
|
|
172
218
|
it( 'createThisMethodCall: builds this.method(\'name\', ...args) call', () => {
|
|
173
219
|
const call = createThisMethodCall( 'invoke', 'n', [ t.numericLiteral( 1 ), t.identifier( 'x' ) ] );
|
|
174
220
|
expect( t.isCallExpression( call ) ).toBe( true );
|
|
@@ -179,9 +225,10 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
179
225
|
expect( call.arguments.length ).toBe( 3 );
|
|
180
226
|
} );
|
|
181
227
|
|
|
182
|
-
it( 'buildSharedStepsNameMap: reads names from
|
|
228
|
+
it( 'buildSharedStepsNameMap: reads names from shared steps module and caches result', () => {
|
|
183
229
|
const dir = mkdtempSync( join( tmpdir(), 'tools-shared-steps-' ) );
|
|
184
|
-
|
|
230
|
+
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
231
|
+
const stepsPath = join( dir, 'shared', 'steps', 'common.js' );
|
|
185
232
|
writeFileSync( stepsPath, [
|
|
186
233
|
'export const StepA = step({ name: "shared.step.a" })',
|
|
187
234
|
'export const StepB = step({ name: "shared.step.b" })'
|
|
@@ -199,9 +246,13 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
199
246
|
it( 'getFileKind: classifies file by its path', () => {
|
|
200
247
|
expect( getFileKind( '/p/workflow.js' ) ).toBe( 'workflow' );
|
|
201
248
|
expect( getFileKind( '/p/steps.js' ) ).toBe( 'steps' );
|
|
202
|
-
|
|
249
|
+
// Files in steps folder are steps
|
|
250
|
+
expect( getFileKind( '/p/steps/fetch.js' ) ).toBe( 'steps' );
|
|
251
|
+
expect( getFileKind( '/p/shared/steps/common.js' ) ).toBe( 'steps' );
|
|
203
252
|
expect( getFileKind( '/p/evaluators.js' ) ).toBe( 'evaluators' );
|
|
253
|
+
expect( getFileKind( '/p/evaluators/quality.js' ) ).toBe( 'evaluators' );
|
|
204
254
|
expect( getFileKind( '/p/other.js' ) ).toBe( null );
|
|
255
|
+
expect( getFileKind( '/p/utils.js' ) ).toBe( null );
|
|
256
|
+
expect( getFileKind( '/p/clients/api.js' ) ).toBe( null );
|
|
205
257
|
} );
|
|
206
258
|
} );
|
|
207
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import loader from './index.mjs';
|
|
@@ -48,12 +48,15 @@ const obj = {
|
|
|
48
48
|
rmSync( dir, { recursive: true, force: true } );
|
|
49
49
|
} );
|
|
50
50
|
|
|
51
|
-
it( 'rewrites ESM
|
|
51
|
+
it( 'rewrites ESM shared steps imports to invokeSharedStep', async () => {
|
|
52
|
+
// Create directory structure: shared/steps/common.js
|
|
52
53
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-' ) );
|
|
53
|
-
|
|
54
|
+
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
55
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
56
|
+
writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedA = step({ name: \'shared.a\' });' );
|
|
54
57
|
|
|
55
58
|
const source = `
|
|
56
|
-
import { SharedA } from '
|
|
59
|
+
import { SharedA } from '../../shared/steps/common.js';
|
|
57
60
|
|
|
58
61
|
const obj = {
|
|
59
62
|
fn: async (x) => {
|
|
@@ -61,21 +64,24 @@ const obj = {
|
|
|
61
64
|
}
|
|
62
65
|
}`;
|
|
63
66
|
|
|
64
|
-
const { code } = await runLoader( source, join( dir, '
|
|
67
|
+
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
65
68
|
|
|
66
|
-
expect( code ).not.toMatch( /from '
|
|
69
|
+
expect( code ).not.toMatch( /from '\.\.\/\.\.\/shared\/steps\/common\.js'/ );
|
|
67
70
|
expect( code ).toMatch( /fn:\s*async function \(x\)/ );
|
|
68
71
|
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.a',\s*1\)/ );
|
|
69
72
|
|
|
70
73
|
rmSync( dir, { recursive: true, force: true } );
|
|
71
74
|
} );
|
|
72
75
|
|
|
73
|
-
it( 'rewrites CJS
|
|
76
|
+
it( 'rewrites CJS shared steps requires to invokeSharedStep', async () => {
|
|
77
|
+
// Create directory structure: shared/steps/common.js
|
|
74
78
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-' ) );
|
|
75
|
-
|
|
79
|
+
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
80
|
+
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
81
|
+
writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedB = step({ name: \'shared.b\' });' );
|
|
76
82
|
|
|
77
83
|
const source = `
|
|
78
|
-
const { SharedB } = require( '
|
|
84
|
+
const { SharedB } = require( '../../shared/steps/common.js' );
|
|
79
85
|
|
|
80
86
|
const obj = {
|
|
81
87
|
fn: async (y) => {
|
|
@@ -83,9 +89,9 @@ const obj = {
|
|
|
83
89
|
}
|
|
84
90
|
}`;
|
|
85
91
|
|
|
86
|
-
const { code } = await runLoader( source, join( dir, '
|
|
92
|
+
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
87
93
|
|
|
88
|
-
expect( code ).not.toMatch( /require\('
|
|
94
|
+
expect( code ).not.toMatch( /require\('\.\.\/\.\.\/shared\/steps\/common\.js'\)/ );
|
|
89
95
|
expect( code ).toMatch( /fn:\s*async function \(y\)/ );
|
|
90
96
|
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.b'\)/ );
|
|
91
97
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import traverseModule from '@babel/traverse';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { parse, toAbsolutePath, getFileKind } from '../tools.js';
|
|
4
|
-
import { ComponentFile } from '../consts.js';
|
|
3
|
+
import { parse, toAbsolutePath, getFileKind, isAnyStepsPath, isEvaluatorsPath, isWorkflowPath } from '../tools.js';
|
|
4
|
+
import { ComponentFile, CoreModule } from '../consts.js';
|
|
5
5
|
import {
|
|
6
6
|
isCallExpression,
|
|
7
7
|
isFunctionExpression,
|
|
@@ -18,21 +18,92 @@ import {
|
|
|
18
18
|
const traverse = traverseModule.default ?? traverseModule;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Determine the file kind label for error messages.
|
|
22
|
+
* Handles both flat files (steps.js) and folder-based files (steps/fetch_data.js).
|
|
23
|
+
* @param {string} filename - The file path
|
|
24
|
+
* @returns {string} Human-readable file kind for error messages
|
|
22
25
|
*/
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
Steps, shared_steps, evaluators or workflows are not allowed dependencies in ${filename}` );
|
|
26
|
+
const getFileKindLabel = filename => {
|
|
27
|
+
if ( isAnyStepsPath( filename ) ) {
|
|
28
|
+
return 'steps.js';
|
|
27
29
|
}
|
|
30
|
+
if ( isEvaluatorsPath( filename ) ) {
|
|
31
|
+
return 'evaluators.js';
|
|
32
|
+
}
|
|
33
|
+
if ( /workflow\.js$/.test( filename ) ) {
|
|
34
|
+
return 'workflow.js';
|
|
35
|
+
}
|
|
36
|
+
return filename;
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
|
-
*
|
|
40
|
+
* Check if workflow dependencies are valid.
|
|
41
|
+
* Workflows can import:
|
|
42
|
+
* - Components (steps, evaluators, workflow)
|
|
43
|
+
* - Core modules (@output.ai/core, local_core)
|
|
44
|
+
* - ANY file that is NOT a component file (flexible utility imports)
|
|
45
|
+
*/
|
|
46
|
+
const validateWorkflowImports = ( { specifier, filename } ) => {
|
|
47
|
+
const isCore = Object.values( CoreModule ).includes( specifier );
|
|
48
|
+
const fileKind = getFileKind( specifier );
|
|
49
|
+
const isComponent = Object.values( ComponentFile ).includes( fileKind );
|
|
50
|
+
const isNonComponentFile = fileKind === null;
|
|
51
|
+
|
|
52
|
+
if ( !isCore && !isComponent && !isNonComponentFile ) {
|
|
53
|
+
throw new Error( `Invalid dependency in workflow.js: '${specifier}'. \
|
|
54
|
+
Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or non-component files are allowed in ${filename}` );
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if evaluators or steps import invalid dependencies.
|
|
60
|
+
* Steps and evaluators CANNOT import:
|
|
61
|
+
* - Other steps (local or shared) - activity isolation
|
|
62
|
+
* - Other evaluators (local or shared) - activity isolation
|
|
63
|
+
* - Workflows
|
|
64
|
+
*
|
|
65
|
+
* Steps and evaluators CAN import:
|
|
66
|
+
* - ANY file that is NOT a component file (flexible utility imports)
|
|
67
|
+
*/
|
|
68
|
+
const validateStepEvaluatorImports = ( { specifier, filename } ) => {
|
|
69
|
+
const importedFileKind = getFileKind( specifier );
|
|
70
|
+
|
|
71
|
+
// Activity isolation: steps/evaluators cannot import other steps, evaluators, or workflows
|
|
72
|
+
if ( Object.values( ComponentFile ).includes( importedFileKind ) ) {
|
|
73
|
+
const fileLabel = getFileKindLabel( filename );
|
|
74
|
+
throw new Error( `Invalid dependency in ${fileLabel}: '${specifier}'. \
|
|
75
|
+
Steps, evaluators or workflows are not allowed dependencies in ${filename}` );
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate import for evaluators, steps, workflow
|
|
32
81
|
*/
|
|
33
82
|
const executeImportValidations = ( { fileKind, specifier, filename } ) => {
|
|
34
|
-
if (
|
|
35
|
-
|
|
83
|
+
if ( fileKind === ComponentFile.WORKFLOW ) {
|
|
84
|
+
validateWorkflowImports( { specifier, filename } );
|
|
85
|
+
} else if ( Object.values( ComponentFile ).includes( fileKind ) ) {
|
|
86
|
+
validateStepEvaluatorImports( { specifier, filename } );
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate that component instantiation calls occur in the correct file locations.
|
|
92
|
+
* - step() must be called in a file whose path contains 'steps'
|
|
93
|
+
* - evaluator() must be called in a file whose path contains 'evaluators'
|
|
94
|
+
* - workflow() must be called in a file whose path contains 'workflow'
|
|
95
|
+
* @param {string} calleeName - The factory function name (step, evaluator, workflow)
|
|
96
|
+
* @param {string} filename - The file path where the call occurs
|
|
97
|
+
*/
|
|
98
|
+
const validateInstantiationLocation = ( calleeName, filename ) => {
|
|
99
|
+
if ( calleeName === 'step' && !isAnyStepsPath( filename ) ) {
|
|
100
|
+
throw new Error( `Invalid instantiation location: step() can only be called in files with 'steps' in the path. Found in: ${filename}` );
|
|
101
|
+
}
|
|
102
|
+
if ( calleeName === 'evaluator' && !isEvaluatorsPath( filename ) ) {
|
|
103
|
+
throw new Error( `Invalid instantiation location: evaluator() can only be called in files with 'evaluators' in the path. Found in: ${filename}` );
|
|
104
|
+
}
|
|
105
|
+
if ( calleeName === 'workflow' && !isWorkflowPath( filename ) ) {
|
|
106
|
+
throw new Error( `Invalid instantiation location: workflow() can only be called in files with 'workflow' in the path. Found in: ${filename}` );
|
|
36
107
|
}
|
|
37
108
|
};
|
|
38
109
|
|
|
@@ -41,12 +112,13 @@ const executeImportValidations = ( { fileKind, specifier, filename } ) => {
|
|
|
41
112
|
* Returns the source unchanged unless a validation error is found.
|
|
42
113
|
*
|
|
43
114
|
* Rules enforced:
|
|
44
|
-
* -
|
|
45
|
-
* - evaluators.js
|
|
46
|
-
* -
|
|
47
|
-
* -
|
|
48
|
-
* - steps.js:
|
|
49
|
-
* -
|
|
115
|
+
* - Instantiation location: step() must be in steps path, evaluator() in evaluators path, workflow() in workflow path
|
|
116
|
+
* - evaluators.js `fn`: at each evaluator().fn body: calling any evaluator, step, or workflow is forbidden
|
|
117
|
+
* - evaluators.js: may not import evaluators.js, steps.js, workflow.js, or any shared steps/evaluators
|
|
118
|
+
* - steps.js: at each step().fn body: calling any evaluator, step, or workflow is forbidden
|
|
119
|
+
* - steps.js: may not import evaluators.js, steps.js, workflow.js, or any shared steps/evaluators
|
|
120
|
+
* - workflow.js: may import components (evaluators.js, steps.js, workflow.js including shared);
|
|
121
|
+
* and any non-component file, or `@output.ai/core`
|
|
50
122
|
*
|
|
51
123
|
* @param {string|Buffer} source
|
|
52
124
|
* @param {any} inputMap
|
|
@@ -66,7 +138,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
66
138
|
// Collect local declarations and imported identifiers by type
|
|
67
139
|
const localStepIds = new Set();
|
|
68
140
|
const localEvaluatorIds = new Set();
|
|
69
|
-
const importedStepIds = new Set();
|
|
141
|
+
const importedStepIds = new Set();
|
|
70
142
|
const importedEvaluatorIds = new Set();
|
|
71
143
|
const importedWorkflowIds = new Set();
|
|
72
144
|
|
|
@@ -78,12 +150,12 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
78
150
|
executeImportValidations( { fileKind, specifier, filename } );
|
|
79
151
|
|
|
80
152
|
// Collect imported identifiers for later call checks
|
|
153
|
+
const importedKind = getFileKind( specifier );
|
|
81
154
|
const accumulator = ( {
|
|
82
155
|
[ComponentFile.STEPS]: importedStepIds,
|
|
83
|
-
[ComponentFile.SHARED_STEPS]: importedStepIds,
|
|
84
156
|
[ComponentFile.EVALUATORS]: importedEvaluatorIds,
|
|
85
157
|
[ComponentFile.WORKFLOW]: importedWorkflowIds
|
|
86
|
-
} )[
|
|
158
|
+
} )[importedKind];
|
|
87
159
|
if ( accumulator ) {
|
|
88
160
|
for ( const s of path.node.specifiers ) {
|
|
89
161
|
if ( isImportSpecifier( s ) || isImportDefaultSpecifier( s ) ) {
|
|
@@ -98,12 +170,21 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
98
170
|
return;
|
|
99
171
|
}
|
|
100
172
|
|
|
101
|
-
//
|
|
102
|
-
if ( isIdentifier( init.callee, { name: 'step' } )
|
|
103
|
-
|
|
173
|
+
// Validate instantiation location for step/evaluator/workflow calls
|
|
174
|
+
if ( isIdentifier( init.callee, { name: 'step' } ) ) {
|
|
175
|
+
validateInstantiationLocation( 'step', filename );
|
|
176
|
+
if ( isIdentifier( path.node.id ) ) {
|
|
177
|
+
localStepIds.add( path.node.id.name );
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if ( isIdentifier( init.callee, { name: 'evaluator' } ) ) {
|
|
181
|
+
validateInstantiationLocation( 'evaluator', filename );
|
|
182
|
+
if ( isIdentifier( path.node.id ) ) {
|
|
183
|
+
localEvaluatorIds.add( path.node.id.name );
|
|
184
|
+
}
|
|
104
185
|
}
|
|
105
|
-
if ( isIdentifier( init.callee, { name: '
|
|
106
|
-
|
|
186
|
+
if ( isIdentifier( init.callee, { name: 'workflow' } ) ) {
|
|
187
|
+
validateInstantiationLocation( 'workflow', filename );
|
|
107
188
|
}
|
|
108
189
|
|
|
109
190
|
// CommonJS requires: validate source and collect identifiers
|
|
@@ -118,7 +199,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
118
199
|
// Collect imported identifiers from require patterns
|
|
119
200
|
if ( isStringLiteral( firstArg ) ) {
|
|
120
201
|
const reqType = getFileKind( toAbsolutePath( fileDir, req ) );
|
|
121
|
-
if (
|
|
202
|
+
if ( reqType === ComponentFile.STEPS && isObjectPattern( path.node.id ) ) {
|
|
122
203
|
for ( const prop of path.node.id.properties ) {
|
|
123
204
|
if ( isObjectProperty( prop ) && isIdentifier( prop.value ) ) {
|
|
124
205
|
importedStepIds.add( prop.value.name );
|
|
@@ -141,7 +222,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
141
222
|
} );
|
|
142
223
|
|
|
143
224
|
// Function-body call validations for steps/evaluators files
|
|
144
|
-
if ( [ ComponentFile.STEPS, ComponentFile.
|
|
225
|
+
if ( [ ComponentFile.STEPS, ComponentFile.EVALUATORS ].includes( fileKind ) ) {
|
|
145
226
|
traverse( ast, {
|
|
146
227
|
ObjectProperty: path => {
|
|
147
228
|
if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) {
|
|
@@ -157,6 +238,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
157
238
|
const callee = cPath.node.callee;
|
|
158
239
|
if ( isIdentifier( callee ) ) {
|
|
159
240
|
const { name } = callee;
|
|
241
|
+
const fileLabel = getFileKindLabel( filename );
|
|
160
242
|
const violation = [
|
|
161
243
|
[ 'step', localStepIds.has( name ) || importedStepIds.has( name ) ],
|
|
162
244
|
[ 'evaluator', localEvaluatorIds.has( name ) || importedEvaluatorIds.has( name ) ],
|
|
@@ -164,7 +246,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
164
246
|
].find( v => v[1] )?.[0];
|
|
165
247
|
|
|
166
248
|
if ( violation ) {
|
|
167
|
-
throw new Error( `Invalid call in ${
|
|
249
|
+
throw new Error( `Invalid call in ${fileLabel} fn: calling a ${violation} ('${name}') is not allowed in ${filename}` );
|
|
168
250
|
}
|
|
169
251
|
}
|
|
170
252
|
}
|