@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
package/package.json
CHANGED
package/src/consts.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
|
|
2
2
|
export const ACTIVITY_GET_TRACE_DESTINATIONS = '__internal#getTraceDestinations';
|
|
3
3
|
export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
4
|
-
export const SHARED_STEP_PREFIX = '
|
|
4
|
+
export const SHARED_STEP_PREFIX = '$shared';
|
|
5
5
|
export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
|
|
6
6
|
export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
|
|
7
7
|
export const WORKFLOW_CATALOG = '$catalog';
|
|
@@ -3,7 +3,7 @@ import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4,
|
|
|
3
3
|
import { validateWorkflow } from './validations/static.js';
|
|
4
4
|
import { validateWithSchema } from './validations/runtime.js';
|
|
5
5
|
import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
|
|
6
|
-
import { deepMerge, mergeActivityOptions,
|
|
6
|
+
import { deepMerge, mergeActivityOptions, setMetadata } from '#utils';
|
|
7
7
|
import { FatalError, ValidationError } from '#errors';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -51,7 +51,6 @@ const defaultActivityOptions = {
|
|
|
51
51
|
|
|
52
52
|
export function workflow( { name, description, inputSchema, outputSchema, fn, options } ) {
|
|
53
53
|
validateWorkflow( { name, description, inputSchema, outputSchema, fn, options } );
|
|
54
|
-
const workflowPath = resolveInvocationDir();
|
|
55
54
|
|
|
56
55
|
const activityOptions = mergeActivityOptions( defaultActivityOptions, options );
|
|
57
56
|
const steps = proxyActivities( activityOptions );
|
|
@@ -99,9 +98,9 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
99
98
|
|
|
100
99
|
// binds the methods called in the code that Webpack loader will add, they will exposed via "this"
|
|
101
100
|
const output = await fn.call( {
|
|
102
|
-
invokeStep: async ( stepName, input, options ) => steps[`${
|
|
101
|
+
invokeStep: async ( stepName, input, options ) => steps[`${name}#${stepName}`]( input, options ),
|
|
103
102
|
invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
|
|
104
|
-
invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${
|
|
103
|
+
invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${name}#${evaluatorName}`]( input, options ),
|
|
105
104
|
|
|
106
105
|
/**
|
|
107
106
|
* Start a child workflow
|
package/src/worker/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const callerDir = process.argv[2];
|
|
|
18
18
|
const workflows = await loadWorkflows( callerDir );
|
|
19
19
|
|
|
20
20
|
console.log( '[Core]', 'Loading activities...', { callerDir } );
|
|
21
|
-
const activities = await loadActivities( callerDir );
|
|
21
|
+
const activities = await loadActivities( callerDir, workflows );
|
|
22
22
|
|
|
23
23
|
console.log( '[Core]', 'Creating worker entry point...' );
|
|
24
24
|
const workflowsPath = createWorkflowsEntryPoint( workflows );
|
package/src/worker/loader.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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';
|
|
5
5
|
import { getTraceDestinations, sendHttpRequest } from '#internal_activities';
|
|
6
|
-
import { importComponents } from './loader_tools.js';
|
|
6
|
+
import { importComponents, staticMatchers, activityMatchersBuilder } from './loader_tools.js';
|
|
7
7
|
import {
|
|
8
8
|
ACTIVITY_SEND_HTTP_REQUEST,
|
|
9
9
|
ACTIVITY_OPTIONS_FILENAME,
|
|
@@ -27,25 +27,61 @@ const writeActivityOptionsFile = map => {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Creates the activity key that will identify it on Temporal.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
32
|
+
* It composes it using a namespace and the name of the activity.
|
|
33
|
+
*
|
|
34
|
+
* No two activities with the same name can exist on the same namespace.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} options
|
|
37
|
+
* @param {string} namespace
|
|
38
|
+
* @param {string} activityName
|
|
39
|
+
* @returns {string} key
|
|
40
|
+
*/
|
|
41
|
+
const generateActivityKey = ( { namespace, activityName } ) => `${namespace}#${activityName}`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load activities:
|
|
45
|
+
*
|
|
46
|
+
* - Scans activities based on workflows, using each workflow folder as a point to lookup for steps, evaluators files;
|
|
47
|
+
* - Scans shared activities in the rootDir;
|
|
48
|
+
* - Loads internal activities as well;
|
|
49
|
+
*
|
|
50
|
+
* Builds a map of activities, where they is generated according to the type of activity and the value is the function itself and return it.
|
|
51
|
+
* - Shared activity keys have a common prefix followed by the activity name;
|
|
52
|
+
* - Internal activities are registered with a fixed key;
|
|
53
|
+
* - Workflow activities keys are composed using the workflow name and the activity name;
|
|
54
|
+
*
|
|
55
|
+
* @param {string} rootDir
|
|
56
|
+
* @param {object[]} workflows
|
|
33
57
|
* @returns {object}
|
|
34
58
|
*/
|
|
35
|
-
export async function loadActivities(
|
|
59
|
+
export async function loadActivities( rootDir, workflows ) {
|
|
36
60
|
const activities = {};
|
|
37
61
|
const activityOptionsMap = {};
|
|
38
|
-
for await ( const { fn, metadata, path } of importComponents( target, [ 'steps.js', 'evaluators.js', 'shared_steps.js' ] ) ) {
|
|
39
|
-
const isShared = basename( path ) === 'shared_steps.js';
|
|
40
|
-
const prefix = isShared ? SHARED_STEP_PREFIX : dirname( path );
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
// Load workflow based activities
|
|
64
|
+
for ( const { path: workflowPath, name: workflowName } of workflows ) {
|
|
65
|
+
const dir = dirname( workflowPath );
|
|
66
|
+
for await ( const { fn, metadata, path } of importComponents( dir, Object.values( activityMatchersBuilder( dir ) ) ) ) {
|
|
67
|
+
console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
|
|
68
|
+
// Activities loaded from a workflow path will use the workflow name as a namespace, which is unique across the platform, avoiding collision
|
|
69
|
+
const activityKey = generateActivityKey( { namespace: workflowName, activityName: metadata.name } );
|
|
70
|
+
activities[activityKey] = fn;
|
|
71
|
+
// propagate the custom options set on the step()/evaluator() constructor
|
|
72
|
+
activityOptionsMap[activityKey] = metadata.options ?? undefined;
|
|
46
73
|
}
|
|
47
74
|
}
|
|
48
75
|
|
|
76
|
+
// Load shared activities/evaluators
|
|
77
|
+
for await ( const { fn, metadata, path } of importComponents( rootDir, [ staticMatchers.sharedStepsDir, staticMatchers.sharedEvaluatorsDir ] ) ) {
|
|
78
|
+
console.log( '[Core.Scanner]', 'Component loaded: shared ', metadata.type, metadata.name, 'at', path );
|
|
79
|
+
// The namespace for shared activities is fixed
|
|
80
|
+
const activityKey = generateActivityKey( { namespace: SHARED_STEP_PREFIX, activityName: metadata.name } );
|
|
81
|
+
activities[activityKey] = fn;
|
|
82
|
+
activityOptionsMap[activityKey] = metadata.options ?? undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
49
85
|
// writes down the activity option overrides
|
|
50
86
|
writeActivityOptionsFile( activityOptionsMap );
|
|
51
87
|
|
|
@@ -56,14 +92,19 @@ export async function loadActivities( target ) {
|
|
|
56
92
|
};
|
|
57
93
|
|
|
58
94
|
/**
|
|
59
|
-
*
|
|
95
|
+
* Scan and find workflow.js files and import them.
|
|
96
|
+
*
|
|
97
|
+
* Creates an array containing their metadata and path and return it.
|
|
60
98
|
*
|
|
61
|
-
* @param {string}
|
|
99
|
+
* @param {string} rootDir
|
|
62
100
|
* @returns {object[]}
|
|
63
101
|
*/
|
|
64
|
-
export async function loadWorkflows(
|
|
102
|
+
export async function loadWorkflows( rootDir ) {
|
|
65
103
|
const workflows = [];
|
|
66
|
-
for await ( const { metadata, path } of importComponents(
|
|
104
|
+
for await ( const { metadata, path } of importComponents( rootDir, [ staticMatchers.workflowFile ] ) ) {
|
|
105
|
+
if ( staticMatchers.workflowPathHasShared( path ) ) {
|
|
106
|
+
throw new Error( 'Workflow directory can\'t be named "shared"' );
|
|
107
|
+
}
|
|
67
108
|
console.log( '[Core.Scanner]', 'Workflow loaded:', metadata.name, 'at', path );
|
|
68
109
|
workflows.push( { ...metadata, path } );
|
|
69
110
|
}
|
|
@@ -71,7 +112,7 @@ export async function loadWorkflows( target ) {
|
|
|
71
112
|
};
|
|
72
113
|
|
|
73
114
|
/**
|
|
74
|
-
* Creates a temporary index file importing all workflows
|
|
115
|
+
* Creates a temporary index file importing all workflows for Temporal.
|
|
75
116
|
*
|
|
76
117
|
* @param {object[]} workflows
|
|
77
118
|
* @returns
|
|
@@ -6,7 +6,7 @@ vi.mock( '#consts', () => ( {
|
|
|
6
6
|
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
7
7
|
WORKFLOW_CATALOG: 'catalog',
|
|
8
8
|
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js',
|
|
9
|
-
SHARED_STEP_PREFIX: '
|
|
9
|
+
SHARED_STEP_PREFIX: '$shared'
|
|
10
10
|
} ) );
|
|
11
11
|
|
|
12
12
|
const sendHttpRequestMock = vi.fn();
|
|
@@ -17,7 +17,10 @@ vi.mock( '#internal_activities', () => ( {
|
|
|
17
17
|
} ) );
|
|
18
18
|
|
|
19
19
|
const importComponentsMock = vi.fn();
|
|
20
|
-
vi.mock( './loader_tools.js',
|
|
20
|
+
vi.mock( './loader_tools.js', async importOriginal => {
|
|
21
|
+
const actual = await importOriginal();
|
|
22
|
+
return { ...actual, importComponents: importComponentsMock };
|
|
23
|
+
} );
|
|
21
24
|
|
|
22
25
|
const mkdirSyncMock = vi.fn();
|
|
23
26
|
const writeFileSyncMock = vi.fn();
|
|
@@ -34,12 +37,16 @@ describe( 'worker/loader', () => {
|
|
|
34
37
|
it( 'loadActivities returns map including system activity and writes options file', async () => {
|
|
35
38
|
const { loadActivities } = await import( './loader.js' );
|
|
36
39
|
|
|
40
|
+
// First call: workflow directory scan
|
|
37
41
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
38
42
|
yield { fn: () => {}, metadata: { name: 'Act1', options: { retry: { maximumAttempts: 3 } } }, path: '/a/steps.js' };
|
|
39
43
|
} );
|
|
44
|
+
// Second call: shared activities scan (no results)
|
|
45
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
40
46
|
|
|
41
|
-
const
|
|
42
|
-
|
|
47
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
48
|
+
const activities = await loadActivities( '/root', workflows );
|
|
49
|
+
expect( activities['A#Act1'] ).toBeTypeOf( 'function' );
|
|
43
50
|
expect( activities['__internal#sendHttpRequest'] ).toBe( sendHttpRequestMock );
|
|
44
51
|
|
|
45
52
|
// options file written with the collected map
|
|
@@ -48,7 +55,7 @@ describe( 'worker/loader', () => {
|
|
|
48
55
|
expect( writtenPath ).toMatch( /temp\/__activity_options\.js$/ );
|
|
49
56
|
expect( contents ).toContain( 'export default' );
|
|
50
57
|
expect( JSON.parse( contents.replace( /^export default\s*/, '' ).replace( /;\s*$/, '' ) ) ).toEqual( {
|
|
51
|
-
'
|
|
58
|
+
'A#Act1': { retry: { maximumAttempts: 3 } }
|
|
52
59
|
} );
|
|
53
60
|
expect( mkdirSyncMock ).toHaveBeenCalled();
|
|
54
61
|
} );
|
|
@@ -77,4 +84,98 @@ describe( 'worker/loader', () => {
|
|
|
77
84
|
expect( contents ).toContain( 'export { default as catalog }' );
|
|
78
85
|
expect( mkdirSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
79
86
|
} );
|
|
87
|
+
|
|
88
|
+
it( 'loadActivities uses folder-based matchers for steps/evaluators and shared', async () => {
|
|
89
|
+
const { loadActivities } = await import( './loader.js' );
|
|
90
|
+
// First call (workflow dir): no results
|
|
91
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
92
|
+
// Second call (shared): no results
|
|
93
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
94
|
+
|
|
95
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
96
|
+
await loadActivities( '/root', workflows );
|
|
97
|
+
|
|
98
|
+
// First invocation should target the workflow directory with folder/file matchers
|
|
99
|
+
expect( importComponentsMock ).toHaveBeenCalledTimes( 2 );
|
|
100
|
+
const [ firstDir, firstMatchers ] = importComponentsMock.mock.calls[0];
|
|
101
|
+
expect( firstDir ).toBe( '/a' );
|
|
102
|
+
expect( Array.isArray( firstMatchers ) ).toBe( true );
|
|
103
|
+
// Should match folder-based steps and evaluators files
|
|
104
|
+
expect( firstMatchers.some( fn => fn( '/a/steps/foo.js' ) ) ).toBe( true );
|
|
105
|
+
expect( firstMatchers.some( fn => fn( '/a/evaluators/bar.js' ) ) ).toBe( true );
|
|
106
|
+
// And also direct file names
|
|
107
|
+
expect( firstMatchers.some( fn => fn( '/a/steps.js' ) ) ).toBe( true );
|
|
108
|
+
expect( firstMatchers.some( fn => fn( '/a/evaluators.js' ) ) ).toBe( true );
|
|
109
|
+
|
|
110
|
+
// Second invocation should target root with shared matchers
|
|
111
|
+
const [ secondDir, secondMatchers ] = importComponentsMock.mock.calls[1];
|
|
112
|
+
expect( secondDir ).toBe( '/root' );
|
|
113
|
+
expect( secondMatchers.some( fn => fn( '/root/shared/steps/baz.js' ) ) ).toBe( true );
|
|
114
|
+
expect( secondMatchers.some( fn => fn( '/root/shared/evaluators/qux.js' ) ) ).toBe( true );
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
it( 'loadActivities includes nested workflow steps and shared evaluators', async () => {
|
|
118
|
+
const { loadActivities } = await import( './loader.js' );
|
|
119
|
+
// Workflow dir scan returns a nested step
|
|
120
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
121
|
+
yield { fn: () => {}, metadata: { name: 'ActNested' }, path: '/a/steps/foo.js' };
|
|
122
|
+
} );
|
|
123
|
+
// Shared scan returns a shared evaluator
|
|
124
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
125
|
+
yield { fn: () => {}, metadata: { name: 'SharedEval' }, path: '/root/shared/evaluators/bar.js' };
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
129
|
+
const activities = await loadActivities( '/root', workflows );
|
|
130
|
+
expect( activities['A#ActNested'] ).toBeTypeOf( 'function' );
|
|
131
|
+
expect( activities['$shared#SharedEval'] ).toBeTypeOf( 'function' );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'loadWorkflows throws when workflow is under shared directory', async () => {
|
|
135
|
+
const { loadWorkflows } = await import( './loader.js' );
|
|
136
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
137
|
+
yield { metadata: { name: 'Invalid' }, path: '/root/shared/workflow.js' };
|
|
138
|
+
} );
|
|
139
|
+
await expect( loadWorkflows( '/root' ) ).rejects.toThrow( 'Workflow directory can\'t be named \"shared\"' );
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
it( 'collects workflow nested steps and evaluators across multiple subfolders', async () => {
|
|
143
|
+
const { loadActivities } = await import( './loader.js' );
|
|
144
|
+
// Workflow dir scan returns nested steps and evaluators
|
|
145
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
146
|
+
yield { fn: () => {}, metadata: { name: 'StepPrimary' }, path: '/a/steps/primary/foo.js' };
|
|
147
|
+
yield { fn: () => {}, metadata: { name: 'StepSecondary' }, path: '/a/steps/secondary/bar.js' };
|
|
148
|
+
yield { fn: () => {}, metadata: { name: 'EvalPrimary' }, path: '/a/evaluators/primary/baz.js' };
|
|
149
|
+
yield { fn: () => {}, metadata: { name: 'EvalSecondary' }, path: '/a/evaluators/secondary/qux.js' };
|
|
150
|
+
} );
|
|
151
|
+
// Shared scan returns nothing for this test
|
|
152
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
153
|
+
|
|
154
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
155
|
+
const activities = await loadActivities( '/root', workflows );
|
|
156
|
+
expect( activities['A#StepPrimary'] ).toBeTypeOf( 'function' );
|
|
157
|
+
expect( activities['A#StepSecondary'] ).toBeTypeOf( 'function' );
|
|
158
|
+
expect( activities['A#EvalPrimary'] ).toBeTypeOf( 'function' );
|
|
159
|
+
expect( activities['A#EvalSecondary'] ).toBeTypeOf( 'function' );
|
|
160
|
+
} );
|
|
161
|
+
|
|
162
|
+
it( 'collects shared nested steps and evaluators across multiple subfolders', async () => {
|
|
163
|
+
const { loadActivities } = await import( './loader.js' );
|
|
164
|
+
// Workflow dir scan returns nothing for this test
|
|
165
|
+
importComponentsMock.mockImplementationOnce( async function *() {} );
|
|
166
|
+
// Shared scan returns nested steps and evaluators
|
|
167
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
168
|
+
yield { fn: () => {}, metadata: { name: 'SharedStepPrimary' }, path: '/root/shared/steps/primary/a.js' };
|
|
169
|
+
yield { fn: () => {}, metadata: { name: 'SharedStepSecondary' }, path: '/root/shared/steps/secondary/b.js' };
|
|
170
|
+
yield { fn: () => {}, metadata: { name: 'SharedEvalPrimary' }, path: '/root/shared/evaluators/primary/c.js' };
|
|
171
|
+
yield { fn: () => {}, metadata: { name: 'SharedEvalSecondary' }, path: '/root/shared/evaluators/secondary/d.js' };
|
|
172
|
+
} );
|
|
173
|
+
|
|
174
|
+
const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
|
|
175
|
+
const activities = await loadActivities( '/root', workflows );
|
|
176
|
+
expect( activities['$shared#SharedStepPrimary'] ).toBeTypeOf( 'function' );
|
|
177
|
+
expect( activities['$shared#SharedStepSecondary'] ).toBeTypeOf( 'function' );
|
|
178
|
+
expect( activities['$shared#SharedEvalPrimary'] ).toBeTypeOf( 'function' );
|
|
179
|
+
expect( activities['$shared#SharedEvalSecondary'] ).toBeTypeOf( 'function' );
|
|
180
|
+
} );
|
|
80
181
|
} );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from 'path';
|
|
1
|
+
import { resolve, sep } from 'path';
|
|
2
2
|
import { pathToFileURL } from 'url';
|
|
3
3
|
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
4
|
import { readdirSync } from 'fs';
|
|
@@ -16,13 +16,14 @@ import { readdirSync } from 'fs';
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Recursive traverse directories
|
|
19
|
+
* Recursive traverse directories collection files with paths that match one of the given matches.
|
|
20
20
|
*
|
|
21
21
|
* @param {string} path - The path to scan
|
|
22
|
-
* @param {
|
|
22
|
+
* @param {function[]} matchers - Boolean functions to match files to add to collection
|
|
23
23
|
* @returns {CollectedFile[]} An array containing the collected files
|
|
24
|
-
|
|
25
|
-
const findByNameRecursively = ( parentPath,
|
|
24
|
+
*/
|
|
25
|
+
const findByNameRecursively = ( parentPath, matchers, ignoreDirNames = [ 'vendor', 'node_modules' ] ) => {
|
|
26
|
+
const collection = [];
|
|
26
27
|
for ( const entry of readdirSync( parentPath, { withFileTypes: true } ) ) {
|
|
27
28
|
if ( ignoreDirNames.includes( entry.name ) ) {
|
|
28
29
|
continue;
|
|
@@ -30,8 +31,8 @@ const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDi
|
|
|
30
31
|
|
|
31
32
|
const path = resolve( parentPath, entry.name );
|
|
32
33
|
if ( entry.isDirectory() ) {
|
|
33
|
-
findByNameRecursively( path,
|
|
34
|
-
} else if (
|
|
34
|
+
collection.push( ...findByNameRecursively( path, matchers ) );
|
|
35
|
+
} else if ( matchers.some( m => m( path ) ) ) {
|
|
35
36
|
collection.push( { path, url: pathToFileURL( path ).href } );
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -40,17 +41,22 @@ const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDi
|
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* Scan a path for files testing each path against a matching function.
|
|
45
|
+
*
|
|
46
|
+
* For each file found, dynamic import it and for each exports on that file, yields it.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* - Only yields exports that have the METADATA_ACCESS_SYMBOL, as they are output components (steps, evaluators, etc).
|
|
44
50
|
*
|
|
45
51
|
* @generator
|
|
46
52
|
* @async
|
|
47
53
|
* @function importComponents
|
|
48
54
|
* @param {string} target - Place to look for files
|
|
49
|
-
* @param {
|
|
55
|
+
* @param {function[]} matchers - Boolean functions to match files
|
|
50
56
|
* @yields {Component}
|
|
51
57
|
*/
|
|
52
|
-
export async function *importComponents( target,
|
|
53
|
-
for ( const { url, path } of findByNameRecursively( target,
|
|
58
|
+
export async function *importComponents( target, matchers ) {
|
|
59
|
+
for ( const { url, path } of findByNameRecursively( target, matchers ) ) {
|
|
54
60
|
const imported = await import( url );
|
|
55
61
|
for ( const fn of Object.values( imported ) ) {
|
|
56
62
|
const metadata = fn[METADATA_ACCESS_SYMBOL];
|
|
@@ -61,3 +67,66 @@ export async function *importComponents( target, filenames ) {
|
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns matchers that need to be built using a relative path
|
|
73
|
+
*
|
|
74
|
+
* @param {string} path
|
|
75
|
+
* @returns {object} The object containing the matchers
|
|
76
|
+
*/
|
|
77
|
+
export const activityMatchersBuilder = path => ( {
|
|
78
|
+
/**
|
|
79
|
+
* Matches a file called steps.js, located at the path
|
|
80
|
+
* @param {string} path - Path to test
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
stepsFile: v => v === `${path}${sep}steps.js`,
|
|
84
|
+
/**
|
|
85
|
+
* Matches a file called evaluators.js, located at the path
|
|
86
|
+
* @param {string} path - Path to test
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
evaluatorsFile: v => v === `${path}${sep}evaluators.js`,
|
|
90
|
+
/**
|
|
91
|
+
* Matches all files on any levels inside a folder called steps/, located at the path
|
|
92
|
+
* @param {string} path - Path to test
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
stepsDir: v => v.startsWith( `${path}${sep}steps${sep}` ),
|
|
96
|
+
/**
|
|
97
|
+
* Matches all files on any levels inside a folder called evaluators/, located at the path
|
|
98
|
+
* @param {string} path - Path to test
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
evaluatorsDir: v => v.startsWith( `${path}${sep}evaluators${sep}` )
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Matchers that can be used to access conditions without initializing them
|
|
106
|
+
*/
|
|
107
|
+
export const staticMatchers = {
|
|
108
|
+
/**
|
|
109
|
+
* Matches a workflow.js file
|
|
110
|
+
* @param {string} path - Path to test
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
workflowFile: v => v.endsWith( `${sep}workflow.js` ),
|
|
114
|
+
/**
|
|
115
|
+
* Matches a workflow.js that is inside a shared folder: eg foo/shared/workflow.js
|
|
116
|
+
* @param {string} path - Path to test
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
workflowPathHasShared: v => v.endsWith( `${sep}shared${sep}workflow.js` ),
|
|
120
|
+
/**
|
|
121
|
+
* Matches the shared folder for steps src/shared/steps/../step_file.js
|
|
122
|
+
* @param {string} path - Path to test
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
sharedStepsDir: v => v.includes( `${sep}shared${sep}steps${sep}` ),
|
|
126
|
+
/**
|
|
127
|
+
* Matches the shared folder for evaluators src/shared/evaluators/../evaluator_file.js
|
|
128
|
+
* @param {string} path - Path to test
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
sharedEvaluatorsDir: v => v.includes( `${sep}shared${sep}evaluators${sep}` )
|
|
132
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
2
|
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, sep } from 'node:path';
|
|
4
4
|
import { importComponents } from './loader_tools.js';
|
|
5
5
|
|
|
6
6
|
describe( '.importComponents', () => {
|
|
@@ -21,7 +21,7 @@ describe( '.importComponents', () => {
|
|
|
21
21
|
].join( '\n' ) );
|
|
22
22
|
|
|
23
23
|
const collected = [];
|
|
24
|
-
for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
|
|
24
|
+
for await ( const m of importComponents( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) {
|
|
25
25
|
collected.push( m );
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -46,7 +46,7 @@ describe( '.importComponents', () => {
|
|
|
46
46
|
].join( '\n' ) );
|
|
47
47
|
|
|
48
48
|
const collected = [];
|
|
49
|
-
for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
|
|
49
|
+
for await ( const m of importComponents( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) {
|
|
50
50
|
collected.push( m );
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -77,7 +77,7 @@ describe( '.importComponents', () => {
|
|
|
77
77
|
writeFileSync( vendorFile, fileContents );
|
|
78
78
|
|
|
79
79
|
const collected = [];
|
|
80
|
-
for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
|
|
80
|
+
for await ( const m of importComponents( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) {
|
|
81
81
|
collected.push( m );
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -86,4 +86,33 @@ describe( '.importComponents', () => {
|
|
|
86
86
|
|
|
87
87
|
rmSync( root, { recursive: true, force: true } );
|
|
88
88
|
} );
|
|
89
|
+
|
|
90
|
+
it( 'supports partial matching by folder name', async () => {
|
|
91
|
+
const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-foldermatch` );
|
|
92
|
+
const okDir = join( root, 'features', 'ok' );
|
|
93
|
+
const otherDir = join( root, 'features', 'other' );
|
|
94
|
+
mkdirSync( okDir, { recursive: true } );
|
|
95
|
+
mkdirSync( otherDir, { recursive: true } );
|
|
96
|
+
|
|
97
|
+
const okFile = join( okDir, 'alpha.js' );
|
|
98
|
+
const otherFile = join( otherDir, 'beta.js' );
|
|
99
|
+
const src = [
|
|
100
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
101
|
+
'export const X = () => {};',
|
|
102
|
+
'X[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "x" };'
|
|
103
|
+
].join( '\n' );
|
|
104
|
+
writeFileSync( okFile, src );
|
|
105
|
+
writeFileSync( otherFile, src );
|
|
106
|
+
|
|
107
|
+
// Match any JS under a folder named "ok"
|
|
108
|
+
const matcher = v => v.includes( `${join( 'features', 'ok' )}${sep}` );
|
|
109
|
+
const collected = [];
|
|
110
|
+
for await ( const m of importComponents( root, [ matcher ] ) ) {
|
|
111
|
+
collected.push( m );
|
|
112
|
+
}
|
|
113
|
+
expect( collected.length ).toBe( 1 );
|
|
114
|
+
expect( collected[0].path ).toBe( okFile );
|
|
115
|
+
|
|
116
|
+
rmSync( root, { recursive: true, force: true } );
|
|
117
|
+
} );
|
|
89
118
|
} );
|