@output.ai/core 0.0.7 → 0.0.8
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 +7 -3
- package/src/configs.js +1 -1
- package/src/consts.js +3 -3
- package/src/index.d.ts +302 -30
- package/src/index.js +3 -2
- package/src/interface/metadata.js +3 -3
- package/src/interface/step.js +3 -3
- package/src/interface/webhook.js +13 -14
- package/src/interface/workflow.js +22 -42
- package/src/internal_activities/index.js +16 -5
- package/src/worker/catalog_workflow/catalog.js +105 -0
- package/src/worker/catalog_workflow/index.js +21 -0
- package/src/worker/catalog_workflow/index.spec.js +139 -0
- package/src/worker/catalog_workflow/workflow.js +13 -0
- package/src/worker/index.js +37 -5
- package/src/worker/internal_utils.js +54 -0
- package/src/worker/internal_utils.spec.js +134 -0
- package/src/worker/loader.js +30 -44
- package/src/worker/loader.spec.js +68 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +117 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +77 -0
- package/src/worker/webpack_loaders/workflow_rewriter/consts.js +3 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +56 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +129 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +64 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +33 -0
- package/src/worker/webpack_loaders/workflow_rewriter/tools.js +225 -0
- package/src/worker/webpack_loaders/workflow_rewriter/tools.spec.js +144 -0
- package/src/errors.d.ts +0 -3
- package/src/worker/temp/__workflows_entrypoint.js +0 -6
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import traverseModule from '@babel/traverse';
|
|
2
|
+
import {
|
|
3
|
+
buildWorkflowNameMap,
|
|
4
|
+
getLocalNameFromDestructuredProperty,
|
|
5
|
+
isStepsPath,
|
|
6
|
+
isWorkflowPath,
|
|
7
|
+
buildStepsNameMap,
|
|
8
|
+
toAbsolutePath
|
|
9
|
+
} from './tools.js';
|
|
10
|
+
import {
|
|
11
|
+
isCallExpression,
|
|
12
|
+
isIdentifier,
|
|
13
|
+
isImportDefaultSpecifier,
|
|
14
|
+
isImportSpecifier,
|
|
15
|
+
isObjectPattern,
|
|
16
|
+
isObjectProperty,
|
|
17
|
+
isStringLiteral,
|
|
18
|
+
isVariableDeclaration
|
|
19
|
+
} from '@babel/types';
|
|
20
|
+
|
|
21
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
22
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Collect and strip target imports and requires from an AST, producing
|
|
26
|
+
* step/workflow import mappings for later rewrites.
|
|
27
|
+
*
|
|
28
|
+
* Mutates the AST by removing matching import declarations and require declarators.
|
|
29
|
+
*
|
|
30
|
+
* @param {import('@babel/types').File} ast - Parsed file AST.
|
|
31
|
+
* @param {string} fileDir - Absolute directory of the file represented by `ast`.
|
|
32
|
+
* @param {{ stepsNameCache: Map<string,Map<string,string>>, workflowNameCache: Map<string,{default:(string|null),named:Map<string,string>}> }} caches
|
|
33
|
+
* Resolved-name caches to avoid re-reading same modules.
|
|
34
|
+
* @returns {{ stepImports: Array<{localName:string,stepName:string}>,
|
|
35
|
+
* flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
|
|
36
|
+
*/
|
|
37
|
+
export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache } ) {
|
|
38
|
+
const stepImports = [];
|
|
39
|
+
const flowImports = [];
|
|
40
|
+
|
|
41
|
+
traverse( ast, {
|
|
42
|
+
ImportDeclaration: path => {
|
|
43
|
+
const src = path.node.source.value;
|
|
44
|
+
// Ignore other imports
|
|
45
|
+
if ( !isStepsPath( src ) && !isWorkflowPath( src ) ) { return; }
|
|
46
|
+
|
|
47
|
+
const absolutePath = toAbsolutePath( fileDir, src );
|
|
48
|
+
if ( isStepsPath( src ) ) {
|
|
49
|
+
const nameMap = buildStepsNameMap( absolutePath, stepsNameCache );
|
|
50
|
+
for ( const s of path.node.specifiers.filter( s => isImportSpecifier( s ) ) ) {
|
|
51
|
+
const importedName = s.imported.name;
|
|
52
|
+
const localName = s.local.name;
|
|
53
|
+
const stepName = nameMap.get( importedName );
|
|
54
|
+
if ( stepName ) { stepImports.push( { localName, stepName } ); }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if ( isWorkflowPath( src ) ) {
|
|
58
|
+
const { named, default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
59
|
+
for ( const s of path.node.specifiers ) {
|
|
60
|
+
if ( isImportDefaultSpecifier( s ) ) {
|
|
61
|
+
const localName = s.local.name;
|
|
62
|
+
flowImports.push( { localName, workflowName: defName ?? localName } );
|
|
63
|
+
} else if ( isImportSpecifier( s ) ) {
|
|
64
|
+
const importedName = s.imported.name;
|
|
65
|
+
const localName = s.local.name;
|
|
66
|
+
const workflowName = named.get( importedName );
|
|
67
|
+
if ( workflowName ) { flowImports.push( { localName, workflowName } ); }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
path.remove();
|
|
72
|
+
},
|
|
73
|
+
VariableDeclarator: path => {
|
|
74
|
+
const init = path.node.init;
|
|
75
|
+
// Not a require call
|
|
76
|
+
if ( !isCallExpression( init ) ) { return; }
|
|
77
|
+
// Different callee
|
|
78
|
+
if ( !isIdentifier( init.callee, { name: 'require' } ) ) { return; }
|
|
79
|
+
const firstArgument = init.arguments[0];
|
|
80
|
+
// Dynamic require is not supported
|
|
81
|
+
if ( !isStringLiteral( firstArgument ) ) { return; }
|
|
82
|
+
|
|
83
|
+
const req = firstArgument.value;
|
|
84
|
+
// Must be steps/workflows module
|
|
85
|
+
if ( !isStepsPath( req ) && !isWorkflowPath( req ) ) { return; }
|
|
86
|
+
|
|
87
|
+
const absolutePath = toAbsolutePath( fileDir, req );
|
|
88
|
+
if ( isStepsPath( req ) && isObjectPattern( path.node.id ) ) {
|
|
89
|
+
const nameMap = buildStepsNameMap( absolutePath, stepsNameCache );
|
|
90
|
+
for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
|
|
91
|
+
const importedName = prop.key.name;
|
|
92
|
+
const localName = getLocalNameFromDestructuredProperty( prop );
|
|
93
|
+
if ( localName ) {
|
|
94
|
+
const stepName = nameMap.get( importedName );
|
|
95
|
+
if ( stepName ) { stepImports.push( { localName, stepName } ); }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
|
|
99
|
+
path.parentPath.remove();
|
|
100
|
+
} else {
|
|
101
|
+
path.remove();
|
|
102
|
+
}
|
|
103
|
+
} else if ( isWorkflowPath( req ) && isIdentifier( path.node.id ) ) {
|
|
104
|
+
const { default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
105
|
+
const localName = path.node.id.name;
|
|
106
|
+
flowImports.push( { localName, workflowName: defName ?? localName } );
|
|
107
|
+
if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
|
|
108
|
+
path.parentPath.remove();
|
|
109
|
+
} else {
|
|
110
|
+
path.remove();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
return { stepImports, flowImports };
|
|
117
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { parse } from './tools.js';
|
|
6
|
+
import collectTargetImports from './collect_target_imports.js';
|
|
7
|
+
|
|
8
|
+
function makeAst( source, filename ) {
|
|
9
|
+
return parse( source, filename );
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe( 'collect_target_imports', () => {
|
|
13
|
+
it( 'collects ESM imports for steps and workflows and flags changes', () => {
|
|
14
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-esm-' ) );
|
|
15
|
+
writeFileSync( join( dir, 'steps.js' ), [
|
|
16
|
+
'export const StepA = step({ name: "step.a" })',
|
|
17
|
+
'export const StepB = step({ name: "step.b" })'
|
|
18
|
+
].join( '\n' ) );
|
|
19
|
+
writeFileSync( join( dir, 'workflow.js' ), [
|
|
20
|
+
'export const FlowA = workflow({ name: "flow.a" })',
|
|
21
|
+
'export default workflow({ name: "flow.def" })'
|
|
22
|
+
].join( '\n' ) );
|
|
23
|
+
|
|
24
|
+
const source = [
|
|
25
|
+
'import { StepA } from "./steps.js";',
|
|
26
|
+
'import WF, { FlowA } from "./workflow.js";',
|
|
27
|
+
'const x = 1;'
|
|
28
|
+
].join( '\n' );
|
|
29
|
+
|
|
30
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
31
|
+
const { stepImports, flowImports } = collectTargetImports(
|
|
32
|
+
ast,
|
|
33
|
+
dir,
|
|
34
|
+
{ stepsNameCache: new Map(), workflowNameCache: new Map() }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect( stepImports ).toEqual( [ { localName: 'StepA', stepName: 'step.a' } ] );
|
|
38
|
+
expect( flowImports ).toEqual( [
|
|
39
|
+
{ localName: 'WF', workflowName: 'flow.def' },
|
|
40
|
+
{ localName: 'FlowA', workflowName: 'flow.a' }
|
|
41
|
+
] );
|
|
42
|
+
// Import declarations should have been removed
|
|
43
|
+
expect( ast.program.body.find( n => n.type === 'ImportDeclaration' ) ).toBeUndefined();
|
|
44
|
+
|
|
45
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'collects CJS requires and removes declarators (steps + default workflow)', () => {
|
|
49
|
+
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-' ) );
|
|
50
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: "step.b" })\n' );
|
|
51
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: "flow.c" })\n' );
|
|
52
|
+
|
|
53
|
+
const source = [
|
|
54
|
+
'const { StepB } = require("./steps.js");',
|
|
55
|
+
'const WF = require("./workflow.js");',
|
|
56
|
+
'const obj = {};'
|
|
57
|
+
].join( '\n' );
|
|
58
|
+
|
|
59
|
+
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
60
|
+
const { stepImports, flowImports } = collectTargetImports(
|
|
61
|
+
ast,
|
|
62
|
+
dir,
|
|
63
|
+
{ stepsNameCache: new Map(), workflowNameCache: new Map() }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect( stepImports ).toEqual( [ { localName: 'StepB', stepName: 'step.b' } ] );
|
|
67
|
+
expect( flowImports ).toEqual( [ { localName: 'WF', workflowName: 'flow.c' } ] );
|
|
68
|
+
// All require-based declarators should have been removed (only non-require decls may remain)
|
|
69
|
+
const hasRequireDecl = ast.program.body.some( n =>
|
|
70
|
+
n.type === 'VariableDeclaration' && n.declarations.some( d => d.init && d.init.type === 'CallExpression' )
|
|
71
|
+
);
|
|
72
|
+
expect( hasRequireDecl ).toBe( false );
|
|
73
|
+
|
|
74
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
75
|
+
} );
|
|
76
|
+
} );
|
|
77
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import generatorModule from '@babel/generator';
|
|
3
|
+
import { parse } from './tools.js';
|
|
4
|
+
|
|
5
|
+
import rewriteFnBodies from './rewrite_fn_bodies.js';
|
|
6
|
+
import collectTargetImports from './collect_target_imports.js';
|
|
7
|
+
|
|
8
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
9
|
+
const generate = generatorModule.default ?? generatorModule;
|
|
10
|
+
|
|
11
|
+
// Caches to avoid re-reading files during a build
|
|
12
|
+
const stepsNameCache = new Map(); // path -> Map<exported, stepName>
|
|
13
|
+
const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exported, flowName> }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Webpack loader that rewrites step/workflow calls by reading names from
|
|
17
|
+
* the respective modules and transforming `fn` bodies accordingly.
|
|
18
|
+
* Preserves sourcemaps.
|
|
19
|
+
*
|
|
20
|
+
* @param {string|Buffer} source - Module source code.
|
|
21
|
+
* @param {any} inputMap - Incoming source map.
|
|
22
|
+
* @this {import('webpack').LoaderContext<{}>}
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
export default function stepImportRewriterAstLoader( source, inputMap ) {
|
|
26
|
+
this.cacheable?.( true );
|
|
27
|
+
const callback = this.async?.() ?? this.callback;
|
|
28
|
+
const cache = { stepsNameCache, workflowNameCache };
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const filename = this.resourcePath;
|
|
32
|
+
const ast = parse( String( source ), filename );
|
|
33
|
+
const fileDir = dirname( filename );
|
|
34
|
+
const { stepImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
35
|
+
|
|
36
|
+
// No imports
|
|
37
|
+
if ( stepImports.length + flowImports.length === 0 ) {
|
|
38
|
+
return callback( null, source, inputMap );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rewrote = rewriteFnBodies( ast, stepImports, flowImports );
|
|
42
|
+
// No edits performed
|
|
43
|
+
if ( !rewrote ) { return callback( null, source, inputMap ); }
|
|
44
|
+
|
|
45
|
+
const { code, map } = generate( ast, {
|
|
46
|
+
sourceMaps: true,
|
|
47
|
+
sourceFileName: filename,
|
|
48
|
+
quotes: 'single',
|
|
49
|
+
jsescOption: { quotes: 'single' }
|
|
50
|
+
}, String( source ) );
|
|
51
|
+
return callback( null, code, map ?? inputMap );
|
|
52
|
+
} catch ( err ) {
|
|
53
|
+
// Fail gracefully as loader error
|
|
54
|
+
return callback( err );
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import loader from './index.mjs';
|
|
6
|
+
|
|
7
|
+
function runLoader( source, resourcePath ) {
|
|
8
|
+
return new Promise( ( resolve, reject ) => {
|
|
9
|
+
const ctx = {
|
|
10
|
+
resourcePath,
|
|
11
|
+
cacheable: () => {},
|
|
12
|
+
async: () => ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map } ) ),
|
|
13
|
+
callback: ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map } ) )
|
|
14
|
+
};
|
|
15
|
+
loader.call( ctx, source, null );
|
|
16
|
+
} );
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe( 'workflows_rewriter Webpack loader spec', () => {
|
|
20
|
+
it( 'rewrites ESM imports and converts fn arrow to function', async () => {
|
|
21
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-' ) );
|
|
22
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const StepA = step({ name: \'step.a\' })\n' );
|
|
23
|
+
writeFileSync( join( dir, 'workflow.js' ), [
|
|
24
|
+
'export const FlowA = workflow({ name: \'flow.a\' })',
|
|
25
|
+
'export default workflow({ name: \'flow.def\' })'
|
|
26
|
+
].join( '\n' ) );
|
|
27
|
+
|
|
28
|
+
const source = [
|
|
29
|
+
'import { StepA } from \'./steps.js\';',
|
|
30
|
+
'import FlowDef, { FlowA } from \'./workflow.js\';',
|
|
31
|
+
'',
|
|
32
|
+
'const obj = {',
|
|
33
|
+
' fn: async (x) => {',
|
|
34
|
+
' StepA(1);',
|
|
35
|
+
' FlowA(2);',
|
|
36
|
+
' FlowDef(3);',
|
|
37
|
+
' }',
|
|
38
|
+
'}',
|
|
39
|
+
''
|
|
40
|
+
].join( '\n' );
|
|
41
|
+
|
|
42
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
43
|
+
|
|
44
|
+
expect( code ).not.toMatch( /from '\.\/steps\.js'/ );
|
|
45
|
+
expect( code ).not.toMatch( /from '\.\/workflow\.js'/ );
|
|
46
|
+
expect( code ).toMatch( /fn:\s*async function \(x\)/ );
|
|
47
|
+
expect( code ).toMatch( /this\.invokeStep\('step\.a',\s*1\)/ );
|
|
48
|
+
expect( code ).toMatch( /this\.startWorkflow\('flow\.a',\s*2\)/ );
|
|
49
|
+
expect( code ).toMatch( /this\.startWorkflow\('flow\.def',\s*3\)/ );
|
|
50
|
+
|
|
51
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'rewrites CJS requires and converts fn arrow to function', async () => {
|
|
55
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-' ) );
|
|
56
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: \'step.b\' })\n' );
|
|
57
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: \'flow.c\' })\n' );
|
|
58
|
+
|
|
59
|
+
const source = [
|
|
60
|
+
'const { StepB } = require(\'./steps.js\');',
|
|
61
|
+
'const FlowDefault = require(\'./workflow.js\');',
|
|
62
|
+
'',
|
|
63
|
+
'const obj = {',
|
|
64
|
+
' fn: async (y) => {',
|
|
65
|
+
' StepB();',
|
|
66
|
+
' FlowDefault();',
|
|
67
|
+
' }',
|
|
68
|
+
'}',
|
|
69
|
+
''
|
|
70
|
+
].join( '\n' );
|
|
71
|
+
|
|
72
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
73
|
+
|
|
74
|
+
expect( code ).not.toMatch( /require\('\.\/steps\.js'\)/ );
|
|
75
|
+
expect( code ).not.toMatch( /require\('\.\/workflow\.js'\)/ );
|
|
76
|
+
expect( code ).toMatch( /fn:\s*async function \(y\)/ );
|
|
77
|
+
expect( code ).toMatch( /this\.invokeStep\('step\.b'\)/ );
|
|
78
|
+
expect( code ).toMatch( /this\.startWorkflow\('flow\.c'\)/ );
|
|
79
|
+
|
|
80
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
it( 'resolves top-level const name variables', async () => {
|
|
84
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-const-' ) );
|
|
85
|
+
writeFileSync( join( dir, 'steps.js' ), [
|
|
86
|
+
'const NAME = \'step.const\'',
|
|
87
|
+
'export const StepC = step({ name: NAME })'
|
|
88
|
+
].join( '\n' ) );
|
|
89
|
+
writeFileSync( join( dir, 'workflow.js' ), [
|
|
90
|
+
'const WF = \'wf.const\'',
|
|
91
|
+
'export const FlowC = workflow({ name: WF })',
|
|
92
|
+
'const D = \'wf.def\'',
|
|
93
|
+
'export default workflow({ name: D })'
|
|
94
|
+
].join( '\n' ) );
|
|
95
|
+
|
|
96
|
+
const source = [
|
|
97
|
+
'import { StepC } from \'./steps.js\';',
|
|
98
|
+
'import FlowDef, { FlowC } from \'./workflow.js\';',
|
|
99
|
+
'const obj = { fn: async () => { StepC(); FlowC(); FlowDef(); } }'
|
|
100
|
+
].join( '\n' );
|
|
101
|
+
|
|
102
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
103
|
+
expect( code ).toMatch( /this\.invokeStep\('step\.const'\)/ );
|
|
104
|
+
expect( code ).toMatch( /this\.startWorkflow\('wf\.const'\)/ );
|
|
105
|
+
expect( code ).toMatch( /this\.startWorkflow\('wf\.def'\)/ );
|
|
106
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
it( 'throws on non-static name', async () => {
|
|
110
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-error-' ) );
|
|
111
|
+
writeFileSync( join( dir, 'steps.js' ), [
|
|
112
|
+
'function n() { return \'x\' }',
|
|
113
|
+
'export const StepX = step({ name: n() })'
|
|
114
|
+
].join( '\n' ) );
|
|
115
|
+
writeFileSync( join( dir, 'workflow.js' ), [
|
|
116
|
+
'const base = \'a\'',
|
|
117
|
+
'export default workflow({ name: `${base}-b` })'
|
|
118
|
+
].join( '\n' ) );
|
|
119
|
+
|
|
120
|
+
const source = [
|
|
121
|
+
'import { StepX } from \'./steps.js\';',
|
|
122
|
+
'import WF from \'./workflow.js\';',
|
|
123
|
+
'const obj = { fn: async () => { StepX(); WF(); } }'
|
|
124
|
+
].join( '\n' );
|
|
125
|
+
|
|
126
|
+
await expect( runLoader( source, join( dir, 'file.js' ) ) ).rejects.toThrow( /Invalid (step|default workflow) name/ );
|
|
127
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
128
|
+
} );
|
|
129
|
+
} );
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import traverseModule from '@babel/traverse';
|
|
2
|
+
import {
|
|
3
|
+
isArrowFunctionExpression,
|
|
4
|
+
isIdentifier,
|
|
5
|
+
isFunctionExpression
|
|
6
|
+
} from '@babel/types';
|
|
7
|
+
import { toFunctionExpression, createThisMethodCall } from './tools.js';
|
|
8
|
+
|
|
9
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
10
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Rewrite calls to imported steps/workflows within `fn` object properties.
|
|
14
|
+
* Converts arrow fns to functions and replaces `StepX(...)` with
|
|
15
|
+
* `this.invokeStep('name', ...)` and `FlowY(...)` with
|
|
16
|
+
* `this.startWorkflow('name', ...)`.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('@babel/types').File} ast - Parsed file AST.
|
|
19
|
+
* @param {Array<{localName:string,stepName:string}>} stepImports - Step imports.
|
|
20
|
+
* @param {Array<{localName:string,workflowName:string}>} flowImports - Workflow imports.
|
|
21
|
+
* @returns {boolean} True if the AST was modified; false otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export default function rewriteFnBodies( ast, stepImports, flowImports ) {
|
|
24
|
+
const state = { rewrote: false };
|
|
25
|
+
traverse( ast, {
|
|
26
|
+
ObjectProperty: path => {
|
|
27
|
+
// If it is not fn property: skip
|
|
28
|
+
if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) { return; }
|
|
29
|
+
|
|
30
|
+
const val = path.node.value;
|
|
31
|
+
|
|
32
|
+
// if it is not function, return
|
|
33
|
+
if ( !isFunctionExpression( val ) && !isArrowFunctionExpression( val ) ) { return; }
|
|
34
|
+
|
|
35
|
+
// replace arrow fn in favor of a function
|
|
36
|
+
if ( isArrowFunctionExpression( val ) ) {
|
|
37
|
+
const func = toFunctionExpression( val );
|
|
38
|
+
path.get( 'value' ).replaceWith( func );
|
|
39
|
+
state.rewrote = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
path.get( 'value.body' ).traverse( {
|
|
43
|
+
CallExpression: cPath => {
|
|
44
|
+
const callee = cPath.node.callee;
|
|
45
|
+
if ( !isIdentifier( callee ) ) { return; } // Skip: complex callee not supported
|
|
46
|
+
const step = stepImports.find( x => x.localName === callee.name );
|
|
47
|
+
if ( step ) {
|
|
48
|
+
const args = cPath.node.arguments;
|
|
49
|
+
cPath.replaceWith( createThisMethodCall( 'invokeStep', step.stepName, args ) );
|
|
50
|
+
state.rewrote = true;
|
|
51
|
+
return; // Stop after rewriting as step call
|
|
52
|
+
}
|
|
53
|
+
const flow = flowImports.find( x => x.localName === callee.name );
|
|
54
|
+
if ( flow ) {
|
|
55
|
+
const args = cPath.node.arguments;
|
|
56
|
+
cPath.replaceWith( createThisMethodCall( 'startWorkflow', flow.workflowName, args ) );
|
|
57
|
+
state.rewrote = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} );
|
|
61
|
+
}
|
|
62
|
+
} );
|
|
63
|
+
return state.rewrote;
|
|
64
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parse } from './tools.js';
|
|
3
|
+
import rewriteFnBodies from './rewrite_fn_bodies.js';
|
|
4
|
+
|
|
5
|
+
describe( 'rewrite_fn_bodies', () => {
|
|
6
|
+
it( 'converts arrow to function and rewrites step/workflow calls', () => {
|
|
7
|
+
const src = [
|
|
8
|
+
'const obj = {',
|
|
9
|
+
' fn: async (x) => {',
|
|
10
|
+
' StepA(1);',
|
|
11
|
+
' FlowB(2);',
|
|
12
|
+
' }',
|
|
13
|
+
'}'
|
|
14
|
+
].join( '\n' );
|
|
15
|
+
const ast = parse( src, 'file.js' );
|
|
16
|
+
const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
|
|
17
|
+
const flowImports = [ { localName: 'FlowB', workflowName: 'flow.b' } ];
|
|
18
|
+
|
|
19
|
+
const rewrote = rewriteFnBodies( ast, stepImports, flowImports );
|
|
20
|
+
expect( rewrote ).toBe( true );
|
|
21
|
+
|
|
22
|
+
const code = ast.program.body.map( n => n.type ).length; // smoke: ast mutated
|
|
23
|
+
expect( code ).toBeGreaterThan( 0 );
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
it( 'does nothing when no matching calls are present', () => {
|
|
27
|
+
const src = [ 'const obj = { fn: function() { other(); } }' ].join( '\n' );
|
|
28
|
+
const ast = parse( src, 'file.js' );
|
|
29
|
+
const rewrote = rewriteFnBodies( ast, [], [] );
|
|
30
|
+
expect( rewrote ).toBe( false );
|
|
31
|
+
} );
|
|
32
|
+
} );
|
|
33
|
+
|