@output.ai/core 0.1.0 → 0.1.2
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/README.md +114 -30
- package/package.json +9 -6
- package/src/consts.js +3 -1
- package/src/index.d.ts +36 -4
- package/src/interface/evaluator.js +12 -8
- package/src/interface/step.js +4 -4
- package/src/interface/validations/static.js +16 -2
- package/src/interface/validations/static.spec.js +20 -0
- package/src/interface/workflow.js +28 -25
- package/src/interface/zod_integration.spec.js +6 -6
- package/src/internal_activities/index.js +1 -33
- package/src/tracing/index.d.ts +4 -4
- package/src/tracing/index.js +12 -121
- package/src/tracing/internal_interface.js +66 -0
- package/src/tracing/processors/local/index.js +50 -0
- package/src/tracing/processors/local/index.spec.js +67 -0
- package/src/tracing/processors/s3/index.js +51 -0
- package/src/tracing/processors/s3/index.spec.js +64 -0
- package/src/tracing/processors/s3/redis_client.js +19 -0
- package/src/tracing/processors/s3/redis_client.spec.js +50 -0
- package/src/tracing/processors/s3/s3_client.js +33 -0
- package/src/tracing/processors/s3/s3_client.spec.js +67 -0
- package/src/tracing/{tracer_tree.js → tools/build_trace_tree.js} +4 -11
- package/src/tracing/{tracer_tree.spec.js → tools/build_trace_tree.spec.js} +4 -20
- package/src/tracing/{utils.js → tools/utils.js} +7 -0
- package/src/tracing/trace_engine.js +63 -0
- package/src/tracing/trace_engine.spec.js +91 -0
- package/src/utils.js +37 -0
- package/src/utils.spec.js +60 -0
- package/src/worker/catalog_workflow/index.js +2 -1
- package/src/worker/catalog_workflow/index.spec.js +6 -10
- package/src/worker/configs.js +24 -0
- package/src/worker/index.js +7 -4
- package/src/worker/interceptors/activity.js +7 -14
- package/src/worker/interceptors/workflow.js +11 -3
- package/src/worker/loader.js +65 -29
- package/src/worker/loader.spec.js +32 -25
- package/src/worker/loader_tools.js +63 -0
- package/src/worker/loader_tools.spec.js +85 -0
- package/src/worker/sinks.js +8 -4
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +38 -20
- 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 +16 -20
- package/src/worker/webpack_loaders/workflow_rewriter/tools.js +23 -0
- package/src/configs.js +0 -31
- package/src/configs.spec.js +0 -331
- package/src/interface/metadata.js +0 -4
- package/src/tracing/index.private.spec.js +0 -84
- package/src/tracing/index.public.spec.js +0 -86
- package/src/worker/internal_utils.js +0 -60
- package/src/worker/internal_utils.spec.js +0 -134
- /package/src/tracing/{utils.spec.js → tools/utils.spec.js} +0 -0
package/src/worker/loader.js
CHANGED
|
@@ -1,51 +1,87 @@
|
|
|
1
|
-
import { dirname, join } from 'path';
|
|
1
|
+
import { basename, dirname, join } from 'node:path';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { EOL } from 'node:os';
|
|
2
4
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { sendWebhook
|
|
4
|
-
import {
|
|
5
|
+
import { sendWebhook } from '#internal_activities';
|
|
6
|
+
import { importComponents } from './loader_tools.js';
|
|
5
7
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
ACTIVITY_SEND_WEBHOOK,
|
|
9
|
+
ACTIVITY_OPTIONS_FILENAME,
|
|
10
|
+
SHARED_STEP_PREFIX,
|
|
11
|
+
WORKFLOWS_INDEX_FILENAME,
|
|
12
|
+
WORKFLOW_CATALOG
|
|
13
|
+
} from '#consts';
|
|
10
14
|
|
|
11
15
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Writes to file the activity options
|
|
19
|
+
*
|
|
20
|
+
* @param {object} optionsMap
|
|
21
|
+
*/
|
|
22
|
+
const writeActivityOptionsFile = map => {
|
|
23
|
+
const path = join( __dirname, 'temp', ACTIVITY_OPTIONS_FILENAME );
|
|
24
|
+
mkdirSync( dirname( path ), { recursive: true } );
|
|
25
|
+
writeFileSync( path, `export default ${JSON.stringify( map, undefined, 2 )};`, 'utf-8' );
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builds a map of activities, where the key is their path and name and the value is the function
|
|
30
|
+
*
|
|
31
|
+
* @param {string} target
|
|
32
|
+
* @returns {object}
|
|
33
|
+
*/
|
|
34
|
+
export async function loadActivities( target ) {
|
|
35
|
+
const activities = {};
|
|
36
|
+
const activityOptionsMap = {};
|
|
37
|
+
for await ( const { fn, metadata, path } of importComponents( target, [ 'steps.js', 'evaluators.js', 'shared_steps.js' ] ) ) {
|
|
38
|
+
const isShared = basename( path ) === 'shared_steps.js';
|
|
39
|
+
const prefix = isShared ? SHARED_STEP_PREFIX : dirname( path );
|
|
40
|
+
|
|
41
|
+
console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
|
|
42
|
+
activities[`${prefix}#${metadata.name}`] = fn;
|
|
43
|
+
if ( metadata.options ) {
|
|
44
|
+
activityOptionsMap[`${prefix}#${metadata.name}`] = metadata.options;
|
|
45
|
+
}
|
|
20
46
|
}
|
|
21
47
|
|
|
48
|
+
// writes down the activity option overrides
|
|
49
|
+
writeActivityOptionsFile( activityOptionsMap );
|
|
50
|
+
|
|
22
51
|
// system activities
|
|
23
52
|
activities[ACTIVITY_SEND_WEBHOOK] = sendWebhook;
|
|
24
|
-
activities[ACTIVITY_READ_TRACE_FILE] = readTraceFile;
|
|
25
53
|
return activities;
|
|
26
54
|
};
|
|
27
55
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Builds an array of workflow objects
|
|
58
|
+
*
|
|
59
|
+
* @param {string} target
|
|
60
|
+
* @returns {object[]}
|
|
61
|
+
*/
|
|
62
|
+
export async function loadWorkflows( target ) {
|
|
31
63
|
const workflows = [];
|
|
32
|
-
for await ( const { metadata,
|
|
33
|
-
|
|
34
|
-
|
|
64
|
+
for await ( const { metadata, path } of importComponents( target, [ 'workflow.js' ] ) ) {
|
|
65
|
+
console.log( '[Core.Scanner]', 'Workflow loaded:', metadata.name, 'at', path );
|
|
66
|
+
workflows.push( { ...metadata, path } );
|
|
35
67
|
}
|
|
36
68
|
return workflows;
|
|
37
69
|
};
|
|
38
70
|
|
|
39
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Creates a temporary index file importing all workflows
|
|
73
|
+
*
|
|
74
|
+
* @param {object[]} workflows
|
|
75
|
+
* @returns
|
|
76
|
+
*/
|
|
40
77
|
export function createWorkflowsEntryPoint( workflows ) {
|
|
41
|
-
const
|
|
78
|
+
const path = join( __dirname, 'temp', WORKFLOWS_INDEX_FILENAME );
|
|
42
79
|
|
|
43
80
|
// default system catalog workflow
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
.map( ( { name, pathname } ) => `export { default as ${name} } from '${pathname}';` )
|
|
47
|
-
.join( '\n' );
|
|
81
|
+
const catalog = { name: WORKFLOW_CATALOG, path: join( __dirname, './catalog_workflow/workflow.js' ) };
|
|
82
|
+
const content = [ ... workflows, catalog ].map( ( { name, path } ) => `export { default as ${name} } from '${path}';` ).join( EOL );
|
|
48
83
|
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
mkdirSync( dirname( path ), { recursive: true } );
|
|
85
|
+
writeFileSync( path, content, 'utf-8' );
|
|
86
|
+
return path;
|
|
51
87
|
};
|
|
@@ -2,25 +2,24 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
vi.mock( '#consts', () => ( {
|
|
4
4
|
ACTIVITY_SEND_WEBHOOK: '__internal#sendWebhook',
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
6
|
+
WORKFLOW_CATALOG: 'catalog',
|
|
7
|
+
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js'
|
|
7
8
|
} ) );
|
|
8
9
|
|
|
9
10
|
const sendWebhookMock = vi.fn();
|
|
10
|
-
const readTraceFileMock = vi.fn();
|
|
11
11
|
vi.mock( '#internal_activities', () => ( {
|
|
12
|
-
sendWebhook: sendWebhookMock
|
|
13
|
-
readTraceFile: readTraceFileMock
|
|
12
|
+
sendWebhook: sendWebhookMock
|
|
14
13
|
} ) );
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
vi.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
const importComponentsMock = vi.fn();
|
|
16
|
+
vi.mock( './loader_tools.js', () => ( { importComponents: importComponentsMock } ) );
|
|
17
|
+
|
|
18
|
+
const mkdirSyncMock = vi.fn();
|
|
19
|
+
const writeFileSyncMock = vi.fn();
|
|
20
|
+
vi.mock( 'node:fs', () => ( {
|
|
21
|
+
mkdirSync: mkdirSyncMock,
|
|
22
|
+
writeFileSync: writeFileSyncMock
|
|
24
23
|
} ) );
|
|
25
24
|
|
|
26
25
|
describe( 'worker/loader', () => {
|
|
@@ -28,42 +27,50 @@ describe( 'worker/loader', () => {
|
|
|
28
27
|
vi.clearAllMocks();
|
|
29
28
|
} );
|
|
30
29
|
|
|
31
|
-
it( 'loadActivities returns map including system activity', async () => {
|
|
30
|
+
it( 'loadActivities returns map including system activity and writes options file', async () => {
|
|
32
31
|
const { loadActivities } = await import( './loader.js' );
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
yield { component: () => {}, metadata: { name: 'Act1' }, pathname: '/a/steps.js', path: '/a' };
|
|
33
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
34
|
+
yield { fn: () => {}, metadata: { name: 'Act1', options: { retry: { maximumAttempts: 3 } } }, path: '/a/steps.js' };
|
|
37
35
|
} );
|
|
38
36
|
|
|
39
37
|
const activities = await loadActivities( '/root' );
|
|
40
38
|
expect( activities['/a#Act1'] ).toBeTypeOf( 'function' );
|
|
41
39
|
expect( activities['__internal#sendWebhook'] ).toBe( sendWebhookMock );
|
|
42
|
-
|
|
40
|
+
|
|
41
|
+
// options file written with the collected map
|
|
42
|
+
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
43
|
+
const [ writtenPath, contents ] = writeFileSyncMock.mock.calls[0];
|
|
44
|
+
expect( writtenPath ).toMatch( /temp\/__activity_options\.js$/ );
|
|
45
|
+
expect( contents ).toContain( 'export default' );
|
|
46
|
+
expect( JSON.parse( contents.replace( /^export default\s*/, '' ).replace( /;\s*$/, '' ) ) ).toEqual( {
|
|
47
|
+
'/a#Act1': { retry: { maximumAttempts: 3 } }
|
|
48
|
+
} );
|
|
49
|
+
expect( mkdirSyncMock ).toHaveBeenCalled();
|
|
43
50
|
} );
|
|
44
51
|
|
|
45
52
|
it( 'loadWorkflows returns array of workflows with metadata', async () => {
|
|
46
53
|
const { loadWorkflows } = await import( './loader.js' );
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
yield { metadata: { name: 'Flow1', description: 'd' }, pathname: '/b/workflow.js', path: '/b' };
|
|
55
|
+
importComponentsMock.mockImplementationOnce( async function *() {
|
|
56
|
+
yield { metadata: { name: 'Flow1', description: 'd' }, path: '/b/workflow.js' };
|
|
51
57
|
} );
|
|
52
58
|
|
|
53
59
|
const workflows = await loadWorkflows( '/root' );
|
|
54
|
-
expect( workflows ).toEqual( [ { name: 'Flow1', description: 'd',
|
|
60
|
+
expect( workflows ).toEqual( [ { name: 'Flow1', description: 'd', path: '/b/workflow.js' } ] );
|
|
55
61
|
} );
|
|
56
62
|
|
|
57
63
|
it( 'createWorkflowsEntryPoint writes index and returns its path', async () => {
|
|
58
64
|
const { createWorkflowsEntryPoint } = await import( './loader.js' );
|
|
59
65
|
|
|
60
|
-
const workflows = [ { name: 'W',
|
|
66
|
+
const workflows = [ { name: 'W', path: '/abs/wf.js' } ];
|
|
61
67
|
const entry = createWorkflowsEntryPoint( workflows );
|
|
62
68
|
|
|
63
|
-
expect(
|
|
64
|
-
const [ writtenPath, contents ] =
|
|
69
|
+
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
70
|
+
const [ writtenPath, contents ] = writeFileSyncMock.mock.calls[0];
|
|
65
71
|
expect( entry ).toBe( writtenPath );
|
|
66
72
|
expect( contents ).toContain( 'export { default as W } from \'/abs/wf.js\';' );
|
|
67
73
|
expect( contents ).toContain( 'export { default as catalog }' );
|
|
74
|
+
expect( mkdirSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
68
75
|
} );
|
|
69
76
|
} );
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
3
|
+
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
|
+
import { readdirSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} CollectedFile
|
|
8
|
+
* @property {string} path - The file path
|
|
9
|
+
* @property {string} url - The resolved url of the file, ready to be imported
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} Component
|
|
13
|
+
* @property {Function} fn - The loaded component function
|
|
14
|
+
* @property {object} metadata - Associated metadata with the component
|
|
15
|
+
* @property {string} path - Associated metadata with the component
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recursive traverse directories looking for files with given name.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} path - The path to scan
|
|
22
|
+
* @param {string[]} filenames - The filenames to look for
|
|
23
|
+
* @returns {CollectedFile[]} An array containing the collected files
|
|
24
|
+
* */
|
|
25
|
+
const findByNameRecursively = ( parentPath, filenames, collection = [], ignoreDirNames = [ 'vendor', 'node_modules' ] ) => {
|
|
26
|
+
for ( const entry of readdirSync( parentPath, { withFileTypes: true } ) ) {
|
|
27
|
+
if ( ignoreDirNames.includes( entry.name ) ) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const path = resolve( parentPath, entry.name );
|
|
32
|
+
if ( entry.isDirectory() ) {
|
|
33
|
+
findByNameRecursively( path, filenames, collection );
|
|
34
|
+
} else if ( filenames.includes( entry.name ) ) {
|
|
35
|
+
collection.push( { path, url: pathToFileURL( path ).href } );
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return collection;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* For each path, dynamic import it, and for each exported component with metadata (step, workflow), yields it.
|
|
44
|
+
*
|
|
45
|
+
* @generator
|
|
46
|
+
* @async
|
|
47
|
+
* @function importComponents
|
|
48
|
+
* @param {string} target - Place to look for files
|
|
49
|
+
* @param {string[]} filenames - File names to load recursively from target
|
|
50
|
+
* @yields {Component}
|
|
51
|
+
*/
|
|
52
|
+
export async function *importComponents( target, filenames ) {
|
|
53
|
+
for ( const { url, path } of findByNameRecursively( target, filenames ) ) {
|
|
54
|
+
const imported = await import( url );
|
|
55
|
+
for ( const fn of Object.values( imported ) ) {
|
|
56
|
+
const metadata = fn[METADATA_ACCESS_SYMBOL];
|
|
57
|
+
if ( !metadata ) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
yield { fn, metadata, path };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { importComponents } from './loader_tools.js';
|
|
5
|
+
|
|
6
|
+
describe( '.importComponents', () => {
|
|
7
|
+
it( 'imports modules and yields metadata from exports tagged with METADATA_ACCESS_SYMBOL', async () => {
|
|
8
|
+
const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}` );
|
|
9
|
+
mkdirSync( root, { recursive: true } );
|
|
10
|
+
const file = join( root, 'meta.module.js' );
|
|
11
|
+
writeFileSync( file, [
|
|
12
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
13
|
+
'export const StepA = () => {};',
|
|
14
|
+
'StepA[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "a" };',
|
|
15
|
+
'export const FlowB = () => {};',
|
|
16
|
+
'FlowB[METADATA_ACCESS_SYMBOL] = { kind: "workflow", name: "b" };'
|
|
17
|
+
].join( '\n' ) );
|
|
18
|
+
|
|
19
|
+
const collected = [];
|
|
20
|
+
for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
|
|
21
|
+
collected.push( m );
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
expect( collected.length ).toBe( 2 );
|
|
25
|
+
expect( collected.map( m => m.metadata.name ).sort() ).toEqual( [ 'a', 'b' ] );
|
|
26
|
+
expect( collected.map( m => m.metadata.kind ).sort() ).toEqual( [ 'step', 'workflow' ] );
|
|
27
|
+
for ( const m of collected ) {
|
|
28
|
+
expect( m.path ).toBe( file );
|
|
29
|
+
expect( typeof m.fn ).toBe( 'function' );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
rmSync( root, { recursive: true, force: true } );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
it( 'ignores exports without metadata symbol', async () => {
|
|
36
|
+
const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-nometa` );
|
|
37
|
+
mkdirSync( root, { recursive: true } );
|
|
38
|
+
const file = join( root, 'meta.module.js' );
|
|
39
|
+
writeFileSync( file, [
|
|
40
|
+
'export const Plain = () => {};',
|
|
41
|
+
'export const AlsoPlain = {}'
|
|
42
|
+
].join( '\n' ) );
|
|
43
|
+
|
|
44
|
+
const collected = [];
|
|
45
|
+
for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
|
|
46
|
+
collected.push( m );
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
expect( collected.length ).toBe( 0 );
|
|
50
|
+
rmSync( root, { recursive: true, force: true } );
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'skips files inside ignored directories (node_modules, vendor)', async () => {
|
|
54
|
+
const root = join( process.cwd(), 'sdk/core/temp_test_modules', `meta-${Date.now()}-ignoredirs` );
|
|
55
|
+
const okDir = join( root, 'ok' );
|
|
56
|
+
const nmDir = join( root, 'node_modules' );
|
|
57
|
+
const vendorDir = join( root, 'vendor' );
|
|
58
|
+
mkdirSync( okDir, { recursive: true } );
|
|
59
|
+
mkdirSync( nmDir, { recursive: true } );
|
|
60
|
+
mkdirSync( vendorDir, { recursive: true } );
|
|
61
|
+
|
|
62
|
+
const okFile = join( okDir, 'meta.module.js' );
|
|
63
|
+
const nmFile = join( nmDir, 'meta.module.js' );
|
|
64
|
+
const vendorFile = join( vendorDir, 'meta.module.js' );
|
|
65
|
+
|
|
66
|
+
const fileContents = [
|
|
67
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
68
|
+
'export const C = () => {};',
|
|
69
|
+
'C[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "c" };'
|
|
70
|
+
].join( '\n' );
|
|
71
|
+
writeFileSync( okFile, fileContents );
|
|
72
|
+
writeFileSync( nmFile, fileContents );
|
|
73
|
+
writeFileSync( vendorFile, fileContents );
|
|
74
|
+
|
|
75
|
+
const collected = [];
|
|
76
|
+
for await ( const m of importComponents( root, [ 'meta.module.js' ] ) ) {
|
|
77
|
+
collected.push( m );
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect( collected.length ).toBe( 1 );
|
|
81
|
+
expect( collected[0].path ).toBe( okFile );
|
|
82
|
+
|
|
83
|
+
rmSync( root, { recursive: true, force: true } );
|
|
84
|
+
} );
|
|
85
|
+
} );
|
package/src/worker/sinks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WORKFLOW_CATALOG } from '#consts';
|
|
1
2
|
import { addEventStart, addEventEnd, addEventError } from '#tracing';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -8,8 +9,11 @@ import { addEventStart, addEventEnd, addEventError } from '#tracing';
|
|
|
8
9
|
* @param {object} details - Teh details to attach to the event
|
|
9
10
|
*/
|
|
10
11
|
const addWorkflowEvent = ( method, workflowInfo, details ) => {
|
|
11
|
-
const { workflowId: id, workflowType: name, memo: { parentId,
|
|
12
|
-
|
|
12
|
+
const { workflowId: id, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
|
|
13
|
+
if ( name === WORKFLOW_CATALOG ) {
|
|
14
|
+
return;
|
|
15
|
+
} // ignore internal catalog events
|
|
16
|
+
method( { id, kind: 'workflow', name, details, parentId, executionContext } );
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -21,8 +25,8 @@ const addWorkflowEvent = ( method, workflowInfo, details ) => {
|
|
|
21
25
|
*/
|
|
22
26
|
const addEvent = ( method, workflowInfo, options ) => {
|
|
23
27
|
const { id, name, kind, details } = options;
|
|
24
|
-
const { workflowId, memo: {
|
|
25
|
-
method( { id, kind, name, details, parentId: workflowId,
|
|
28
|
+
const { workflowId, memo: { executionContext } } = workflowInfo;
|
|
29
|
+
method( { id, kind, name, details, parentId: workflowId, executionContext } );
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
// This sink allow for sandbox Temporal environment to send trace logs back to the main thread.
|
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
buildWorkflowNameMap,
|
|
4
4
|
getLocalNameFromDestructuredProperty,
|
|
5
5
|
isEvaluatorsPath,
|
|
6
|
+
isSharedStepsPath,
|
|
6
7
|
isStepsPath,
|
|
7
8
|
isWorkflowPath,
|
|
8
9
|
buildStepsNameMap,
|
|
10
|
+
buildSharedStepsNameMap,
|
|
9
11
|
buildEvaluatorsNameMap,
|
|
10
12
|
toAbsolutePath
|
|
11
13
|
} from './tools.js';
|
|
@@ -36,8 +38,9 @@ const traverse = traverseModule.default ?? traverseModule;
|
|
|
36
38
|
* @returns {{ stepImports: Array<{localName:string,stepName:string}>,
|
|
37
39
|
* flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
|
|
38
40
|
*/
|
|
39
|
-
export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache, evaluatorsNameCache } ) {
|
|
41
|
+
export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache, evaluatorsNameCache, sharedStepsNameCache } ) {
|
|
40
42
|
const stepImports = [];
|
|
43
|
+
const sharedStepImports = [];
|
|
41
44
|
const flowImports = [];
|
|
42
45
|
const evaluatorImports = [];
|
|
43
46
|
|
|
@@ -45,33 +48,31 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
45
48
|
ImportDeclaration: path => {
|
|
46
49
|
const src = path.node.source.value;
|
|
47
50
|
// Ignore other imports
|
|
48
|
-
if ( !isStepsPath( src ) && !isWorkflowPath( src ) && !isEvaluatorsPath( src ) ) {
|
|
51
|
+
if ( !isStepsPath( src ) && !isSharedStepsPath( src ) && !isWorkflowPath( src ) && !isEvaluatorsPath( src ) ) {
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const absolutePath = toAbsolutePath( fileDir, src );
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const importedName = s.imported.name;
|
|
57
|
-
const localName = s.local.name;
|
|
58
|
-
const stepName = nameMap.get( importedName );
|
|
59
|
-
if ( stepName ) {
|
|
60
|
-
stepImports.push( { localName, stepName } );
|
|
61
|
-
}
|
|
56
|
+
const collectNamedImports = ( match, buildMapFn, cache, targetArr, valueKey ) => {
|
|
57
|
+
if ( !match ) {
|
|
58
|
+
return;
|
|
62
59
|
}
|
|
63
|
-
|
|
64
|
-
if ( isEvaluatorsPath( src ) ) {
|
|
65
|
-
const nameMap = buildEvaluatorsNameMap( absolutePath, evaluatorsNameCache );
|
|
60
|
+
const nameMap = buildMapFn( absolutePath, cache );
|
|
66
61
|
for ( const s of path.node.specifiers.filter( s => isImportSpecifier( s ) ) ) {
|
|
67
62
|
const importedName = s.imported.name;
|
|
68
63
|
const localName = s.local.name;
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
64
|
+
const value = nameMap.get( importedName );
|
|
65
|
+
if ( value ) {
|
|
66
|
+
const entry = { localName };
|
|
67
|
+
entry[valueKey] = value;
|
|
68
|
+
targetArr.push( entry );
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
|
-
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
collectNamedImports( isStepsPath( src ), buildStepsNameMap, stepsNameCache, stepImports, 'stepName' );
|
|
74
|
+
collectNamedImports( isSharedStepsPath( src ), buildSharedStepsNameMap, sharedStepsNameCache, sharedStepImports, 'stepName' );
|
|
75
|
+
collectNamedImports( isEvaluatorsPath( src ), buildEvaluatorsNameMap, evaluatorsNameCache, evaluatorImports, 'evaluatorName' );
|
|
75
76
|
if ( isWorkflowPath( src ) ) {
|
|
76
77
|
const { named, default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
77
78
|
for ( const s of path.node.specifiers ) {
|
|
@@ -108,7 +109,7 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
108
109
|
|
|
109
110
|
const req = firstArgument.value;
|
|
110
111
|
// Must be steps/workflows module
|
|
111
|
-
if ( !isStepsPath( req ) && !isWorkflowPath( req ) && !isEvaluatorsPath( req ) ) {
|
|
112
|
+
if ( !isStepsPath( req ) && !isSharedStepsPath( req ) && !isWorkflowPath( req ) && !isEvaluatorsPath( req ) ) {
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -130,6 +131,23 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
130
131
|
} else {
|
|
131
132
|
path.remove();
|
|
132
133
|
}
|
|
134
|
+
} else if ( isSharedStepsPath( req ) && isObjectPattern( path.node.id ) ) {
|
|
135
|
+
const nameMap = buildSharedStepsNameMap( absolutePath, sharedStepsNameCache ?? stepsNameCache );
|
|
136
|
+
for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
|
|
137
|
+
const importedName = prop.key.name;
|
|
138
|
+
const localName = getLocalNameFromDestructuredProperty( prop );
|
|
139
|
+
if ( localName ) {
|
|
140
|
+
const stepName = nameMap.get( importedName );
|
|
141
|
+
if ( stepName ) {
|
|
142
|
+
sharedStepImports.push( { localName, stepName } );
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
|
|
147
|
+
path.parentPath.remove();
|
|
148
|
+
} else {
|
|
149
|
+
path.remove();
|
|
150
|
+
}
|
|
133
151
|
} else if ( isEvaluatorsPath( req ) && isObjectPattern( path.node.id ) ) {
|
|
134
152
|
const nameMap = buildEvaluatorsNameMap( absolutePath, evaluatorsNameCache );
|
|
135
153
|
for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
|
|
@@ -160,5 +178,5 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
160
178
|
}
|
|
161
179
|
} );
|
|
162
180
|
|
|
163
|
-
return { stepImports, evaluatorImports, flowImports };
|
|
181
|
+
return { stepImports, sharedStepImports, evaluatorImports, flowImports };
|
|
164
182
|
};
|
|
@@ -10,6 +10,7 @@ const generate = generatorModule.default ?? generatorModule;
|
|
|
10
10
|
|
|
11
11
|
// Caches to avoid re-reading files during a build
|
|
12
12
|
const stepsNameCache = new Map(); // path -> Map<exported, stepName>
|
|
13
|
+
const sharedStepsNameCache = new Map(); // path -> Map<exported, stepName> (shared)
|
|
13
14
|
const evaluatorsNameCache = new Map(); // path -> Map<exported, evaluatorName>
|
|
14
15
|
const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exported, flowName> }
|
|
15
16
|
|
|
@@ -26,20 +27,20 @@ const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exp
|
|
|
26
27
|
export default function stepImportRewriterAstLoader( source, inputMap ) {
|
|
27
28
|
this.cacheable?.( true );
|
|
28
29
|
const callback = this.async?.() ?? this.callback;
|
|
29
|
-
const cache = { stepsNameCache, evaluatorsNameCache, workflowNameCache };
|
|
30
|
+
const cache = { stepsNameCache, sharedStepsNameCache, evaluatorsNameCache, workflowNameCache };
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
33
|
const filename = this.resourcePath;
|
|
33
34
|
const ast = parse( String( source ), filename );
|
|
34
35
|
const fileDir = dirname( filename );
|
|
35
|
-
const { stepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
36
|
+
const { stepImports, sharedStepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
36
37
|
|
|
37
38
|
// No imports
|
|
38
|
-
if ( [].concat( stepImports, evaluatorImports, flowImports ).length === 0 ) {
|
|
39
|
+
if ( [].concat( stepImports, sharedStepImports, evaluatorImports, flowImports ).length === 0 ) {
|
|
39
40
|
return callback( null, source, inputMap );
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } );
|
|
43
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, sharedStepImports, evaluatorImports, flowImports } );
|
|
43
44
|
// No edits performed
|
|
44
45
|
if ( !rewrote ) {
|
|
45
46
|
return callback( null, source, inputMap );
|
|
@@ -51,6 +51,54 @@ describe( 'workflows_rewriter Webpack loader spec', () => {
|
|
|
51
51
|
rmSync( dir, { recursive: true, force: true } );
|
|
52
52
|
} );
|
|
53
53
|
|
|
54
|
+
it( 'rewrites ESM shared_steps imports to invokeSharedStep', async () => {
|
|
55
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-' ) );
|
|
56
|
+
writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedA = step({ name: \'shared.a\' })\n' );
|
|
57
|
+
|
|
58
|
+
const source = [
|
|
59
|
+
'import { SharedA } from \'./shared_steps.js\';',
|
|
60
|
+
'',
|
|
61
|
+
'const obj = {',
|
|
62
|
+
' fn: async (x) => {',
|
|
63
|
+
' SharedA(1);',
|
|
64
|
+
' }',
|
|
65
|
+
'}',
|
|
66
|
+
''
|
|
67
|
+
].join( '\n' );
|
|
68
|
+
|
|
69
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
70
|
+
|
|
71
|
+
expect( code ).not.toMatch( /from '\.\/shared_steps\.js'/ );
|
|
72
|
+
expect( code ).toMatch( /fn:\s*async function \(x\)/ );
|
|
73
|
+
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.a',\s*1\)/ );
|
|
74
|
+
|
|
75
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'rewrites CJS shared_steps requires to invokeSharedStep', async () => {
|
|
79
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-' ) );
|
|
80
|
+
writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedB = step({ name: \'shared.b\' })\n' );
|
|
81
|
+
|
|
82
|
+
const source = [
|
|
83
|
+
'const { SharedB } = require(\'./shared_steps.js\');',
|
|
84
|
+
'',
|
|
85
|
+
'const obj = {',
|
|
86
|
+
' fn: async (y) => {',
|
|
87
|
+
' SharedB();',
|
|
88
|
+
' }',
|
|
89
|
+
'}',
|
|
90
|
+
''
|
|
91
|
+
].join( '\n' );
|
|
92
|
+
|
|
93
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
94
|
+
|
|
95
|
+
expect( code ).not.toMatch( /require\('\.\/shared_steps\.js'\)/ );
|
|
96
|
+
expect( code ).toMatch( /fn:\s*async function \(y\)/ );
|
|
97
|
+
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.b'\)/ );
|
|
98
|
+
|
|
99
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
100
|
+
} );
|
|
101
|
+
|
|
54
102
|
it( 'rewrites CJS requires and converts fn arrow to function', async () => {
|
|
55
103
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-' ) );
|
|
56
104
|
writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: \'step.b\' })\n' );
|
|
@@ -18,11 +18,12 @@ const traverse = traverseModule.default ?? traverseModule;
|
|
|
18
18
|
* @param {object} params
|
|
19
19
|
* @param {import('@babel/types').File} params.ast - Parsed file AST.
|
|
20
20
|
* @param {Array<{localName:string,stepName:string}>} params.stepImports - Step imports.
|
|
21
|
+
* @param {Array<{localName:string,stepName:string}>} params.sharedStepImports - Shared step imports.
|
|
21
22
|
* @param {Array<{localName:string,evaluatorName:string}>} params.evaluatorImports - Evaluator imports.
|
|
22
23
|
* @param {Array<{localName:string,workflowName:string}>} params.flowImports - Workflow imports.
|
|
23
24
|
* @returns {boolean} True if the AST was modified; false otherwise.
|
|
24
25
|
*/
|
|
25
|
-
export default function rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } ) {
|
|
26
|
+
export default function rewriteFnBodies( { ast, stepImports, sharedStepImports = [], evaluatorImports, flowImports } ) {
|
|
26
27
|
const state = { rewrote: false };
|
|
27
28
|
traverse( ast, {
|
|
28
29
|
ObjectProperty: path => {
|
|
@@ -51,25 +52,20 @@ export default function rewriteFnBodies( { ast, stepImports, evaluatorImports, f
|
|
|
51
52
|
if ( !isIdentifier( callee ) ) {
|
|
52
53
|
return;
|
|
53
54
|
} // Skip: complex callee not supported
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const flow = flowImports.find( x => x.localName === callee.name );
|
|
69
|
-
if ( flow ) {
|
|
70
|
-
const args = cPath.node.arguments;
|
|
71
|
-
cPath.replaceWith( createThisMethodCall( 'startWorkflow', flow.workflowName, args ) );
|
|
72
|
-
state.rewrote = true;
|
|
55
|
+
const descriptors = [
|
|
56
|
+
{ list: stepImports, method: 'invokeStep', key: 'stepName' },
|
|
57
|
+
{ list: sharedStepImports, method: 'invokeSharedStep', key: 'stepName' },
|
|
58
|
+
{ list: evaluatorImports, method: 'invokeEvaluator', key: 'evaluatorName' },
|
|
59
|
+
{ list: flowImports, method: 'startWorkflow', key: 'workflowName' }
|
|
60
|
+
];
|
|
61
|
+
for ( const { list, method, key } of descriptors ) {
|
|
62
|
+
const found = list.find( x => x.localName === callee.name );
|
|
63
|
+
if ( found ) {
|
|
64
|
+
const args = cPath.node.arguments;
|
|
65
|
+
cPath.replaceWith( createThisMethodCall( method, found[key], args ) );
|
|
66
|
+
state.rewrote = true;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
71
|
} );
|