@output.ai/core 0.2.2 → 0.3.0-dev.pr263-a59dd0e
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/worker/loader.js +2 -3
- package/src/worker/loader.spec.js +21 -0
- package/src/worker/loader_tools.js +45 -4
- package/src/worker/loader_tools.spec.js +80 -0
- package/src/worker/webpack_loaders/consts.js +0 -14
- package/src/worker/webpack_loaders/tools.js +71 -27
- package/src/worker/webpack_loaders/tools.spec.js +63 -56
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +17 -11
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +99 -33
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +401 -78
package/package.json
CHANGED
package/src/worker/loader.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { EOL } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
@@ -35,8 +35,7 @@ const writeActivityOptionsFile = map => {
|
|
|
35
35
|
export async function loadActivities( target ) {
|
|
36
36
|
const activities = {};
|
|
37
37
|
const activityOptionsMap = {};
|
|
38
|
-
for await ( const { fn, metadata, path } of importComponents( target, [ 'steps.js', 'evaluators.js'
|
|
39
|
-
const isShared = basename( path ) === 'shared_steps.js';
|
|
38
|
+
for await ( const { fn, metadata, path, isShared } of importComponents( target, [ 'steps.js', 'evaluators.js' ] ) ) {
|
|
40
39
|
const prefix = isShared ? SHARED_STEP_PREFIX : dirname( path );
|
|
41
40
|
|
|
42
41
|
console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
|
|
@@ -53,6 +53,27 @@ describe( 'worker/loader', () => {
|
|
|
53
53
|
expect( mkdirSyncMock ).toHaveBeenCalled();
|
|
54
54
|
} );
|
|
55
55
|
|
|
56
|
+
it( 'loadActivities uses SHARED_STEP_PREFIX for components with isShared flag', async () => {
|
|
57
|
+
const { loadActivities } = await import( './loader.js' );
|
|
58
|
+
|
|
59
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
60
|
+
yield { fn: () => 'local', metadata: { name: 'LocalStep' }, path: '/workflows/example/steps.js', isShared: false };
|
|
61
|
+
yield { fn: () => 'shared', metadata: { name: 'SharedStep' }, path: '/shared/steps/tools.js', isShared: true };
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
const activities = await loadActivities( '/root' );
|
|
65
|
+
|
|
66
|
+
// Local step uses dirname as prefix
|
|
67
|
+
expect( activities['/workflows/example#LocalStep'] ).toBeTypeOf( 'function' );
|
|
68
|
+
|
|
69
|
+
// Shared step uses SHARED_STEP_PREFIX ('/shared' from mock)
|
|
70
|
+
expect( activities['/shared#SharedStep'] ).toBeTypeOf( 'function' );
|
|
71
|
+
|
|
72
|
+
// Verify they're different functions
|
|
73
|
+
expect( activities['/workflows/example#LocalStep']() ).toBe( 'local' );
|
|
74
|
+
expect( activities['/shared#SharedStep']() ).toBe( 'shared' );
|
|
75
|
+
} );
|
|
76
|
+
|
|
56
77
|
it( 'loadWorkflows returns array of workflows with metadata', async () => {
|
|
57
78
|
const { loadWorkflows } = await import( './loader.js' );
|
|
58
79
|
|
|
@@ -7,14 +7,51 @@ import { readdirSync } from 'fs';
|
|
|
7
7
|
* @typedef {object} CollectedFile
|
|
8
8
|
* @property {string} path - The file path
|
|
9
9
|
* @property {string} url - The resolved url of the file, ready to be imported
|
|
10
|
+
* @property {boolean} [isShared] - Whether this file is in a shared/steps/ directory
|
|
10
11
|
*/
|
|
11
12
|
/**
|
|
12
13
|
* @typedef {object} Component
|
|
13
14
|
* @property {Function} fn - The loaded component function
|
|
14
15
|
* @property {object} metadata - Associated metadata with the component
|
|
15
|
-
* @property {string} path -
|
|
16
|
+
* @property {string} path - The file path of the component
|
|
17
|
+
* @property {boolean} [isShared] - Whether this component is from a shared/steps/ directory
|
|
16
18
|
*/
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Check if the search is targeting step or evaluator files.
|
|
22
|
+
* @param {string[]} filenames - The filenames being searched for.
|
|
23
|
+
* @returns {boolean} True if searching for steps.js or evaluators.js.
|
|
24
|
+
*/
|
|
25
|
+
const isSearchingForStepFiles = filenames =>
|
|
26
|
+
filenames.includes( 'steps.js' ) || filenames.includes( 'evaluators.js' );
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a directory is a shared steps directory.
|
|
30
|
+
* @param {string} dirName - The directory name.
|
|
31
|
+
* @param {string} parentPath - The parent path.
|
|
32
|
+
* @returns {boolean} True if this is a shared/steps directory.
|
|
33
|
+
*/
|
|
34
|
+
const isSharedStepsDirectory = ( dirName, parentPath ) =>
|
|
35
|
+
dirName === 'steps' && parentPath.endsWith( '/shared' );
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Collect all .js files from a shared steps directory.
|
|
39
|
+
* @param {string} dirPath - The directory path.
|
|
40
|
+
* @param {CollectedFile[]} collection - The collection to add files to.
|
|
41
|
+
*/
|
|
42
|
+
const collectSharedStepsFiles = ( dirPath, collection ) => {
|
|
43
|
+
for ( const entry of readdirSync( dirPath, { withFileTypes: true } ) ) {
|
|
44
|
+
if ( entry.isFile() && entry.name.endsWith( '.js' ) ) {
|
|
45
|
+
const filePath = resolve( dirPath, entry.name );
|
|
46
|
+
collection.push( {
|
|
47
|
+
path: filePath,
|
|
48
|
+
url: pathToFileURL( filePath ).href,
|
|
49
|
+
isShared: true
|
|
50
|
+
} );
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
18
55
|
/**
|
|
19
56
|
* Recursive traverse directories looking for files with given name.
|
|
20
57
|
*
|
|
@@ -30,7 +67,11 @@ const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDi
|
|
|
30
67
|
|
|
31
68
|
const path = resolve( parentPath, entry.name );
|
|
32
69
|
if ( entry.isDirectory() ) {
|
|
33
|
-
|
|
70
|
+
if ( isSharedStepsDirectory( entry.name, parentPath ) && isSearchingForStepFiles( filenames ) ) {
|
|
71
|
+
collectSharedStepsFiles( path, collection );
|
|
72
|
+
} else {
|
|
73
|
+
findByNameRecursively( path, filenames, collection, ignoreDirNames );
|
|
74
|
+
}
|
|
34
75
|
} else if ( filenames.includes( entry.name ) ) {
|
|
35
76
|
collection.push( { path, url: pathToFileURL( path ).href } );
|
|
36
77
|
}
|
|
@@ -50,14 +91,14 @@ const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDi
|
|
|
50
91
|
* @yields {Component}
|
|
51
92
|
*/
|
|
52
93
|
export async function *importComponents( target, filenames ) {
|
|
53
|
-
for ( const { url, path } of findByNameRecursively( target, filenames ) ) {
|
|
94
|
+
for ( const { url, path, isShared } of findByNameRecursively( target, filenames ) ) {
|
|
54
95
|
const imported = await import( url );
|
|
55
96
|
for ( const fn of Object.values( imported ) ) {
|
|
56
97
|
const metadata = fn[METADATA_ACCESS_SYMBOL];
|
|
57
98
|
if ( !metadata ) {
|
|
58
99
|
continue;
|
|
59
100
|
}
|
|
60
|
-
yield { fn, metadata, path };
|
|
101
|
+
yield { fn, metadata, path, isShared };
|
|
61
102
|
}
|
|
62
103
|
}
|
|
63
104
|
};
|
|
@@ -86,4 +86,84 @@ describe( '.importComponents', () => {
|
|
|
86
86
|
|
|
87
87
|
rmSync( root, { recursive: true, force: true } );
|
|
88
88
|
} );
|
|
89
|
+
|
|
90
|
+
it( 'collects all .js files from shared/steps/ directory with isShared flag', async () => {
|
|
91
|
+
const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-shared` );
|
|
92
|
+
const sharedStepsDir = join( root, 'shared', 'steps' );
|
|
93
|
+
const workflowDir = join( root, 'workflows', 'example' );
|
|
94
|
+
mkdirSync( sharedStepsDir, { recursive: true } );
|
|
95
|
+
mkdirSync( workflowDir, { recursive: true } );
|
|
96
|
+
|
|
97
|
+
const sharedToolsFile = join( sharedStepsDir, 'tools.js' );
|
|
98
|
+
const sharedUtilsFile = join( sharedStepsDir, 'utils.js' );
|
|
99
|
+
const workflowStepsFile = join( workflowDir, 'steps.js' );
|
|
100
|
+
|
|
101
|
+
const sharedFileContents = [
|
|
102
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
103
|
+
'export const SharedStep = () => {};',
|
|
104
|
+
'SharedStep[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "sharedStep" };'
|
|
105
|
+
].join( '\n' );
|
|
106
|
+
const workflowFileContents = [
|
|
107
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
108
|
+
'export const LocalStep = () => {};',
|
|
109
|
+
'LocalStep[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "localStep" };'
|
|
110
|
+
].join( '\n' );
|
|
111
|
+
writeFileSync( sharedToolsFile, sharedFileContents );
|
|
112
|
+
writeFileSync( sharedUtilsFile, sharedFileContents );
|
|
113
|
+
writeFileSync( workflowStepsFile, workflowFileContents );
|
|
114
|
+
|
|
115
|
+
const collected = [];
|
|
116
|
+
for await ( const m of importComponents( root, [ 'steps.js' ] ) ) {
|
|
117
|
+
collected.push( m );
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
expect( collected.length ).toBe( 3 );
|
|
121
|
+
|
|
122
|
+
const sharedComponents = collected.filter( m => m.isShared === true );
|
|
123
|
+
const localComponents = collected.filter( m => !m.isShared );
|
|
124
|
+
|
|
125
|
+
expect( sharedComponents.length ).toBe( 2 );
|
|
126
|
+
expect( localComponents.length ).toBe( 1 );
|
|
127
|
+
|
|
128
|
+
expect( sharedComponents.map( m => m.path ).sort() ).toEqual( [ sharedToolsFile, sharedUtilsFile ].sort() );
|
|
129
|
+
expect( localComponents[0].path ).toBe( workflowStepsFile );
|
|
130
|
+
|
|
131
|
+
rmSync( root, { recursive: true, force: true } );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'does not collect shared/steps/ files when searching for workflow.js', async () => {
|
|
135
|
+
const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-workflow-exclude` );
|
|
136
|
+
const sharedStepsDir = join( root, 'shared', 'steps' );
|
|
137
|
+
const workflowDir = join( root, 'workflows', 'example' );
|
|
138
|
+
mkdirSync( sharedStepsDir, { recursive: true } );
|
|
139
|
+
mkdirSync( workflowDir, { recursive: true } );
|
|
140
|
+
|
|
141
|
+
const sharedToolsFile = join( sharedStepsDir, 'tools.js' );
|
|
142
|
+
const workflowFile = join( workflowDir, 'workflow.js' );
|
|
143
|
+
|
|
144
|
+
const sharedFileContents = [
|
|
145
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
146
|
+
'export const SharedStep = () => {};',
|
|
147
|
+
'SharedStep[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "sharedStep" };'
|
|
148
|
+
].join( '\n' );
|
|
149
|
+
const workflowFileContents = [
|
|
150
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
151
|
+
'export default function myWorkflow() {};',
|
|
152
|
+
'myWorkflow[METADATA_ACCESS_SYMBOL] = { kind: "workflow", name: "myWorkflow" };'
|
|
153
|
+
].join( '\n' );
|
|
154
|
+
writeFileSync( sharedToolsFile, sharedFileContents );
|
|
155
|
+
writeFileSync( workflowFile, workflowFileContents );
|
|
156
|
+
|
|
157
|
+
const collected = [];
|
|
158
|
+
for await ( const m of importComponents( root, [ 'workflow.js' ] ) ) {
|
|
159
|
+
collected.push( m );
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expect( collected.length ).toBe( 1 );
|
|
163
|
+
expect( collected[0].path ).toBe( workflowFile );
|
|
164
|
+
expect( collected[0].metadata.kind ).toBe( 'workflow' );
|
|
165
|
+
expect( collected[0].isShared ).toBeUndefined();
|
|
166
|
+
|
|
167
|
+
rmSync( root, { recursive: true, force: true } );
|
|
168
|
+
} );
|
|
89
169
|
} );
|
|
@@ -5,23 +5,9 @@ export const NodeType = {
|
|
|
5
5
|
export const ComponentFile = {
|
|
6
6
|
EVALUATORS: 'evaluators',
|
|
7
7
|
STEPS: 'steps',
|
|
8
|
-
SHARED_STEPS: 'shared_steps',
|
|
9
8
|
WORKFLOW: 'workflow'
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
export const EXTRANEOUS_FILE = 'extraneous';
|
|
13
|
-
export const ExtraneousFileList = [
|
|
14
|
-
'types',
|
|
15
|
-
'consts',
|
|
16
|
-
'constants',
|
|
17
|
-
'vars',
|
|
18
|
-
'variables',
|
|
19
|
-
'utils',
|
|
20
|
-
'tools',
|
|
21
|
-
'functions',
|
|
22
|
-
'shared'
|
|
23
|
-
];
|
|
24
|
-
|
|
25
11
|
export const CoreModule = {
|
|
26
12
|
LOCAL: 'local_core',
|
|
27
13
|
NPM: '@output.ai/core'
|
|
@@ -24,7 +24,15 @@ import {
|
|
|
24
24
|
isExportDefaultDeclaration,
|
|
25
25
|
isFunctionDeclaration
|
|
26
26
|
} from '@babel/types';
|
|
27
|
-
import { ComponentFile,
|
|
27
|
+
import { ComponentFile, NodeType } from './consts.js';
|
|
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\//;
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
38
|
* Resolve a relative module specifier against a base directory.
|
|
@@ -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,35 +182,29 @@ 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
|
|
140
|
-
|
|
141
|
-
export const isExtraneousFile = value => ExtraneousFileList.map( t => new RegExp( `(^|\/)${t}\\.js$` ) ).find( rx => rx.test( value ) );
|
|
189
|
+
export const isComponentFile = value =>
|
|
190
|
+
isAnyStepsPath( value ) || isEvaluatorsPath( value ) || isWorkflowPath( value );
|
|
142
191
|
|
|
143
192
|
/**
|
|
144
193
|
* Determine file kind based on its path.
|
|
145
|
-
*
|
|
146
|
-
* @
|
|
194
|
+
* Returns the component type if it's a component file, null otherwise.
|
|
195
|
+
* @param {string} path
|
|
196
|
+
* @returns {'workflow'|'steps'|'evaluators'|null}
|
|
147
197
|
*/
|
|
148
198
|
export const getFileKind = path => {
|
|
149
|
-
if (
|
|
199
|
+
if ( isAnyStepsPath( path ) ) {
|
|
150
200
|
return ComponentFile.STEPS;
|
|
151
201
|
}
|
|
152
|
-
if ( isSharedStepsPath( path ) ) {
|
|
153
|
-
return ComponentFile.SHARED_STEPS;
|
|
154
|
-
}
|
|
155
202
|
if ( isEvaluatorsPath( path ) ) {
|
|
156
203
|
return ComponentFile.EVALUATORS;
|
|
157
204
|
}
|
|
158
205
|
if ( isWorkflowPath( path ) ) {
|
|
159
206
|
return ComponentFile.WORKFLOW;
|
|
160
207
|
}
|
|
161
|
-
if ( isExtraneousFile( path ) ) {
|
|
162
|
-
return EXTRANEOUS_FILE;
|
|
163
|
-
}
|
|
164
208
|
return null;
|
|
165
209
|
};
|
|
166
210
|
|
|
@@ -283,12 +327,12 @@ export const buildStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
|
283
327
|
|
|
284
328
|
/**
|
|
285
329
|
* Build a map from exported shared step identifier to declared step name.
|
|
286
|
-
*
|
|
287
|
-
* Uses the same factory as regular steps.
|
|
330
|
+
* Same as buildStepsNameMap but for shared steps.
|
|
288
331
|
*
|
|
289
332
|
* @param {string} path - Absolute path to the shared steps module file.
|
|
290
|
-
* @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.
|
|
291
334
|
* @returns {Map<string,string>} Exported identifier -> step name.
|
|
335
|
+
* @throws {Error} When a step name is invalid (non-static or missing).
|
|
292
336
|
*/
|
|
293
337
|
export const buildSharedStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
294
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,
|
|
@@ -20,7 +22,6 @@ import {
|
|
|
20
22
|
buildSharedStepsNameMap,
|
|
21
23
|
buildWorkflowNameMap,
|
|
22
24
|
buildEvaluatorsNameMap,
|
|
23
|
-
isExtraneousFile,
|
|
24
25
|
getFileKind
|
|
25
26
|
} from './tools.js';
|
|
26
27
|
|
|
@@ -138,14 +139,49 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
138
139
|
expect( t.isFunctionExpression( fn2 ) ).toBe( true );
|
|
139
140
|
} );
|
|
140
141
|
|
|
141
|
-
it( 'isStepsPath: matches steps.js
|
|
142
|
+
it( 'isStepsPath: matches LOCAL steps.js (no path traversal)', () => {
|
|
143
|
+
// Local steps (without ../ or /shared/)
|
|
142
144
|
expect( isStepsPath( 'steps.js' ) ).toBe( true );
|
|
143
145
|
expect( isStepsPath( './steps.js' ) ).toBe( true );
|
|
144
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
|
|
145
152
|
expect( isStepsPath( 'steps.ts' ) ).toBe( false );
|
|
146
153
|
expect( isStepsPath( 'workflow.js' ) ).toBe( false );
|
|
147
154
|
} );
|
|
148
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
|
+
|
|
149
185
|
it( 'isWorkflowPath: matches workflow.js at root or subpath', () => {
|
|
150
186
|
expect( isWorkflowPath( 'workflow.js' ) ).toBe( true );
|
|
151
187
|
expect( isWorkflowPath( './workflow.js' ) ).toBe( true );
|
|
@@ -154,63 +190,29 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
154
190
|
expect( isWorkflowPath( 'steps.js' ) ).toBe( false );
|
|
155
191
|
} );
|
|
156
192
|
|
|
157
|
-
it( 'isSharedStepsPath: matches shared_steps.js at root or subpath', () => {
|
|
158
|
-
expect( isSharedStepsPath( 'shared_steps.js' ) ).toBe( true );
|
|
159
|
-
expect( isSharedStepsPath( './shared_steps.js' ) ).toBe( true );
|
|
160
|
-
expect( isSharedStepsPath( '/a/b/shared_steps.js' ) ).toBe( true );
|
|
161
|
-
expect( isSharedStepsPath( 'shared_steps.ts' ) ).toBe( false );
|
|
162
|
-
expect( isSharedStepsPath( 'evaluators.js' ) ).toBe( false );
|
|
163
|
-
} );
|
|
164
|
-
|
|
165
193
|
it( 'isEvaluatorsPath: matches evaluators.js at root or subpath', () => {
|
|
166
194
|
expect( isEvaluatorsPath( 'evaluators.js' ) ).toBe( true );
|
|
167
195
|
expect( isEvaluatorsPath( './evaluators.js' ) ).toBe( true );
|
|
168
196
|
expect( isEvaluatorsPath( '/a/b/evaluators.js' ) ).toBe( true );
|
|
197
|
+
expect( isEvaluatorsPath( './evaluators/quality.js' ) ).toBe( true );
|
|
169
198
|
expect( isEvaluatorsPath( 'evaluators.ts' ) ).toBe( false );
|
|
170
199
|
expect( isEvaluatorsPath( 'steps.js' ) ).toBe( false );
|
|
171
200
|
} );
|
|
172
201
|
|
|
173
|
-
it( '
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
'variables.js',
|
|
188
|
-
'./variables.js',
|
|
189
|
-
'/a/b/variables.js'
|
|
190
|
-
];
|
|
191
|
-
for ( const p of ok ) {
|
|
192
|
-
expect( Boolean( isExtraneousFile( p ) ) ).toBe( true );
|
|
193
|
-
}
|
|
194
|
-
} );
|
|
195
|
-
|
|
196
|
-
it( 'isExtraneousFile: returns falsy for non-extraneous or non-.js files', () => {
|
|
197
|
-
const bad = [
|
|
198
|
-
'types.ts',
|
|
199
|
-
'/a/b/types.ts',
|
|
200
|
-
'types.mjs',
|
|
201
|
-
'variables.jsx',
|
|
202
|
-
'variables.mjs',
|
|
203
|
-
'myconstants.js',
|
|
204
|
-
'steps.js',
|
|
205
|
-
'evaluators.js',
|
|
206
|
-
'shared_steps.js',
|
|
207
|
-
'workflow.js',
|
|
208
|
-
'typess.js',
|
|
209
|
-
'/a/b/c/variables.json'
|
|
210
|
-
];
|
|
211
|
-
for ( const p of bad ) {
|
|
212
|
-
expect( isExtraneousFile( p ) ).toBeFalsy();
|
|
213
|
-
}
|
|
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 );
|
|
214
216
|
} );
|
|
215
217
|
|
|
216
218
|
it( 'createThisMethodCall: builds this.method(\'name\', ...args) call', () => {
|
|
@@ -223,9 +225,10 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
223
225
|
expect( call.arguments.length ).toBe( 3 );
|
|
224
226
|
} );
|
|
225
227
|
|
|
226
|
-
it( 'buildSharedStepsNameMap: reads names from
|
|
228
|
+
it( 'buildSharedStepsNameMap: reads names from shared steps module and caches result', () => {
|
|
227
229
|
const dir = mkdtempSync( join( tmpdir(), 'tools-shared-steps-' ) );
|
|
228
|
-
|
|
230
|
+
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
231
|
+
const stepsPath = join( dir, 'shared', 'steps', 'common.js' );
|
|
229
232
|
writeFileSync( stepsPath, [
|
|
230
233
|
'export const StepA = step({ name: "shared.step.a" })',
|
|
231
234
|
'export const StepB = step({ name: "shared.step.b" })'
|
|
@@ -243,9 +246,13 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
243
246
|
it( 'getFileKind: classifies file by its path', () => {
|
|
244
247
|
expect( getFileKind( '/p/workflow.js' ) ).toBe( 'workflow' );
|
|
245
248
|
expect( getFileKind( '/p/steps.js' ) ).toBe( 'steps' );
|
|
246
|
-
|
|
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' );
|
|
247
252
|
expect( getFileKind( '/p/evaluators.js' ) ).toBe( 'evaluators' );
|
|
253
|
+
expect( getFileKind( '/p/evaluators/quality.js' ) ).toBe( 'evaluators' );
|
|
248
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 );
|
|
249
257
|
} );
|
|
250
258
|
} );
|
|
251
|
-
|
|
@@ -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
|
|