@outputai/core 0.10.0 → 0.10.1-next.09ed166.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/package.json +1 -1
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +2 -1
- package/src/helpers/object.js +48 -35
- package/src/helpers/object.spec.js +123 -75
- package/src/hooks/index.d.ts +48 -45
- package/src/hooks/index.js +49 -44
- package/src/hooks/index.spec.js +74 -102
- package/src/interface/workflow.js +92 -85
- package/src/interface/workflow.spec.js +135 -142
- package/src/sdk/helpers/objects.d.ts +24 -19
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +3 -3
- package/src/worker/index.spec.js +6 -6
- package/src/worker/interceptors/activity.js +4 -4
- package/src/worker/interceptors/activity.spec.js +9 -9
- package/src/worker/interceptors/workflow.js +5 -5
- package/src/worker/interceptors/workflow.spec.js +7 -7
- package/src/worker/loader/activities.js +56 -40
- package/src/worker/loader/activities.spec.js +21 -22
- package/src/worker/loader/workflows.spec.js +1 -2
- package/src/worker/log_hooks.js +9 -9
- package/src/worker/log_hooks.spec.js +2 -2
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/webpack_loaders/consts.js +6 -0
- package/src/worker/webpack_loaders/tools.js +1 -146
- package/src/worker/webpack_loaders/tools.spec.js +0 -23
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import loader from './index.mjs';
|
|
6
6
|
|
|
7
|
+
const { invokeActivitySymbolKey } = vi.hoisted( () => ( {
|
|
8
|
+
invokeActivitySymbolKey: 'test:invoke_activity'
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
vi.mock( '#consts', async importOriginal => ( {
|
|
12
|
+
...await importOriginal(),
|
|
13
|
+
INVOKE_ACTIVITY_SYMBOL: Symbol.for( invokeActivitySymbolKey )
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
7
16
|
function runLoader( source, resourcePath ) {
|
|
8
17
|
return new Promise( ( resolve, reject ) => {
|
|
9
18
|
const ctx = {
|
|
@@ -16,8 +25,12 @@ function runLoader( source, resourcePath ) {
|
|
|
16
25
|
} );
|
|
17
26
|
}
|
|
18
27
|
|
|
28
|
+
const escapeRegExp = value => value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
|
|
29
|
+
const invokeActivityPattern = activityName =>
|
|
30
|
+
new RegExp( `globalThis\\[globalThis\\.Symbol\\.for\\(([\"'])${escapeRegExp( invokeActivitySymbolKey )}\\1\\)\\]\\('${activityName}'` );
|
|
31
|
+
|
|
19
32
|
describe( 'workflows_rewriter Webpack loader spec', () => {
|
|
20
|
-
it( 'rewrites ESM
|
|
33
|
+
it( 'rewrites ESM activity imports and leaves workflow imports intact', async () => {
|
|
21
34
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-' ) );
|
|
22
35
|
writeFileSync( join( dir, 'steps.js' ), 'export const StepA = step({ name: \'step.a\' });' );
|
|
23
36
|
writeFileSync( join( dir, 'workflow.js' ), `
|
|
@@ -29,7 +42,7 @@ import { StepA } from './steps.js';
|
|
|
29
42
|
import FlowDef, { FlowA } from './workflow.js';
|
|
30
43
|
|
|
31
44
|
const obj = {
|
|
32
|
-
fn: async
|
|
45
|
+
fn: async x => {
|
|
33
46
|
StepA(1);
|
|
34
47
|
FlowA(2);
|
|
35
48
|
FlowDef(3);
|
|
@@ -40,65 +53,14 @@ const obj = {
|
|
|
40
53
|
|
|
41
54
|
expect( code ).not.toMatch( /from '\.\/steps\.js'/ );
|
|
42
55
|
expect( code ).toMatch( /from '\.\/workflow\.js'/ );
|
|
43
|
-
expect( code ).toMatch(
|
|
44
|
-
expect( code ).toMatch(
|
|
56
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
|
|
57
|
+
expect( code ).toMatch( /,\s*1\)/ );
|
|
45
58
|
expect( code ).toMatch( /FlowA\(2\)/ );
|
|
46
59
|
expect( code ).toMatch( /FlowDef\(3\)/ );
|
|
47
|
-
|
|
48
|
-
rmSync( dir, { recursive: true, force: true } );
|
|
49
|
-
} );
|
|
50
|
-
|
|
51
|
-
it( 'rewrites ESM shared steps imports to invokeSharedStep', async () => {
|
|
52
|
-
// Create directory structure: shared/steps/common.js
|
|
53
|
-
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-' ) );
|
|
54
|
-
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
55
|
-
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
56
|
-
writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedA = step({ name: \'shared.a\' });' );
|
|
57
|
-
|
|
58
|
-
const source = `
|
|
59
|
-
import { SharedA } from '../../shared/steps/common.js';
|
|
60
|
-
|
|
61
|
-
const obj = {
|
|
62
|
-
fn: async (x) => {
|
|
63
|
-
SharedA(1);
|
|
64
|
-
}
|
|
65
|
-
}`;
|
|
66
|
-
|
|
67
|
-
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
68
|
-
|
|
69
|
-
expect( code ).not.toMatch( /from '\.\.\/\.\.\/shared\/steps\/common\.js'/ );
|
|
70
|
-
expect( code ).toMatch( /fn:\s*async function \(x\)/ );
|
|
71
|
-
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.a',\s*1\)/ );
|
|
72
|
-
|
|
73
60
|
rmSync( dir, { recursive: true, force: true } );
|
|
74
61
|
} );
|
|
75
62
|
|
|
76
|
-
it( 'rewrites CJS
|
|
77
|
-
// Create directory structure: shared/steps/common.js
|
|
78
|
-
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-' ) );
|
|
79
|
-
mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
|
|
80
|
-
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
81
|
-
writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedB = step({ name: \'shared.b\' });' );
|
|
82
|
-
|
|
83
|
-
const source = `
|
|
84
|
-
const { SharedB } = require( '../../shared/steps/common.js' );
|
|
85
|
-
|
|
86
|
-
const obj = {
|
|
87
|
-
fn: async (y) => {
|
|
88
|
-
SharedB();
|
|
89
|
-
}
|
|
90
|
-
}`;
|
|
91
|
-
|
|
92
|
-
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
93
|
-
|
|
94
|
-
expect( code ).not.toMatch( /require\('\.\.\/\.\.\/shared\/steps\/common\.js'\)/ );
|
|
95
|
-
expect( code ).toMatch( /fn:\s*async function \(y\)/ );
|
|
96
|
-
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.b'\)/ );
|
|
97
|
-
|
|
98
|
-
rmSync( dir, { recursive: true, force: true } );
|
|
99
|
-
} );
|
|
100
|
-
|
|
101
|
-
it( 'rewrites CJS step requires and leaves workflow calls intact', async () => {
|
|
63
|
+
it( 'rewrites CJS activity requires and leaves workflow requires intact', async () => {
|
|
102
64
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-' ) );
|
|
103
65
|
writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: \'step.b\' });' );
|
|
104
66
|
writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: \'flow.c\' });' );
|
|
@@ -108,8 +70,8 @@ const { StepB } = require( './steps.js' );
|
|
|
108
70
|
const FlowDefault = require( './workflow.js' );
|
|
109
71
|
|
|
110
72
|
const obj = {
|
|
111
|
-
fn: async
|
|
112
|
-
StepB();
|
|
73
|
+
fn: async y => {
|
|
74
|
+
StepB(y);
|
|
113
75
|
FlowDefault();
|
|
114
76
|
}
|
|
115
77
|
}`;
|
|
@@ -118,97 +80,73 @@ const obj = {
|
|
|
118
80
|
|
|
119
81
|
expect( code ).not.toMatch( /require\('\.\/steps\.js'\)/ );
|
|
120
82
|
expect( code ).toMatch( /require\('\.\/workflow\.js'\)/ );
|
|
121
|
-
expect( code ).toMatch(
|
|
122
|
-
expect( code ).toMatch(
|
|
83
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.b' ) );
|
|
84
|
+
expect( code ).toMatch( /,\s*y\)/ );
|
|
123
85
|
expect( code ).toMatch( /FlowDefault\(\)/ );
|
|
124
|
-
|
|
125
86
|
rmSync( dir, { recursive: true, force: true } );
|
|
126
87
|
} );
|
|
127
88
|
|
|
128
|
-
it( '
|
|
129
|
-
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-
|
|
130
|
-
writeFileSync( join( dir, 'steps.js' ), `
|
|
131
|
-
const NAME = 'step.const';
|
|
132
|
-
export const StepC = step({ name: NAME });` );
|
|
133
|
-
writeFileSync( join( dir, 'workflow.js' ), `
|
|
134
|
-
const WF = 'wf.const';
|
|
135
|
-
export const FlowC = workflow({ name: WF });
|
|
136
|
-
const D = 'wf.def';
|
|
137
|
-
export default workflow({ name: D });` );
|
|
138
|
-
|
|
139
|
-
const source = `
|
|
140
|
-
import { StepC } from './steps.js';
|
|
141
|
-
import FlowDef, { FlowC } from './workflow.js';
|
|
142
|
-
const obj = { fn: async () => { StepC(); FlowC(); FlowDef(); } }`;
|
|
143
|
-
|
|
144
|
-
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
145
|
-
expect( code ).toMatch( /this\.invokeStep\('step\.const'\)/ );
|
|
146
|
-
expect( code ).toMatch( /FlowC\(\)/ );
|
|
147
|
-
expect( code ).toMatch( /FlowDef\(\)/ );
|
|
148
|
-
rmSync( dir, { recursive: true, force: true } );
|
|
149
|
-
} );
|
|
150
|
-
|
|
151
|
-
it( 'rewrites ESM shared evaluator imports to invokeSharedEvaluator', async () => {
|
|
152
|
-
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-eval-' ) );
|
|
89
|
+
it( 'rewrites shared activity imports from helper modules', async () => {
|
|
90
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-shared-helper-' ) );
|
|
153
91
|
mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
|
|
154
92
|
mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
|
|
155
|
-
writeFileSync(
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join( dir, 'shared', 'evaluators', 'common.js' ),
|
|
95
|
+
'export const SharedEval = evaluator({ name: \'shared.eval\' });'
|
|
96
|
+
);
|
|
156
97
|
|
|
157
98
|
const source = `
|
|
158
99
|
import { SharedEval } from '../../shared/evaluators/common.js';
|
|
159
100
|
|
|
160
|
-
const
|
|
161
|
-
fn: async (x) => {
|
|
162
|
-
SharedEval(1);
|
|
163
|
-
}
|
|
164
|
-
}`;
|
|
165
|
-
|
|
166
|
-
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
|
|
101
|
+
export const helper = async value => SharedEval(value);`;
|
|
167
102
|
|
|
168
|
-
|
|
169
|
-
expect( code ).toMatch( /fn:\s*async function \(x\)/ );
|
|
170
|
-
expect( code ).toMatch( /this\.invokeSharedEvaluator\('shared\.eval',\s*1\)/ );
|
|
103
|
+
const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'helper.js' ) );
|
|
171
104
|
|
|
105
|
+
expect( code ).not.toMatch( /shared\/evaluators\/common\.js/ );
|
|
106
|
+
expect( code ).toMatch( invokeActivityPattern( 'shared\\.eval' ) );
|
|
107
|
+
expect( code ).toMatch( /,\s*value\)/ );
|
|
108
|
+
expect( code ).toMatch( /export const helper = async value =>/ );
|
|
172
109
|
rmSync( dir, { recursive: true, force: true } );
|
|
173
110
|
} );
|
|
174
111
|
|
|
175
|
-
it( '
|
|
176
|
-
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
112
|
+
it( 'resolves top-level const activity names', async () => {
|
|
113
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-const-' ) );
|
|
114
|
+
writeFileSync( join( dir, 'steps.js' ), `
|
|
115
|
+
const NAME = 'step.const';
|
|
116
|
+
export const StepC = step({ name: NAME });` );
|
|
180
117
|
|
|
181
118
|
const source = `
|
|
182
|
-
|
|
119
|
+
import { StepC } from './steps.js';
|
|
120
|
+
const obj = { fn: async () => StepC() };`;
|
|
183
121
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
}`;
|
|
122
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
123
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.const' ) );
|
|
124
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
125
|
+
} );
|
|
189
126
|
|
|
190
|
-
|
|
127
|
+
it( 'throws when globalThis is shadowed at a rewritten call site', async () => {
|
|
128
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-globalthis-shadow-' ) );
|
|
129
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const StepA = step({ name: \'step.shadow\' });' );
|
|
191
130
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
131
|
+
const source = `
|
|
132
|
+
import { StepA } from './steps.js';
|
|
133
|
+
export function helper(globalThis) {
|
|
134
|
+
return StepA();
|
|
135
|
+
}`;
|
|
195
136
|
|
|
137
|
+
await expect( runLoader( source, join( dir, 'helper.js' ) ) ).rejects.toThrow( /globalThis.*shadowed/ );
|
|
196
138
|
rmSync( dir, { recursive: true, force: true } );
|
|
197
139
|
} );
|
|
198
140
|
|
|
199
|
-
it( '
|
|
141
|
+
it( 'propagates errors for non-static activity names', async () => {
|
|
200
142
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-error-' ) );
|
|
201
143
|
writeFileSync( join( dir, 'steps.js' ), `
|
|
202
144
|
function n() { return 'x'; }
|
|
203
145
|
export const StepX = step({ name: n() });` );
|
|
204
|
-
writeFileSync( join( dir, 'workflow.js' ), `
|
|
205
|
-
const base = 'a';
|
|
206
|
-
export default workflow({ name: \`\${base}-b\` });` );
|
|
207
146
|
|
|
208
147
|
const source = `
|
|
209
148
|
import { StepX } from './steps.js';
|
|
210
|
-
|
|
211
|
-
const obj = { fn: async () => { StepX(); WF(); } }`;
|
|
149
|
+
const obj = { fn: async () => StepX() };`;
|
|
212
150
|
|
|
213
151
|
await expect( runLoader( source, join( dir, 'file.js' ) ) ).rejects.toThrow( /Invalid step name/ );
|
|
214
152
|
rmSync( dir, { recursive: true, force: true } );
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import traverseModule from '@babel/traverse';
|
|
2
|
+
import { INVOKE_ACTIVITY_SYMBOL } from '#consts';
|
|
3
|
+
import {
|
|
4
|
+
callExpression,
|
|
5
|
+
identifier,
|
|
6
|
+
isIdentifier,
|
|
7
|
+
memberExpression,
|
|
8
|
+
stringLiteral
|
|
9
|
+
} from '@babel/types';
|
|
10
|
+
|
|
11
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
12
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
13
|
+
const invokeActivitySymbolKey = Symbol.keyFor( INVOKE_ACTIVITY_SYMBOL );
|
|
14
|
+
|
|
15
|
+
if ( !invokeActivitySymbolKey ) {
|
|
16
|
+
throw new Error( 'INVOKE_ACTIVITY_SYMBOL must be created with Symbol.for().' );
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Only direct function calls are rewritten. Member calls and dynamic callees stay untouched.
|
|
20
|
+
const isIdentifierCallee = cPath => isIdentifier( cPath.node.callee );
|
|
21
|
+
|
|
22
|
+
// Collapse collected activity imports into local identifier -> declared activity name.
|
|
23
|
+
const buildActivityNameMap = activityImports =>
|
|
24
|
+
activityImports.reduce( ( map, { localName, activityName } ) => map.set( localName, activityName ), new Map() );
|
|
25
|
+
|
|
26
|
+
// Ignore calls when the imported binding has been shadowed by a live local binding.
|
|
27
|
+
const hasLiveLocalBinding = ( path, name ) => {
|
|
28
|
+
const binding = path.scope.getBinding( name );
|
|
29
|
+
return binding && !binding.path.removed;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const assertGlobalThisIsSafe = path => {
|
|
33
|
+
if ( hasLiveLocalBinding( path, 'globalThis' ) ) {
|
|
34
|
+
throw new Error( 'Cannot rewrite activity call because "globalThis" is shadowed in this scope.' );
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Build the generated dispatcher call, preserving the original call arguments.
|
|
39
|
+
const createInvokeActivityCall = ( activityName, args ) =>
|
|
40
|
+
callExpression(
|
|
41
|
+
memberExpression(
|
|
42
|
+
identifier( 'globalThis' ),
|
|
43
|
+
callExpression(
|
|
44
|
+
memberExpression(
|
|
45
|
+
memberExpression( identifier( 'globalThis' ), identifier( 'Symbol' ) ),
|
|
46
|
+
identifier( 'for' )
|
|
47
|
+
),
|
|
48
|
+
[ stringLiteral( invokeActivitySymbolKey ) ]
|
|
49
|
+
),
|
|
50
|
+
true
|
|
51
|
+
),
|
|
52
|
+
[ stringLiteral( activityName ), ...args ]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Rewrite valid function-body activity calls; top-level call validation belongs to the validator loader.
|
|
56
|
+
const rewriteActivityCallsInFunctions = ( { ast, activityNames } ) => {
|
|
57
|
+
const state = { rewrote: false };
|
|
58
|
+
|
|
59
|
+
traverse( ast, {
|
|
60
|
+
CallExpression: path => {
|
|
61
|
+
if ( !path.getFunctionParent() || !isIdentifierCallee( path ) ) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const callee = path.node.callee;
|
|
66
|
+
const activityName = activityNames.get( callee.name );
|
|
67
|
+
if ( !activityName || hasLiveLocalBinding( path, callee.name ) ) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
assertGlobalThisIsSafe( path );
|
|
72
|
+
path.replaceWith( createInvokeActivityCall( activityName, path.node.arguments ) );
|
|
73
|
+
state.rewrote = true;
|
|
74
|
+
}
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
return state.rewrote;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Entry point: resolve collected component imports into plain dispatcher calls.
|
|
81
|
+
export default function rewriteActivityCalls( { ast, activityImports } ) {
|
|
82
|
+
const activityNames = buildActivityNameMap( activityImports );
|
|
83
|
+
if ( activityNames.size === 0 ) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return rewriteActivityCallsInFunctions( { ast, activityNames } );
|
|
88
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import generatorModule from '@babel/generator';
|
|
3
|
+
import { parse } from '../tools.js';
|
|
4
|
+
import rewriteActivityCalls from './rewrite_activity_calls.js';
|
|
5
|
+
|
|
6
|
+
const { invokeActivitySymbolKey } = vi.hoisted( () => ( {
|
|
7
|
+
invokeActivitySymbolKey: 'test:invoke_activity'
|
|
8
|
+
} ) );
|
|
9
|
+
|
|
10
|
+
vi.mock( '#consts', async importOriginal => ( {
|
|
11
|
+
...await importOriginal(),
|
|
12
|
+
INVOKE_ACTIVITY_SYMBOL: Symbol.for( invokeActivitySymbolKey )
|
|
13
|
+
} ) );
|
|
14
|
+
|
|
15
|
+
const generate = generatorModule.default ?? generatorModule;
|
|
16
|
+
const escapeRegExp = value => value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
|
|
17
|
+
const invokeActivityPattern = activityName =>
|
|
18
|
+
new RegExp( `globalThis\\[globalThis\\.Symbol\\.for\\(([\"'])${escapeRegExp( invokeActivitySymbolKey )}\\1\\)\\]\\(([\"'])${activityName}\\2` );
|
|
19
|
+
|
|
20
|
+
describe( 'rewrite_activity_calls', () => {
|
|
21
|
+
it( 'rewrites step calls inside functions without changing function shape', () => {
|
|
22
|
+
const src = `
|
|
23
|
+
const obj = {
|
|
24
|
+
fn: async x => {
|
|
25
|
+
StepA( 1 );
|
|
26
|
+
FlowB( 2 );
|
|
27
|
+
}
|
|
28
|
+
}`;
|
|
29
|
+
const ast = parse( src, 'file.js' );
|
|
30
|
+
const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
|
|
31
|
+
|
|
32
|
+
const rewrote = rewriteActivityCalls( { ast, activityImports } );
|
|
33
|
+
expect( rewrote ).toBe( true );
|
|
34
|
+
|
|
35
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
36
|
+
expect( code ).not.toMatch( /@outputai\/core\/invoker/ );
|
|
37
|
+
expect( code ).toMatch( /fn:\s*async x =>/ );
|
|
38
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
|
|
39
|
+
expect( code ).toMatch( /,\s*1\)/ );
|
|
40
|
+
expect( code ).toMatch( /FlowB\(2\)/ );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
it( 'rewrites evaluator calls to the global activity dispatcher', () => {
|
|
44
|
+
const src = `
|
|
45
|
+
const obj = {
|
|
46
|
+
fn: async x => {
|
|
47
|
+
EvalA(3);
|
|
48
|
+
}
|
|
49
|
+
};`;
|
|
50
|
+
const ast = parse( src, 'file.js' );
|
|
51
|
+
const activityImports = [ { localName: 'EvalA', activityName: 'eval.a' } ];
|
|
52
|
+
const rewrote = rewriteActivityCalls( { ast, activityImports } );
|
|
53
|
+
expect( rewrote ).toBe( true );
|
|
54
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
55
|
+
expect( code ).toMatch( invokeActivityPattern( 'eval\\.a' ) );
|
|
56
|
+
expect( code ).toMatch( /,\s*3\)/ );
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
it( 'does nothing when no matching calls are present', () => {
|
|
60
|
+
const src = [ 'const obj = { fn: function() { other(); } }' ].join( '\n' );
|
|
61
|
+
const ast = parse( src, 'file.js' );
|
|
62
|
+
const rewrote = rewriteActivityCalls( { ast, activityImports: [] } );
|
|
63
|
+
expect( rewrote ).toBe( false );
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'rewrites imported activity calls in any function body', () => {
|
|
67
|
+
const src = `
|
|
68
|
+
const foo = async () => {
|
|
69
|
+
StepA( 1 );
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function bar( x ) {
|
|
73
|
+
EvalA( x );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const obj = {
|
|
77
|
+
fn: async x => {
|
|
78
|
+
foo();
|
|
79
|
+
bar( 2 );
|
|
80
|
+
}
|
|
81
|
+
}`;
|
|
82
|
+
|
|
83
|
+
const ast = parse( src, 'file.js' );
|
|
84
|
+
const activityImports = [
|
|
85
|
+
{ localName: 'StepA', activityName: 'step.a' },
|
|
86
|
+
{ localName: 'EvalA', activityName: 'eval.a' }
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const rewrote = rewriteActivityCalls( { ast, activityImports } );
|
|
90
|
+
expect( rewrote ).toBe( true );
|
|
91
|
+
|
|
92
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
93
|
+
|
|
94
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
|
|
95
|
+
expect( code ).toMatch( invokeActivityPattern( 'eval\\.a' ) );
|
|
96
|
+
expect( code ).toMatch( /,\s*1\)/ );
|
|
97
|
+
expect( code ).toMatch( /,\s*x\)/ );
|
|
98
|
+
expect( code ).toMatch( /foo\(\)/ );
|
|
99
|
+
expect( code ).toMatch( /bar\(2\)/ );
|
|
100
|
+
expect( code ).toMatch( /const foo = async \(\) =>/ );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
it( 'does not rewrite top-level calls', () => {
|
|
104
|
+
const src = `
|
|
105
|
+
StepA( 1 );
|
|
106
|
+
function helper() {
|
|
107
|
+
StepA( 2 );
|
|
108
|
+
}`;
|
|
109
|
+
|
|
110
|
+
const ast = parse( src, 'file.js' );
|
|
111
|
+
const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
|
|
112
|
+
const rewrote = rewriteActivityCalls( { ast, activityImports } );
|
|
113
|
+
expect( rewrote ).toBe( true );
|
|
114
|
+
|
|
115
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
116
|
+
expect( code ).toMatch( /StepA\(1\)/ );
|
|
117
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
|
|
118
|
+
expect( code ).toMatch( /,\s*2\)/ );
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
it( 'ignores local invoke activity names because globalThis is explicit', () => {
|
|
122
|
+
const src = `
|
|
123
|
+
const __invokeActivity = () => 'local';
|
|
124
|
+
const _invokeActivity = () => 'local';
|
|
125
|
+
function helper() {
|
|
126
|
+
StepA();
|
|
127
|
+
}`;
|
|
128
|
+
const ast = parse( src, 'file.js' );
|
|
129
|
+
const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
|
|
130
|
+
const rewrote = rewriteActivityCalls( { ast, activityImports } );
|
|
131
|
+
expect( rewrote ).toBe( true );
|
|
132
|
+
|
|
133
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
134
|
+
expect( code ).not.toMatch( /@outputai\/core\/invoker/ );
|
|
135
|
+
expect( code ).toMatch( /const __invokeActivity = \(\) => 'local'/ );
|
|
136
|
+
expect( code ).toMatch( /const _invokeActivity = \(\) => 'local'/ );
|
|
137
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
it( 'reads Symbol through globalThis so local Symbol bindings are safe', () => {
|
|
141
|
+
const src = `
|
|
142
|
+
function helper( Symbol ) {
|
|
143
|
+
StepA();
|
|
144
|
+
}`;
|
|
145
|
+
const ast = parse( src, 'file.js' );
|
|
146
|
+
const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
|
|
147
|
+
const rewrote = rewriteActivityCalls( { ast, activityImports } );
|
|
148
|
+
expect( rewrote ).toBe( true );
|
|
149
|
+
|
|
150
|
+
const { code } = generate( ast, { quotes: 'single' } );
|
|
151
|
+
expect( code ).toMatch( /function helper\(Symbol\)/ );
|
|
152
|
+
expect( code ).toMatch( invokeActivityPattern( 'step\\.a' ) );
|
|
153
|
+
} );
|
|
154
|
+
|
|
155
|
+
it( 'throws when globalThis is shadowed at an activity call site', () => {
|
|
156
|
+
const src = `
|
|
157
|
+
function helper( globalThis ) {
|
|
158
|
+
StepA();
|
|
159
|
+
}`;
|
|
160
|
+
const ast = parse( src, 'file.js' );
|
|
161
|
+
const activityImports = [ { localName: 'StepA', activityName: 'step.a' } ];
|
|
162
|
+
|
|
163
|
+
expect( () => rewriteActivityCalls( { ast, activityImports } ) ).toThrow( /globalThis.*shadowed/ );
|
|
164
|
+
} );
|
|
165
|
+
} );
|