@outputai/core 0.1.0
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/LICENSE +201 -0
- package/README.md +11 -0
- package/bin/healthcheck.mjs +36 -0
- package/bin/healthcheck.spec.js +90 -0
- package/bin/worker.sh +26 -0
- package/package.json +67 -0
- package/src/activity_integration/context.d.ts +27 -0
- package/src/activity_integration/context.js +17 -0
- package/src/activity_integration/context.spec.js +42 -0
- package/src/activity_integration/events.d.ts +7 -0
- package/src/activity_integration/events.js +10 -0
- package/src/activity_integration/index.d.ts +9 -0
- package/src/activity_integration/index.js +3 -0
- package/src/activity_integration/tracing.d.ts +32 -0
- package/src/activity_integration/tracing.js +37 -0
- package/src/async_storage.js +19 -0
- package/src/bus.js +3 -0
- package/src/consts.js +32 -0
- package/src/errors.d.ts +15 -0
- package/src/errors.js +14 -0
- package/src/hooks/index.d.ts +28 -0
- package/src/hooks/index.js +32 -0
- package/src/index.d.ts +49 -0
- package/src/index.js +4 -0
- package/src/interface/evaluation_result.d.ts +173 -0
- package/src/interface/evaluation_result.js +215 -0
- package/src/interface/evaluator.d.ts +70 -0
- package/src/interface/evaluator.js +34 -0
- package/src/interface/evaluator.spec.js +565 -0
- package/src/interface/index.d.ts +9 -0
- package/src/interface/index.js +26 -0
- package/src/interface/step.d.ts +138 -0
- package/src/interface/step.js +22 -0
- package/src/interface/types.d.ts +27 -0
- package/src/interface/validations/runtime.js +20 -0
- package/src/interface/validations/runtime.spec.js +29 -0
- package/src/interface/validations/schema_utils.js +8 -0
- package/src/interface/validations/schema_utils.spec.js +67 -0
- package/src/interface/validations/static.js +136 -0
- package/src/interface/validations/static.spec.js +366 -0
- package/src/interface/webhook.d.ts +84 -0
- package/src/interface/webhook.js +64 -0
- package/src/interface/webhook.spec.js +122 -0
- package/src/interface/workflow.d.ts +273 -0
- package/src/interface/workflow.js +128 -0
- package/src/interface/workflow.spec.js +467 -0
- package/src/interface/workflow_context.js +31 -0
- package/src/interface/workflow_utils.d.ts +76 -0
- package/src/interface/workflow_utils.js +50 -0
- package/src/interface/workflow_utils.spec.js +190 -0
- package/src/interface/zod_integration.spec.js +646 -0
- package/src/internal_activities/index.js +66 -0
- package/src/internal_activities/index.spec.js +102 -0
- package/src/logger.js +73 -0
- package/src/tracing/internal_interface.js +71 -0
- package/src/tracing/processors/local/index.js +111 -0
- package/src/tracing/processors/local/index.spec.js +149 -0
- package/src/tracing/processors/s3/configs.js +31 -0
- package/src/tracing/processors/s3/configs.spec.js +64 -0
- package/src/tracing/processors/s3/index.js +114 -0
- package/src/tracing/processors/s3/index.spec.js +153 -0
- package/src/tracing/processors/s3/redis_client.js +62 -0
- package/src/tracing/processors/s3/redis_client.spec.js +185 -0
- package/src/tracing/processors/s3/s3_client.js +27 -0
- package/src/tracing/processors/s3/s3_client.spec.js +62 -0
- package/src/tracing/tools/build_trace_tree.js +83 -0
- package/src/tracing/tools/build_trace_tree.spec.js +135 -0
- package/src/tracing/tools/utils.js +21 -0
- package/src/tracing/tools/utils.spec.js +14 -0
- package/src/tracing/trace_engine.js +97 -0
- package/src/tracing/trace_engine.spec.js +199 -0
- package/src/utils/index.d.ts +134 -0
- package/src/utils/index.js +2 -0
- package/src/utils/resolve_invocation_dir.js +34 -0
- package/src/utils/resolve_invocation_dir.spec.js +102 -0
- package/src/utils/utils.js +211 -0
- package/src/utils/utils.spec.js +448 -0
- package/src/worker/bundler_options.js +43 -0
- package/src/worker/catalog_workflow/catalog.js +114 -0
- package/src/worker/catalog_workflow/index.js +54 -0
- package/src/worker/catalog_workflow/index.spec.js +196 -0
- package/src/worker/catalog_workflow/workflow.js +24 -0
- package/src/worker/configs.js +49 -0
- package/src/worker/configs.spec.js +130 -0
- package/src/worker/index.js +89 -0
- package/src/worker/index.spec.js +177 -0
- package/src/worker/interceptors/activity.js +62 -0
- package/src/worker/interceptors/activity.spec.js +212 -0
- package/src/worker/interceptors/workflow.js +70 -0
- package/src/worker/interceptors/workflow.spec.js +167 -0
- package/src/worker/interceptors.js +10 -0
- package/src/worker/loader.js +151 -0
- package/src/worker/loader.spec.js +236 -0
- package/src/worker/loader_tools.js +132 -0
- package/src/worker/loader_tools.spec.js +156 -0
- package/src/worker/log_hooks.js +95 -0
- package/src/worker/log_hooks.spec.js +217 -0
- package/src/worker/sandboxed_utils.js +18 -0
- package/src/worker/shutdown.js +26 -0
- package/src/worker/shutdown.spec.js +82 -0
- package/src/worker/sinks.js +74 -0
- package/src/worker/start_catalog.js +36 -0
- package/src/worker/start_catalog.spec.js +118 -0
- package/src/worker/webpack_loaders/consts.js +9 -0
- package/src/worker/webpack_loaders/tools.js +548 -0
- package/src/worker/webpack_loaders/tools.spec.js +330 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +613 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import traverseModule from '@babel/traverse';
|
|
2
|
+
import { isArrowFunctionExpression, isIdentifier } from '@babel/types';
|
|
3
|
+
import {
|
|
4
|
+
toFunctionExpression,
|
|
5
|
+
createThisMethodCall,
|
|
6
|
+
isFunction,
|
|
7
|
+
bindThisAtCallSite,
|
|
8
|
+
isFunctionLikeBinding
|
|
9
|
+
} from '../tools.js';
|
|
10
|
+
|
|
11
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
12
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check whether a CallExpression callee is a simple Identifier.
|
|
16
|
+
* Only direct identifier calls are rewritten; member/dynamic calls are skipped.
|
|
17
|
+
*
|
|
18
|
+
* We only support rewriting `Foo()` calls that refer to imported steps/flows/evaluators
|
|
19
|
+
* or local call-chain functions. Calls like `obj.Foo()` or `(getFn())()` are out of scope.
|
|
20
|
+
*
|
|
21
|
+
* Examples:
|
|
22
|
+
* - Supported: `Foo()`
|
|
23
|
+
* - Skipped: `obj.Foo()`, `(getFn())()`
|
|
24
|
+
*
|
|
25
|
+
* @param {import('@babel/traverse').NodePath} cPath - Path to a CallExpression node.
|
|
26
|
+
* @returns {boolean} True when callee is an Identifier.
|
|
27
|
+
*/
|
|
28
|
+
const isIdentifierCallee = cPath => isIdentifier( cPath.node.callee );
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert an ArrowFunctionExpression at the given path into a FunctionExpression
|
|
32
|
+
* to ensure dynamic `this` semantics inside the function body.
|
|
33
|
+
*
|
|
34
|
+
* Workflow code relies on `this` to invoke steps/flows (e.g., `this.invokeStep(...)`).
|
|
35
|
+
* Arrow functions capture `this` lexically, which would break that contract.
|
|
36
|
+
*
|
|
37
|
+
* If the node is an arrow, it is replaced by an equivalent FunctionExpression and
|
|
38
|
+
* the `state.rewrote` flag is set. If not an arrow, this is a no-op.
|
|
39
|
+
*
|
|
40
|
+
* @param {import('@babel/traverse').NodePath} nodePath - Path to a function node.
|
|
41
|
+
* @param {{ rewrote: boolean }} state - Mutation target to indicate a rewrite occurred.
|
|
42
|
+
* @returns {void}
|
|
43
|
+
*/
|
|
44
|
+
const normalizeArrowToFunctionPath = ( nodePath, state ) => {
|
|
45
|
+
if ( isArrowFunctionExpression( nodePath.node ) ) {
|
|
46
|
+
nodePath.replaceWith( toFunctionExpression( nodePath.node ) );
|
|
47
|
+
state.rewrote = true;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Rewrite calls inside a function body and collect call-chain functions discovered within.
|
|
52
|
+
* - Imported calls (steps/shared/evaluators/flows) are rewritten to `this.invokeX` or `this.startWorkflow`.
|
|
53
|
+
* - Local call-chain function calls are rewritten to `fn.call(this, ...)` to bind `this` correctly.
|
|
54
|
+
* - Returns a map of call-chain function name -> binding path for further recursive processing.
|
|
55
|
+
*
|
|
56
|
+
* @param {import('@babel/traverse').NodePath} bodyPath - Path to a function's body node.
|
|
57
|
+
* @param {Array<{ list: Array<any>, method: string, key: string }>} descriptors - Import rewrite descriptors.
|
|
58
|
+
* @param {{ rewrote: boolean }} state - Mutable state used to flag that edits were performed.
|
|
59
|
+
* @returns {Map<string, import('@babel/traverse').NodePath>} Discovered call-chain function bindings.
|
|
60
|
+
*/
|
|
61
|
+
const rewriteCallsInBody = ( bodyPath, descriptors, state ) => {
|
|
62
|
+
const callChainFunctions = new Map();
|
|
63
|
+
bodyPath.traverse( {
|
|
64
|
+
CallExpression: cPath => {
|
|
65
|
+
if ( !isIdentifierCallee( cPath ) ) {
|
|
66
|
+
return; // Only identifier callees are supported (skip member/dynamic)
|
|
67
|
+
}
|
|
68
|
+
const callee = cPath.node.callee;
|
|
69
|
+
|
|
70
|
+
// Rewrite imported calls (steps/shared/evaluators/flows)
|
|
71
|
+
for ( const { list, method, key } of descriptors ) {
|
|
72
|
+
const found = list.find( x => x.localName === callee.name );
|
|
73
|
+
if ( found ) {
|
|
74
|
+
const args = cPath.node.arguments;
|
|
75
|
+
cPath.replaceWith( createThisMethodCall( method, found[key], args ) );
|
|
76
|
+
state.rewrote = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Rewrite local call-chain function calls and track for recursive processing
|
|
82
|
+
const binding = cPath.scope.getBinding( callee.name );
|
|
83
|
+
if ( !binding ) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if ( !isFunctionLikeBinding( binding.path.node ) ) {
|
|
87
|
+
return; // Not a function-like binding
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Queue call-chain function for recursive processing
|
|
91
|
+
if ( !callChainFunctions.has( callee.name ) ) {
|
|
92
|
+
callChainFunctions.set( callee.name, binding.path );
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Bind `this` at callsite: fn(...) -> fn.call(this, ...)
|
|
96
|
+
cPath.replaceWith( bindThisAtCallSite( callee.name, cPath.node.arguments ) );
|
|
97
|
+
state.rewrote = true;
|
|
98
|
+
}
|
|
99
|
+
} );
|
|
100
|
+
return callChainFunctions;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Recursively process a call-chain function:
|
|
105
|
+
* - Ensures the function is a FunctionExpression (converts arrow when needed).
|
|
106
|
+
* - Rewrites calls inside the function using `rewriteCallsInBody`.
|
|
107
|
+
* - Follows nested call-chain functions depth-first while avoiding cycles via `processedFns`.
|
|
108
|
+
*
|
|
109
|
+
* @param {object} params - Params for processing a call-chain function.
|
|
110
|
+
* @param {string} params.name - Local identifier name in the current scope.
|
|
111
|
+
* @param {import('@babel/traverse').NodePath} params.bindingPath - Binding path of the function declaration.
|
|
112
|
+
* @param {{ rewrote: boolean }} params.state - Mutable state used to flag that edits were performed.
|
|
113
|
+
* @param {Array<{ list: Array<any>, method: string, key: string }>} params.descriptors - Import rewrite descriptors.
|
|
114
|
+
* @param {Set<string>} [params.processedFns] - Already processed names to avoid cycles.
|
|
115
|
+
*/
|
|
116
|
+
const processFunction = ( { name, bindingPath, state, descriptors, processedFns = new Set() } ) => {
|
|
117
|
+
// Avoid infinite loops for recursive/repeated references
|
|
118
|
+
if ( processedFns.has( name ) || bindingPath.removed ) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
processedFns.add( name );
|
|
122
|
+
|
|
123
|
+
if ( bindingPath.isVariableDeclarator() ) {
|
|
124
|
+
// Case 1: const foo = <function or arrow>
|
|
125
|
+
const initPath = bindingPath.get( 'init' );
|
|
126
|
+
// Arrow functions capture `this` lexically; normalize for dynamic `this`
|
|
127
|
+
normalizeArrowToFunctionPath( initPath, state );
|
|
128
|
+
// Rewrite calls in body; collect nested call-chain functions from this scope
|
|
129
|
+
const callChainFunctions = rewriteCallsInBody( initPath.get( 'body' ), descriptors, state );
|
|
130
|
+
// DFS: process nested call-chain functions (processedFns prevents cycles)
|
|
131
|
+
callChainFunctions.forEach( ( childBindingPath, childName ) => {
|
|
132
|
+
processFunction( { name: childName, bindingPath: childBindingPath, state, descriptors, processedFns } );
|
|
133
|
+
} );
|
|
134
|
+
} else if ( bindingPath.isFunctionDeclaration() ) {
|
|
135
|
+
// Case 2: function foo(...) { ... }
|
|
136
|
+
// Function declarations already have dynamic `this`; no normalization needed
|
|
137
|
+
const callChainFunctions = rewriteCallsInBody( bindingPath.get( 'body' ), descriptors, state );
|
|
138
|
+
// Continue DFS into any functions called from this declaration
|
|
139
|
+
callChainFunctions.forEach( ( childBindingPath, childName ) => {
|
|
140
|
+
processFunction( { name: childName, bindingPath: childBindingPath, state, descriptors, processedFns } );
|
|
141
|
+
} );
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Rewrite calls to imported steps/workflows within `fn` object properties.
|
|
147
|
+
* Converts arrow fns to functions and replaces `StepX(...)` with
|
|
148
|
+
* `this.invokeStep('name', ...)` and `FlowY(...)` with
|
|
149
|
+
* `this.startWorkflow('name', ...)`.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} params
|
|
152
|
+
* @param {import('@babel/types').File} params.ast - Parsed file AST.
|
|
153
|
+
* @param {Array<{localName:string,stepName:string}>} params.stepImports - Step imports.
|
|
154
|
+
* @param {Array<{localName:string,stepName:string}>} params.sharedStepImports - Shared step imports.
|
|
155
|
+
* @param {Array<{localName:string,evaluatorName:string}>} params.evaluatorImports - Evaluator imports.
|
|
156
|
+
* @param {Array<{localName:string,workflowName:string}>} params.flowImports - Workflow imports.
|
|
157
|
+
* @returns {boolean} True if the AST was modified; false otherwise.
|
|
158
|
+
*/
|
|
159
|
+
export default function rewriteFnBodies( { ast, stepImports, sharedStepImports = [], evaluatorImports, sharedEvaluatorImports = [], flowImports } ) {
|
|
160
|
+
const state = { rewrote: false };
|
|
161
|
+
// Build rewrite descriptors once per traversal
|
|
162
|
+
const descriptors = [
|
|
163
|
+
{ list: stepImports, method: 'invokeStep', key: 'stepName' },
|
|
164
|
+
{ list: sharedStepImports, method: 'invokeSharedStep', key: 'stepName' },
|
|
165
|
+
{ list: evaluatorImports, method: 'invokeEvaluator', key: 'evaluatorName' },
|
|
166
|
+
{ list: sharedEvaluatorImports, method: 'invokeSharedEvaluator', key: 'evaluatorName' },
|
|
167
|
+
{ list: flowImports, method: 'startWorkflow', key: 'workflowName' }
|
|
168
|
+
];
|
|
169
|
+
traverse( ast, {
|
|
170
|
+
ObjectProperty: path => {
|
|
171
|
+
// Only transform object properties named 'fn'
|
|
172
|
+
if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const val = path.node.value;
|
|
177
|
+
|
|
178
|
+
// Only functions (including arrows) are eligible
|
|
179
|
+
if ( !isFunction( val ) && !isArrowFunctionExpression( val ) ) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Normalize arrow to function for correct dynamic `this`
|
|
184
|
+
normalizeArrowToFunctionPath( path.get( 'value' ), state );
|
|
185
|
+
|
|
186
|
+
// Rewrite the main workflow fn body and collect call-chain functions discovered within it
|
|
187
|
+
const callChainFunctions = rewriteCallsInBody( path.get( 'value.body' ), descriptors, state );
|
|
188
|
+
|
|
189
|
+
// Recursively rewrite call-chain functions and any functions they call
|
|
190
|
+
callChainFunctions.forEach( ( bindingPath, name ) => {
|
|
191
|
+
processFunction( { name, bindingPath, state, descriptors } );
|
|
192
|
+
} );
|
|
193
|
+
}
|
|
194
|
+
} );
|
|
195
|
+
return state.rewrote;
|
|
196
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import generatorModule from '@babel/generator';
|
|
3
|
+
import { parse } from '../tools.js';
|
|
4
|
+
import rewriteFnBodies from './rewrite_fn_bodies.js';
|
|
5
|
+
|
|
6
|
+
const generate = generatorModule.default ?? generatorModule;
|
|
7
|
+
|
|
8
|
+
describe( 'rewrite_fn_bodies', () => {
|
|
9
|
+
it( 'converts arrow to function and rewrites step/workflow calls', () => {
|
|
10
|
+
const src = `
|
|
11
|
+
const obj = {
|
|
12
|
+
fn: async x => {
|
|
13
|
+
StepA( 1 );
|
|
14
|
+
FlowB( 2 );
|
|
15
|
+
}
|
|
16
|
+
}`;
|
|
17
|
+
const ast = parse( src, 'file.js' );
|
|
18
|
+
const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
|
|
19
|
+
const flowImports = [ { localName: 'FlowB', workflowName: 'flow.b' } ];
|
|
20
|
+
|
|
21
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports: [], flowImports } );
|
|
22
|
+
expect( rewrote ).toBe( true );
|
|
23
|
+
|
|
24
|
+
const code = ast.program.body.map( n => n.type ).length; // smoke: ast mutated
|
|
25
|
+
expect( code ).toBeGreaterThan( 0 );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'rewrites evaluator calls to this.invokeEvaluator', () => {
|
|
29
|
+
const src = `
|
|
30
|
+
const obj = {
|
|
31
|
+
fn: async x => {
|
|
32
|
+
EvalA(3);
|
|
33
|
+
}
|
|
34
|
+
};`;
|
|
35
|
+
const ast = parse( src, 'file.js' );
|
|
36
|
+
const evaluatorImports = [ { localName: 'EvalA', evaluatorName: 'eval.a' } ];
|
|
37
|
+
const rewrote = rewriteFnBodies( { ast, stepImports: [], evaluatorImports, flowImports: [] } );
|
|
38
|
+
expect( rewrote ).toBe( true );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'does nothing when no matching calls are present', () => {
|
|
42
|
+
const src = [ 'const obj = { fn: function() { other(); } }' ].join( '\n' );
|
|
43
|
+
const ast = parse( src, 'file.js' );
|
|
44
|
+
const rewrote = rewriteFnBodies( { ast, stepImports: [], evaluatorImports: [], flowImports: [] } );
|
|
45
|
+
expect( rewrote ).toBe( false );
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'rewrites helper calls and helper bodies (steps and evaluators)', () => {
|
|
49
|
+
const src = `
|
|
50
|
+
const foo = async () => {
|
|
51
|
+
StepA( 1 );
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function bar( x ) {
|
|
55
|
+
EvalA( x );
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const obj = {
|
|
59
|
+
fn: async x => {
|
|
60
|
+
foo();
|
|
61
|
+
bar( 2 );
|
|
62
|
+
}
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
const ast = parse( src, 'file.js' );
|
|
66
|
+
const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
|
|
67
|
+
const evaluatorImports = [ { localName: 'EvalA', evaluatorName: 'eval.a' } ];
|
|
68
|
+
|
|
69
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, sharedStepImports: [], evaluatorImports, flowImports: [] } );
|
|
70
|
+
expect( rewrote ).toBe( true );
|
|
71
|
+
|
|
72
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
73
|
+
|
|
74
|
+
// Helper calls in fn are rewritten to call(this, ...)
|
|
75
|
+
expect( code ).toMatch( /foo\.call\(this\)/ );
|
|
76
|
+
expect( code ).toMatch( /bar\.call\(this,\s*2\)/ );
|
|
77
|
+
|
|
78
|
+
// Inside helpers, calls are rewritten
|
|
79
|
+
expect( code ).toMatch( /this\.invokeStep\(([\"'])step\.a\1,\s*1\)/ );
|
|
80
|
+
expect( code ).toMatch( /this\.invokeEvaluator\(([\"'])eval\.a\1,\s*x\)/ );
|
|
81
|
+
|
|
82
|
+
// Arrow helper converted to function expression to allow dynamic this
|
|
83
|
+
expect( code ).toMatch( /const foo = async function/ );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
it( 'rewrites nested helper chains until the step invocation', () => {
|
|
87
|
+
const src = `
|
|
88
|
+
const foo = () => {
|
|
89
|
+
bar();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function bar() {
|
|
93
|
+
baz( 42 );
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const baz = n => {
|
|
97
|
+
StepA( n );
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const obj = {
|
|
101
|
+
fn: async () => {
|
|
102
|
+
foo();
|
|
103
|
+
}
|
|
104
|
+
}`;
|
|
105
|
+
|
|
106
|
+
const ast = parse( src, 'file.js' );
|
|
107
|
+
const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
|
|
108
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports: [], flowImports: [] } );
|
|
109
|
+
expect( rewrote ).toBe( true );
|
|
110
|
+
|
|
111
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
112
|
+
// Calls along the chain are bound with this
|
|
113
|
+
expect( code ).toMatch( /foo\.call\(this\)/ );
|
|
114
|
+
expect( code ).toMatch( /bar\.call\(this\)/ );
|
|
115
|
+
expect( code ).toMatch( /baz\.call\(this,\s*42\)/ );
|
|
116
|
+
// Deep step rewrite in the last helper
|
|
117
|
+
expect( code ).toMatch( /this\.invokeStep\(([\"'])step\.a\1,\s*n\)/ );
|
|
118
|
+
// Arrow helpers converted to functions
|
|
119
|
+
expect( code ).toMatch( /const foo = function/ );
|
|
120
|
+
expect( code ).toMatch( /const baz = function/ );
|
|
121
|
+
} );
|
|
122
|
+
} );
|
|
123
|
+
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import traverseModule from '@babel/traverse';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { parse, toAbsolutePath, getFileKind, isAnyStepsPath, isAnyEvaluatorsPath, isWorkflowPath } from '../tools.js';
|
|
4
|
+
import { ComponentFile } from '../consts.js';
|
|
5
|
+
import {
|
|
6
|
+
isCallExpression,
|
|
7
|
+
isFunctionExpression,
|
|
8
|
+
isArrowFunctionExpression,
|
|
9
|
+
isIdentifier,
|
|
10
|
+
isImportDefaultSpecifier,
|
|
11
|
+
isImportSpecifier,
|
|
12
|
+
isObjectPattern,
|
|
13
|
+
isObjectProperty,
|
|
14
|
+
isStringLiteral
|
|
15
|
+
} from '@babel/types';
|
|
16
|
+
|
|
17
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
18
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Determine the file kind label for error messages.
|
|
22
|
+
* Handles both flat files (steps.js) and folder-based files (steps/fetch_data.js).
|
|
23
|
+
* @param {string} filename - The file path
|
|
24
|
+
* @returns {string} Human-readable file kind for error messages
|
|
25
|
+
*/
|
|
26
|
+
const getFileKindLabel = filename => {
|
|
27
|
+
if ( isAnyStepsPath( filename ) ) {
|
|
28
|
+
return 'steps.js';
|
|
29
|
+
}
|
|
30
|
+
if ( isAnyEvaluatorsPath( filename ) ) {
|
|
31
|
+
return 'evaluators.js';
|
|
32
|
+
}
|
|
33
|
+
if ( /workflow\.js$/.test( filename ) ) {
|
|
34
|
+
return 'workflow.js';
|
|
35
|
+
}
|
|
36
|
+
return filename;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate that component instantiation calls occur in the correct file locations.
|
|
41
|
+
* - step() must be called in a file whose path contains 'steps'
|
|
42
|
+
* - evaluator() must be called in a file whose path contains 'evaluators'
|
|
43
|
+
* - workflow() must be called in a file whose path contains 'workflow'
|
|
44
|
+
* @param {string} calleeName - The factory function name (step, evaluator, workflow)
|
|
45
|
+
* @param {string} filename - The file path where the call occurs
|
|
46
|
+
*/
|
|
47
|
+
const validateInstantiationLocation = ( calleeName, filename ) => {
|
|
48
|
+
if ( calleeName === 'step' && !isAnyStepsPath( filename ) ) {
|
|
49
|
+
throw new Error( `Invalid instantiation location: step() can only be called in files with 'steps' in the path. Found in: ${filename}` );
|
|
50
|
+
}
|
|
51
|
+
if ( calleeName === 'evaluator' && !isAnyEvaluatorsPath( filename ) ) {
|
|
52
|
+
throw new Error( `Invalid instantiation location: evaluator() can only be called in files with 'evaluators' in the path. Found in: ${filename}` );
|
|
53
|
+
}
|
|
54
|
+
if ( calleeName === 'workflow' && !isWorkflowPath( filename ) ) {
|
|
55
|
+
throw new Error( `Invalid instantiation location: workflow() can only be called in files with 'workflow' in the path. Found in: ${filename}` );
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Webpack loader that validates component instantiation and fn body calls.
|
|
61
|
+
* Returns the source unchanged unless a validation error is found.
|
|
62
|
+
*
|
|
63
|
+
* Rules enforced:
|
|
64
|
+
* - Instantiation location: step() must be in steps path, evaluator() in evaluators path, workflow() in workflow path
|
|
65
|
+
* - steps.js `fn`: calling any step, evaluator, or workflow inside fn body emits warning
|
|
66
|
+
* - evaluators.js `fn`: calling any step, evaluator, or workflow inside fn body emits warning
|
|
67
|
+
*
|
|
68
|
+
* NOTE: Import restrictions have been removed - any file can import any other file.
|
|
69
|
+
*
|
|
70
|
+
* @param {string|Buffer} source
|
|
71
|
+
* @param {any} inputMap
|
|
72
|
+
* @this {import('webpack').LoaderContext<{}>}
|
|
73
|
+
*/
|
|
74
|
+
export default function workflowValidatorLoader( source, inputMap ) {
|
|
75
|
+
this.cacheable?.( true );
|
|
76
|
+
const callback = this.async?.() ?? this.callback;
|
|
77
|
+
const emitWarning = this.emitWarning?.bind( this ) ?? ( () => {} );
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const filename = this.resourcePath;
|
|
81
|
+
const fileDir = dirname( filename );
|
|
82
|
+
const ast = parse( String( source ), filename );
|
|
83
|
+
|
|
84
|
+
const fileKind = getFileKind( filename );
|
|
85
|
+
|
|
86
|
+
// Collect local declarations and imported identifiers by type
|
|
87
|
+
const localStepIds = new Set();
|
|
88
|
+
const localEvaluatorIds = new Set();
|
|
89
|
+
const importedStepIds = new Set();
|
|
90
|
+
const importedEvaluatorIds = new Set();
|
|
91
|
+
const importedWorkflowIds = new Set();
|
|
92
|
+
|
|
93
|
+
// First pass: collect imported identifiers for fn body call checks
|
|
94
|
+
traverse( ast, {
|
|
95
|
+
ImportDeclaration: path => {
|
|
96
|
+
const specifier = path.node.source.value;
|
|
97
|
+
|
|
98
|
+
// Collect imported identifiers for later call checks
|
|
99
|
+
const importedKind = getFileKind( specifier );
|
|
100
|
+
const accumulator = ( {
|
|
101
|
+
[ComponentFile.STEPS]: importedStepIds,
|
|
102
|
+
[ComponentFile.EVALUATORS]: importedEvaluatorIds,
|
|
103
|
+
[ComponentFile.WORKFLOW]: importedWorkflowIds
|
|
104
|
+
} )[importedKind];
|
|
105
|
+
if ( accumulator ) {
|
|
106
|
+
for ( const s of path.node.specifiers ) {
|
|
107
|
+
if ( isImportSpecifier( s ) || isImportDefaultSpecifier( s ) ) {
|
|
108
|
+
accumulator.add( s.local.name );
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
VariableDeclarator: path => {
|
|
114
|
+
const init = path.node.init;
|
|
115
|
+
if ( !isCallExpression( init ) ) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate instantiation location for step/evaluator/workflow calls
|
|
120
|
+
if ( isIdentifier( init.callee, { name: 'step' } ) ) {
|
|
121
|
+
validateInstantiationLocation( 'step', filename );
|
|
122
|
+
if ( isIdentifier( path.node.id ) ) {
|
|
123
|
+
localStepIds.add( path.node.id.name );
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if ( isIdentifier( init.callee, { name: 'evaluator' } ) ) {
|
|
127
|
+
validateInstantiationLocation( 'evaluator', filename );
|
|
128
|
+
if ( isIdentifier( path.node.id ) ) {
|
|
129
|
+
localEvaluatorIds.add( path.node.id.name );
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if ( isIdentifier( init.callee, { name: 'workflow' } ) ) {
|
|
133
|
+
validateInstantiationLocation( 'workflow', filename );
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// CommonJS requires: collect identifiers for fn body call checks
|
|
137
|
+
if ( isIdentifier( init.callee, { name: 'require' } ) ) {
|
|
138
|
+
const firstArg = init.arguments[0];
|
|
139
|
+
if ( !isStringLiteral( firstArg ) ) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const req = firstArg.value;
|
|
143
|
+
|
|
144
|
+
// Collect imported identifiers from require patterns
|
|
145
|
+
const reqType = getFileKind( toAbsolutePath( fileDir, req ) );
|
|
146
|
+
if ( reqType === ComponentFile.STEPS && isObjectPattern( path.node.id ) ) {
|
|
147
|
+
for ( const prop of path.node.id.properties ) {
|
|
148
|
+
if ( isObjectProperty( prop ) && isIdentifier( prop.value ) ) {
|
|
149
|
+
importedStepIds.add( prop.value.name );
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ( reqType === ComponentFile.EVALUATORS && isObjectPattern( path.node.id ) ) {
|
|
154
|
+
for ( const prop of path.node.id.properties ) {
|
|
155
|
+
if ( isObjectProperty( prop ) && isIdentifier( prop.value ) ) {
|
|
156
|
+
importedEvaluatorIds.add( prop.value.name );
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if ( reqType === ComponentFile.WORKFLOW && isIdentifier( path.node.id ) ) {
|
|
161
|
+
importedWorkflowIds.add( path.node.id.name );
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
// Function-body call validations for steps/evaluators files
|
|
168
|
+
if ( [ ComponentFile.STEPS, ComponentFile.EVALUATORS ].includes( fileKind ) ) {
|
|
169
|
+
traverse( ast, {
|
|
170
|
+
ObjectProperty: path => {
|
|
171
|
+
if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const val = path.node.value;
|
|
175
|
+
if ( !isFunctionExpression( val ) && !isArrowFunctionExpression( val ) ) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
path.get( 'value' ).traverse( {
|
|
180
|
+
CallExpression: cPath => {
|
|
181
|
+
const callee = cPath.node.callee;
|
|
182
|
+
if ( isIdentifier( callee ) ) {
|
|
183
|
+
const { name } = callee;
|
|
184
|
+
const fileLabel = getFileKindLabel( filename );
|
|
185
|
+
const violation = [
|
|
186
|
+
[ 'step', localStepIds.has( name ) || importedStepIds.has( name ) ],
|
|
187
|
+
[ 'evaluator', localEvaluatorIds.has( name ) || importedEvaluatorIds.has( name ) ],
|
|
188
|
+
[ 'workflow', importedWorkflowIds.has( name ) ]
|
|
189
|
+
].find( v => v[1] )?.[0];
|
|
190
|
+
|
|
191
|
+
if ( violation ) {
|
|
192
|
+
emitWarning( new Error( `Invalid call in ${fileLabel} fn: calling a ${violation} ('${name}') is not allowed in ${filename}` ) );
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} );
|
|
197
|
+
}
|
|
198
|
+
} );
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return callback( null, source, inputMap );
|
|
202
|
+
} catch ( err ) {
|
|
203
|
+
return callback( err );
|
|
204
|
+
}
|
|
205
|
+
}
|