@output.ai/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -8
- package/package.json +8 -2
- package/src/consts.js +2 -0
- package/src/index.d.ts +36 -4
- package/src/interface/evaluator.js +4 -4
- 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 +19 -14
- package/src/interface/zod_integration.spec.js +6 -6
- package/src/internal_activities/index.js +1 -1
- package/src/utils/index.d.ts +35 -0
- package/src/utils/index.js +2 -0
- package/src/utils/resolve_invocation_dir.js +27 -0
- package/src/utils/resolve_invocation_dir.spec.js +102 -0
- package/src/utils/utils.js +37 -0
- package/src/utils/utils.spec.js +60 -0
- package/src/worker/interceptors/workflow.js +10 -1
- package/src/worker/loader.js +31 -4
- package/src/worker/loader.spec.js +14 -3
- 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/interface/metadata.js +0 -4
- package/src/interface/utils.js +0 -19
- package/src/interface/utils.spec.js +0 -35
- package/src/utils.js +0 -8
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import resolveInvocationDir from './resolve_invocation_dir';
|
|
3
|
+
|
|
4
|
+
const OriginalError = Error;
|
|
5
|
+
|
|
6
|
+
describe( 'Resolve Invocation Dir', () => {
|
|
7
|
+
afterEach( () => {
|
|
8
|
+
Error = OriginalError;
|
|
9
|
+
} );
|
|
10
|
+
|
|
11
|
+
it( 'Should detect the invocation dir from the tests workflow', () => {
|
|
12
|
+
const stack = `Error
|
|
13
|
+
at resolveInvocationDir (file:///app/sdk/core/src/utils/resolve_invocation_dir.js)
|
|
14
|
+
at fn (file:///app/test_workflows/dist/simple/steps.js:8:21)
|
|
15
|
+
at wrapper (file:///app/sdk/core/src/interface/step.js:12:26)
|
|
16
|
+
at executeNextHandler (/app/node_modules/@temporalio/worker/lib/activity.js:99:54)
|
|
17
|
+
at Storage.runWithContext.parentId (file:///app/sdk/core/src/worker/interceptors/activity.js:31:63)
|
|
18
|
+
at AsyncLocalStorage.run (node:internal/async_local_storage/async_context_frame:63:14)
|
|
19
|
+
at Object.runWithContext (file:///app/sdk/core/src/async_storage.js:12:44)
|
|
20
|
+
at ActivityExecutionInterceptor.execute (file:///app/sdk/core/src/worker/interceptors/activity.js:31:36)
|
|
21
|
+
at next (/app/node_modules/@temporalio/common/lib/interceptors.js:22:51)
|
|
22
|
+
at Activity.execute (/app/node_modules/@temporalio/worker/lib/activity.js:101:26)
|
|
23
|
+
at /app/node_modules/@temporalio/worker/lib/activity.js:149:29`;
|
|
24
|
+
|
|
25
|
+
Error = class extends OriginalError {
|
|
26
|
+
constructor( ...args ) {
|
|
27
|
+
super( ...args );
|
|
28
|
+
this.stack = stack;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/simple' );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
it( 'Should detect the invocation dir from the sandbox environment at sdk/core', () => {
|
|
36
|
+
const stack = `Error
|
|
37
|
+
at resolveInvocationDir (file:///app/sdk/core/src/utils/resolve_invocation_dir.js)
|
|
38
|
+
at workflow (file:///app/sdk/core/src/interface/workflow.js:25:16)
|
|
39
|
+
at file:///app/test_workflows/dist/nested/workflow.js:4:16
|
|
40
|
+
at ModuleJob.run (node:internal/modules/esm/module_job:365:25)
|
|
41
|
+
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
|
|
42
|
+
at async importComponents (file:///app/sdk/core/src/worker/loader_tools.js:54:22)
|
|
43
|
+
at async loadWorkflows (file:///app/sdk/core/src/worker/loader.js:64:38)
|
|
44
|
+
at async file:///app/sdk/core/src/worker/index.js:21:21`;
|
|
45
|
+
|
|
46
|
+
Error = class extends OriginalError {
|
|
47
|
+
constructor( ...args ) {
|
|
48
|
+
super( ...args );
|
|
49
|
+
this.stack = stack;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'Should detect the invocation dir from workflow loading at core', () => {
|
|
57
|
+
const stack = `Error
|
|
58
|
+
at __WEBPACK_DEFAULT_EXPORT__ (/app/sdk/core/src/utils/resolve_invocation_dir.js:13:0)
|
|
59
|
+
at workflow (/app/sdk/core/src/interface/workflow.js:22:43)
|
|
60
|
+
at ../../test_workflows/dist/nested/workflow.js (/app/test_workflows/dist/nested/workflow.js:4:24)
|
|
61
|
+
at __webpack_require__ (webpack/bootstrap:19:0)
|
|
62
|
+
at ./src/worker/temp/__workflows_entrypoint.js (null:null:null)
|
|
63
|
+
at __webpack_require__ (webpack/bootstrap:19:0)
|
|
64
|
+
at importWorkflows (/app/sdk/core/src/worker/temp/__workflows_entrypoint-autogenerated-entrypoint.cjs:9:9)
|
|
65
|
+
at Object.initRuntime (/app/node_modules/@temporalio/workflow/src/worker-interface.ts:78:16)
|
|
66
|
+
at __TEMPORAL_CALL_INTO_SCOPE (evalmachine.<anonymous>:30:40)
|
|
67
|
+
at evalmachine.<anonymous>:1:1`;
|
|
68
|
+
|
|
69
|
+
Error = class extends OriginalError {
|
|
70
|
+
constructor( ...args ) {
|
|
71
|
+
super( ...args );
|
|
72
|
+
this.stack = stack;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
it( 'Should detect the invocation dir from a workflow using core installed via NPM', () => {
|
|
80
|
+
const stack = `Error
|
|
81
|
+
at resolveInvocationDir (file:///app/node_modules/@output.ai/core/src/utils/resolve_invocation_dir.js)
|
|
82
|
+
at fn (file:///app/dist/simple/steps.js:8:21)
|
|
83
|
+
at wrapper (file:///app/node_modules/@output.ai/core/src/interface/step.js:12:26)
|
|
84
|
+
at executeNextHandler (/app/node_modules/@temporalio/worker/lib/activity.js:99:54)
|
|
85
|
+
at Storage.runWithContext.parentId (file:///app/node_modules/@output.ai/core/src/worker/interceptors/activity.js:31:63)
|
|
86
|
+
at AsyncLocalStorage.run (node:internal/async_local_storage/async_context_frame:63:14)
|
|
87
|
+
at Object.runWithContext (file:///app/node_modules/@output.ai/core/src/async_storage.js:12:44)
|
|
88
|
+
at ActivityExecutionInterceptor.execute (file:///app/node_modules/@output.ai/core/src/worker/interceptors/activity.js:31:36)
|
|
89
|
+
at next (/app/node_modules/@temporalio/common/lib/interceptors.js:22:51)
|
|
90
|
+
at Activity.execute (/app/node_modules/@temporalio/worker/lib/activity.js:101:26)
|
|
91
|
+
at /app/node_modules/@temporalio/worker/lib/activity.js:149:29`;
|
|
92
|
+
|
|
93
|
+
Error = class extends OriginalError {
|
|
94
|
+
constructor( ...args ) {
|
|
95
|
+
super( ...args );
|
|
96
|
+
this.stack = stack;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
expect( resolveInvocationDir() ).toBe( '/app/dist/simple' );
|
|
101
|
+
} );
|
|
102
|
+
} );
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node safe clone implementation that doesn't use global structuredClone()
|
|
5
|
+
* @param {object} v
|
|
6
|
+
* @returns {object}
|
|
7
|
+
*/
|
|
8
|
+
export const clone = v => JSON.parse( JSON.stringify( v ) );
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Throw given error
|
|
12
|
+
* @param {Error} e
|
|
13
|
+
* @throws {e}
|
|
14
|
+
*/
|
|
15
|
+
export const throws = e => {
|
|
16
|
+
throw e;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add metadata "values" property to a given object
|
|
21
|
+
* @param {object} target
|
|
22
|
+
* @param {object} values
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
export const setMetadata = ( target, values ) =>
|
|
26
|
+
Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merge two temporal activity options
|
|
30
|
+
* @param {import('@temporalio/workflow').ActivityOptions} base
|
|
31
|
+
* @param {import('@temporalio/workflow').ActivityOptions} ext
|
|
32
|
+
* @returns {import('@temporalio/workflow').ActivityOptions}
|
|
33
|
+
*/
|
|
34
|
+
export const mergeActivityOptions = ( base = {}, ext = {} ) =>
|
|
35
|
+
Object.entries( ext ).reduce( ( options, [ k, v ] ) =>
|
|
36
|
+
Object.assign( options, { [k]: typeof v === 'object' ? mergeActivityOptions( options[k], v ) : v } )
|
|
37
|
+
, clone( base ) );
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { clone, mergeActivityOptions } from './utils.js';
|
|
3
|
+
|
|
4
|
+
describe( 'clone', () => {
|
|
5
|
+
it( 'produces a deep copy without shared references', () => {
|
|
6
|
+
const original = { a: 1, nested: { b: 2 } };
|
|
7
|
+
const copied = clone( original );
|
|
8
|
+
|
|
9
|
+
copied.nested.b = 3;
|
|
10
|
+
|
|
11
|
+
expect( original.nested.b ).toBe( 2 );
|
|
12
|
+
expect( copied.nested.b ).toBe( 3 );
|
|
13
|
+
expect( copied ).not.toBe( original );
|
|
14
|
+
} );
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
describe( 'mergeActivityOptions', () => {
|
|
18
|
+
it( 'recursively merges nested objects', () => {
|
|
19
|
+
const base = {
|
|
20
|
+
taskQueue: 'q1',
|
|
21
|
+
retry: { maximumAttempts: 3, backoffCoefficient: 2 }
|
|
22
|
+
};
|
|
23
|
+
const ext = {
|
|
24
|
+
retry: { maximumAttempts: 5, initialInterval: '1s' }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const result = mergeActivityOptions( base, ext );
|
|
28
|
+
|
|
29
|
+
expect( result ).toEqual( {
|
|
30
|
+
taskQueue: 'q1',
|
|
31
|
+
retry: { maximumAttempts: 5, backoffCoefficient: 2, initialInterval: '1s' }
|
|
32
|
+
} );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
it( 'omitted properties in second do not overwrite first', () => {
|
|
36
|
+
const base = {
|
|
37
|
+
taskQueue: 'q2',
|
|
38
|
+
retry: { initialInterval: '2s', backoffCoefficient: 2 }
|
|
39
|
+
};
|
|
40
|
+
const ext = {
|
|
41
|
+
retry: { backoffCoefficient: 3 }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = mergeActivityOptions( base, ext );
|
|
45
|
+
|
|
46
|
+
expect( result.retry.initialInterval ).toBe( '2s' );
|
|
47
|
+
expect( result.retry.backoffCoefficient ).toBe( 3 );
|
|
48
|
+
expect( result.taskQueue ).toBe( 'q2' );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'handles omitted second argument by returning a clone', () => {
|
|
52
|
+
const base = { taskQueue: 'q3', retry: { maximumAttempts: 2 } };
|
|
53
|
+
|
|
54
|
+
const result = mergeActivityOptions( base );
|
|
55
|
+
|
|
56
|
+
expect( result ).toEqual( base );
|
|
57
|
+
expect( result ).not.toBe( base );
|
|
58
|
+
} );
|
|
59
|
+
} );
|
|
60
|
+
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { workflowInfo, proxySinks, ApplicationFailure } from '@temporalio/workflow';
|
|
3
3
|
import { memoToHeaders } from '../sandboxed_utils.js';
|
|
4
|
+
import { mergeActivityOptions } from '#utils';
|
|
5
|
+
// this is a dynamic generated file with activity configs overwrites
|
|
6
|
+
import stepOptions from '../temp/__activity_options.js';
|
|
4
7
|
|
|
5
8
|
/*
|
|
6
9
|
This is not an AI comment!
|
|
@@ -13,7 +16,13 @@ import { memoToHeaders } from '../sandboxed_utils.js';
|
|
|
13
16
|
*/
|
|
14
17
|
class HeadersInjectionInterceptor {
|
|
15
18
|
async scheduleActivity( input, next ) {
|
|
16
|
-
|
|
19
|
+
const memo = workflowInfo().memo ?? {};
|
|
20
|
+
Object.assign( input.headers, memoToHeaders( memo ) );
|
|
21
|
+
// apply per-invocation options passed as second argument by rewritten calls
|
|
22
|
+
const options = stepOptions[input.activityType];
|
|
23
|
+
if ( options ) {
|
|
24
|
+
input.options = mergeActivityOptions( memo.activityOptions, options );
|
|
25
|
+
}
|
|
17
26
|
return next( input );
|
|
18
27
|
}
|
|
19
28
|
};
|
package/src/worker/loader.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
import { dirname, join } from 'node:path';
|
|
1
|
+
import { basename, 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 { sendWebhook } from '#internal_activities';
|
|
6
|
-
import { ACTIVITY_SEND_WEBHOOK, WORKFLOWS_INDEX_FILENAME, WORKFLOW_CATALOG } from '#consts';
|
|
7
6
|
import { importComponents } from './loader_tools.js';
|
|
7
|
+
import {
|
|
8
|
+
ACTIVITY_SEND_WEBHOOK,
|
|
9
|
+
ACTIVITY_OPTIONS_FILENAME,
|
|
10
|
+
SHARED_STEP_PREFIX,
|
|
11
|
+
WORKFLOWS_INDEX_FILENAME,
|
|
12
|
+
WORKFLOW_CATALOG
|
|
13
|
+
} from '#consts';
|
|
8
14
|
|
|
9
15
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
10
16
|
|
|
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
|
+
|
|
11
28
|
/**
|
|
12
29
|
* Builds a map of activities, where the key is their path and name and the value is the function
|
|
13
30
|
*
|
|
@@ -16,11 +33,21 @@ const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
|
16
33
|
*/
|
|
17
34
|
export async function loadActivities( target ) {
|
|
18
35
|
const activities = {};
|
|
19
|
-
|
|
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
|
+
|
|
20
41
|
console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
|
|
21
|
-
activities[`${
|
|
42
|
+
activities[`${prefix}#${metadata.name}`] = fn;
|
|
43
|
+
if ( metadata.options ) {
|
|
44
|
+
activityOptionsMap[`${prefix}#${metadata.name}`] = metadata.options;
|
|
45
|
+
}
|
|
22
46
|
}
|
|
23
47
|
|
|
48
|
+
// writes down the activity option overrides
|
|
49
|
+
writeActivityOptionsFile( activityOptionsMap );
|
|
50
|
+
|
|
24
51
|
// system activities
|
|
25
52
|
activities[ACTIVITY_SEND_WEBHOOK] = sendWebhook;
|
|
26
53
|
return activities;
|
|
@@ -3,7 +3,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
3
3
|
vi.mock( '#consts', () => ( {
|
|
4
4
|
ACTIVITY_SEND_WEBHOOK: '__internal#sendWebhook',
|
|
5
5
|
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
6
|
-
WORKFLOW_CATALOG: 'catalog'
|
|
6
|
+
WORKFLOW_CATALOG: 'catalog',
|
|
7
|
+
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js'
|
|
7
8
|
} ) );
|
|
8
9
|
|
|
9
10
|
const sendWebhookMock = vi.fn();
|
|
@@ -26,16 +27,26 @@ describe( 'worker/loader', () => {
|
|
|
26
27
|
vi.clearAllMocks();
|
|
27
28
|
} );
|
|
28
29
|
|
|
29
|
-
it( 'loadActivities returns map including system activity', async () => {
|
|
30
|
+
it( 'loadActivities returns map including system activity and writes options file', async () => {
|
|
30
31
|
const { loadActivities } = await import( './loader.js' );
|
|
31
32
|
|
|
32
33
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
33
|
-
yield { fn: () => {}, metadata: { name: 'Act1' }, path: '/a/steps.js' };
|
|
34
|
+
yield { fn: () => {}, metadata: { name: 'Act1', options: { retry: { maximumAttempts: 3 } } }, path: '/a/steps.js' };
|
|
34
35
|
} );
|
|
35
36
|
|
|
36
37
|
const activities = await loadActivities( '/root' );
|
|
37
38
|
expect( activities['/a#Act1'] ).toBeTypeOf( 'function' );
|
|
38
39
|
expect( activities['__internal#sendWebhook'] ).toBe( sendWebhookMock );
|
|
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();
|
|
39
50
|
} );
|
|
40
51
|
|
|
41
52
|
it( 'loadWorkflows returns array of workflows with metadata', async () => {
|
|
@@ -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
|
} );
|
|
@@ -106,6 +106,13 @@ export const toFunctionExpression = arrow => {
|
|
|
106
106
|
*/
|
|
107
107
|
export const isStepsPath = value => /(^|\/)steps\.js$/.test( value );
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Check if a module specifier or request string points to shared_steps.js.
|
|
111
|
+
* @param {string} value - Module path or request string.
|
|
112
|
+
* @returns {boolean} True if it matches shared_steps.js.
|
|
113
|
+
*/
|
|
114
|
+
export const isSharedStepsPath = value => /(^|\/)shared_steps\.js$/.test( value );
|
|
115
|
+
|
|
109
116
|
/**
|
|
110
117
|
* Check if a module specifier or request string points to evaluators.js.
|
|
111
118
|
* @param {string} value - Module path or request string.
|
|
@@ -215,6 +222,22 @@ export const buildStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
|
215
222
|
invalidMessagePrefix: 'Invalid step name in'
|
|
216
223
|
} );
|
|
217
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Build a map from exported shared step identifier to declared step name.
|
|
227
|
+
* Parses `shared_steps.js` for `export const X = step({ name: '...' })`.
|
|
228
|
+
* Uses the same factory as regular steps.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} path - Absolute path to the shared steps module file.
|
|
231
|
+
* @param {Map<string, Map<string,string>>} cache - Cache of computed name maps.
|
|
232
|
+
* @returns {Map<string,string>} Exported identifier -> step name.
|
|
233
|
+
*/
|
|
234
|
+
export const buildSharedStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
235
|
+
path,
|
|
236
|
+
cache,
|
|
237
|
+
calleeName: 'step',
|
|
238
|
+
invalidMessagePrefix: 'Invalid shared step name in'
|
|
239
|
+
} );
|
|
240
|
+
|
|
218
241
|
/**
|
|
219
242
|
* Build a map from exported evaluator identifier to declared evaluator name.
|
|
220
243
|
* Parses `evaluators.js` for `export const X = evaluator({ name: '...' })`.
|
package/src/interface/utils.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Function rigged to return the folder path of the source of calls for the interface methods (step/workflow)
|
|
3
|
-
*
|
|
4
|
-
* IMPORTANT!!!
|
|
5
|
-
* If to refactor this, pay attention to the depth in the stack trace to extract the info.
|
|
6
|
-
* Currently it is 3:
|
|
7
|
-
* - 1st line is the name of the function;
|
|
8
|
-
* - 2nd line is this function;
|
|
9
|
-
* - 3rd line is step/workflow;
|
|
10
|
-
* - 4th line is caller;
|
|
11
|
-
*
|
|
12
|
-
* @returns {string} The folder path of the caller
|
|
13
|
-
*/
|
|
14
|
-
export const getInvocationDir = () => new Error()
|
|
15
|
-
.stack.split( '\n' )[3]
|
|
16
|
-
.split( ' ' )
|
|
17
|
-
.at( -1 )
|
|
18
|
-
.replace( /\((.+):\d+:\d+\)/, '$1' )
|
|
19
|
-
.split( '/' ).slice( 0, -1 ).join( '/' );
|