@outputai/core 0.3.3-next.32f4d87.0 → 0.3.3-next.7ccc4fe.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 +7 -2
- package/src/consts.js +4 -0
- package/src/interface/workflow.d.ts +2 -2
- package/src/worker/bundler_options.js +33 -4
- package/src/worker/bundler_options.spec.js +62 -0
- package/src/worker/interceptors/workflow.js +8 -3
- package/src/worker/interceptors/workflow.spec.js +22 -2
- package/src/worker/loader.js +62 -50
- package/src/worker/loader.spec.js +285 -82
- package/src/worker/loader_tools.js +232 -60
- package/src/worker/loader_tools.spec.js +496 -25
- package/src/worker/webpack_loaders/npm_workflow_export_resolve.js +474 -0
- package/src/worker/webpack_loaders/npm_workflow_export_resolve.spec.js +374 -0
- package/src/worker/webpack_loaders/tools.js +9 -0
- package/src/worker/webpack_loaders/tools.spec.js +7 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +80 -11
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +67 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +2 -1
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +73 -1
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +66 -1
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +62 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import loader from './index.mjs';
|
|
6
6
|
|
|
7
7
|
function runLoader( source, resourcePath ) {
|
|
@@ -196,6 +196,78 @@ const obj = {
|
|
|
196
196
|
rmSync( dir, { recursive: true, force: true } );
|
|
197
197
|
} );
|
|
198
198
|
|
|
199
|
+
it( 'rewrites ESM imports from @growthxlabs/workflows_catalog to startWorkflow', async () => {
|
|
200
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-catalog-' ) );
|
|
201
|
+
const pkgRoot = join( dir, 'node_modules', '@growthxlabs', 'workflows_catalog' );
|
|
202
|
+
const srcDir = join( pkgRoot, 'src' );
|
|
203
|
+
mkdirSync( join( srcDir, 'workflows', 'wf' ), { recursive: true } );
|
|
204
|
+
writeFileSync( join( pkgRoot, 'package.json' ), JSON.stringify( {
|
|
205
|
+
name: '@growthxlabs/workflows_catalog',
|
|
206
|
+
type: 'module',
|
|
207
|
+
main: './src/index.js',
|
|
208
|
+
dependencies: { '@outputai/core': '1.0.0' }
|
|
209
|
+
} ) );
|
|
210
|
+
writeFileSync( join( srcDir, 'index.js' ), 'export { default as sumNumbers } from \'./workflows/wf/workflow.js\';\n' );
|
|
211
|
+
writeFileSync( join( srcDir, 'workflows', 'wf', 'workflow.js' ), 'export default workflow({ name: \'nest.cat\' });\n' );
|
|
212
|
+
|
|
213
|
+
const resourcePath = join( dir, 'workflows', 'mine', 'workflow.js' );
|
|
214
|
+
mkdirSync( dirname( resourcePath ), { recursive: true } );
|
|
215
|
+
|
|
216
|
+
const source = `
|
|
217
|
+
import { sumNumbers } from '@growthxlabs/workflows_catalog';
|
|
218
|
+
|
|
219
|
+
const obj = {
|
|
220
|
+
fn: async () => {
|
|
221
|
+
sumNumbers( 1 );
|
|
222
|
+
}
|
|
223
|
+
}`;
|
|
224
|
+
|
|
225
|
+
const { code } = await runLoader( source, resourcePath );
|
|
226
|
+
|
|
227
|
+
expect( code ).not.toMatch( /@growthxlabs\/workflows_catalog/ );
|
|
228
|
+
expect( code ).toMatch( /this\.startWorkflow\('nest\.cat',\s*1\)/ );
|
|
229
|
+
|
|
230
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
it( 'rewrites imports through the output workflow bundle export condition', async () => {
|
|
234
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-catalog-condition-' ) );
|
|
235
|
+
const pkgRoot = join( dir, 'node_modules', '@test', 'conditional_catalog' );
|
|
236
|
+
mkdirSync( join( pkgRoot, 'bundle' ), { recursive: true } );
|
|
237
|
+
writeFileSync( join( pkgRoot, 'package.json' ), JSON.stringify( {
|
|
238
|
+
name: '@test/conditional_catalog',
|
|
239
|
+
type: 'module',
|
|
240
|
+
main: './node-entry.js',
|
|
241
|
+
exports: {
|
|
242
|
+
'.': {
|
|
243
|
+
'output-workflow-bundle': './bundle/workflow.js',
|
|
244
|
+
default: './node-entry.js'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} ) );
|
|
248
|
+
writeFileSync( join( pkgRoot, 'node-entry.js' ), 'export const helper = () => 1;\n' );
|
|
249
|
+
writeFileSync( join( pkgRoot, 'bundle', 'workflow.js' ), 'export default workflow({ name: \'bundle.cat\' });\n' );
|
|
250
|
+
|
|
251
|
+
const resourcePath = join( dir, 'workflows', 'mine', 'workflow.js' );
|
|
252
|
+
mkdirSync( dirname( resourcePath ), { recursive: true } );
|
|
253
|
+
|
|
254
|
+
const source = `
|
|
255
|
+
import BundleCatalog from '@test/conditional_catalog';
|
|
256
|
+
|
|
257
|
+
const obj = {
|
|
258
|
+
fn: async () => {
|
|
259
|
+
BundleCatalog( 1 );
|
|
260
|
+
}
|
|
261
|
+
}`;
|
|
262
|
+
|
|
263
|
+
const { code } = await runLoader( source, resourcePath );
|
|
264
|
+
|
|
265
|
+
expect( code ).not.toMatch( /@test\/conditional_catalog/ );
|
|
266
|
+
expect( code ).toMatch( /this\.startWorkflow\('bundle\.cat',\s*1\)/ );
|
|
267
|
+
|
|
268
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
269
|
+
} );
|
|
270
|
+
|
|
199
271
|
it( 'throws on non-static name', async () => {
|
|
200
272
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-error-' ) );
|
|
201
273
|
writeFileSync( join( dir, 'steps.js' ), `
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import traverseModule from '@babel/traverse';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parse,
|
|
5
|
+
toAbsolutePath,
|
|
6
|
+
getFileKind,
|
|
7
|
+
isAnyStepsPath,
|
|
8
|
+
isAnyEvaluatorsPath,
|
|
9
|
+
isWorkflowPath,
|
|
10
|
+
isAbsoluteWorkflowJsResource
|
|
11
|
+
} from '../tools.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Files where a bare npm import may bind to child workflows (catalog packages, etc.).
|
|
15
|
+
* Matches {@link collectTargetImports} for `workflow.js`; steps/evaluators need the same
|
|
16
|
+
* binding knowledge for fn-body validation only.
|
|
17
|
+
* @param {string} filename - Absolute resource path.
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
const fileMayBindBareNpmWorkflowImports = filename => {
|
|
21
|
+
const n = filename.replace( /\\/g, '/' );
|
|
22
|
+
return isAbsoluteWorkflowJsResource( filename ) ||
|
|
23
|
+
isAnyStepsPath( n ) || isAnyEvaluatorsPath( n );
|
|
24
|
+
};
|
|
25
|
+
import {
|
|
26
|
+
isBareNpmSpecifier,
|
|
27
|
+
resolveBareImportSpecifiersAsWorkflows,
|
|
28
|
+
resolveBareDestructuredRequireAsWorkflows,
|
|
29
|
+
resolveBareDefaultRequireAsWorkflow
|
|
30
|
+
} from '../npm_workflow_export_resolve.js';
|
|
4
31
|
import { ComponentFile } from '../consts.js';
|
|
5
32
|
import {
|
|
6
33
|
isCallExpression,
|
|
@@ -95,6 +122,21 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
95
122
|
ImportDeclaration: path => {
|
|
96
123
|
const specifier = path.node.source.value;
|
|
97
124
|
|
|
125
|
+
if ( isBareNpmSpecifier( specifier ) && fileMayBindBareNpmWorkflowImports( filename ) ) {
|
|
126
|
+
const outcome = resolveBareImportSpecifiersAsWorkflows( {
|
|
127
|
+
fromAbsoluteFile: filename,
|
|
128
|
+
specifier,
|
|
129
|
+
specifiers: path.node.specifiers,
|
|
130
|
+
workflowNameCache: new Map()
|
|
131
|
+
} );
|
|
132
|
+
if ( outcome.type === 'all' ) {
|
|
133
|
+
for ( const { localName } of outcome.bindings ) {
|
|
134
|
+
importedWorkflowIds.add( localName );
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
98
140
|
// Collect imported identifiers for later call checks
|
|
99
141
|
const importedKind = getFileKind( specifier );
|
|
100
142
|
const accumulator = ( {
|
|
@@ -141,6 +183,29 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
141
183
|
}
|
|
142
184
|
const req = firstArg.value;
|
|
143
185
|
|
|
186
|
+
if ( isBareNpmSpecifier( req ) && fileMayBindBareNpmWorkflowImports( filename ) ) {
|
|
187
|
+
if ( isObjectPattern( path.node.id ) ) {
|
|
188
|
+
const outcome = resolveBareDestructuredRequireAsWorkflows( {
|
|
189
|
+
fromAbsoluteFile: filename,
|
|
190
|
+
specifier: req,
|
|
191
|
+
properties: path.node.id.properties,
|
|
192
|
+
workflowNameCache: new Map()
|
|
193
|
+
} );
|
|
194
|
+
if ( outcome.type === 'all' ) {
|
|
195
|
+
for ( const { localName } of outcome.bindings ) {
|
|
196
|
+
importedWorkflowIds.add( localName );
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else if ( isIdentifier( path.node.id ) ) {
|
|
200
|
+
const outcome = resolveBareDefaultRequireAsWorkflow(
|
|
201
|
+
filename, req, path.node.id.name, new Map()
|
|
202
|
+
);
|
|
203
|
+
if ( outcome.type === 'binding' ) {
|
|
204
|
+
importedWorkflowIds.add( outcome.localName );
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
144
209
|
// Collect imported identifiers from require patterns
|
|
145
210
|
const reqType = getFileKind( toAbsolutePath( fileDir, req ) );
|
|
146
211
|
if ( reqType === ComponentFile.STEPS && isObjectPattern( path.node.id ) ) {
|
|
@@ -4,6 +4,28 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import validatorLoader from './index.mjs';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Minimal published catalog under `dir/node_modules` so `require.resolve` / export following works.
|
|
9
|
+
* @param {string} dir - Temp project root containing `steps.js` / `workflow.js` under test.
|
|
10
|
+
* @param {string} workflowName - Declared workflow `name` in the leaf `workflow.js`.
|
|
11
|
+
*/
|
|
12
|
+
const writeMockGrowthxlabsCatalog = ( dir, workflowName ) => {
|
|
13
|
+
const pkgRoot = join( dir, 'node_modules', '@growthxlabs', 'workflows_catalog' );
|
|
14
|
+
const srcDir = join( pkgRoot, 'src' );
|
|
15
|
+
mkdirSync( join( srcDir, 'workflows', 'wf' ), { recursive: true } );
|
|
16
|
+
writeFileSync( join( pkgRoot, 'package.json' ), JSON.stringify( {
|
|
17
|
+
name: '@growthxlabs/workflows_catalog',
|
|
18
|
+
type: 'module',
|
|
19
|
+
main: './src/index.js',
|
|
20
|
+
dependencies: { '@outputai/core': '1.0.0' }
|
|
21
|
+
} ) );
|
|
22
|
+
writeFileSync( join( srcDir, 'index.js' ), 'export { default as sumNumbers } from \'./workflows/wf/workflow.js\';\n' );
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join( srcDir, 'workflows', 'wf', 'workflow.js' ),
|
|
25
|
+
`export default workflow({ name: '${workflowName}' });\n`
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
7
29
|
function runLoader( filename, source ) {
|
|
8
30
|
return new Promise( ( resolve, reject ) => {
|
|
9
31
|
const warnings = [];
|
|
@@ -76,6 +98,46 @@ describe( 'workflow_validator loader', () => {
|
|
|
76
98
|
rmSync( dir, { recursive: true, force: true } );
|
|
77
99
|
} );
|
|
78
100
|
|
|
101
|
+
it( 'steps.js: warns when calling imported catalog workflow inside fn', async () => {
|
|
102
|
+
const dir = mkdtempSync( join( tmpdir(), 'steps-catalog-warn-' ) );
|
|
103
|
+
writeMockGrowthxlabsCatalog( dir, 'cat.warn' );
|
|
104
|
+
const src = [
|
|
105
|
+
'import { sumNumbers } from "@growthxlabs/workflows_catalog";',
|
|
106
|
+
'const A = step({ name: "a", fn: async () => ({}) });',
|
|
107
|
+
'const obj = { fn: function() { sumNumbers(); } };'
|
|
108
|
+
].join( '\n' );
|
|
109
|
+
const result = await runLoader( join( dir, 'steps.js' ), src );
|
|
110
|
+
expect( result.warnings ).toHaveLength( 1 );
|
|
111
|
+
expect( result.warnings[0].message ).toMatch( /Invalid call in .*steps\.js fn: calling a workflow/ );
|
|
112
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
it( 'evaluators.js: warns when calling catalog workflow inside fn (require)', async () => {
|
|
116
|
+
const dir = mkdtempSync( join( tmpdir(), 'evals-catalog-warn-' ) );
|
|
117
|
+
writeMockGrowthxlabsCatalog( dir, 'cat.warn.req' );
|
|
118
|
+
const src = [
|
|
119
|
+
'const { sumNumbers } = require("@growthxlabs/workflows_catalog");',
|
|
120
|
+
'const E = evaluator({ name: "e", fn: async () => ({ value: 1 }) });',
|
|
121
|
+
'const obj = { fn: function() { sumNumbers(); } };'
|
|
122
|
+
].join( '\n' );
|
|
123
|
+
const result = await runLoader( join( dir, 'evaluators.js' ), src );
|
|
124
|
+
expect( result.warnings ).toHaveLength( 1 );
|
|
125
|
+
expect( result.warnings[0].message ).toMatch( /Invalid call in .*evaluators\.js fn: calling a workflow/ );
|
|
126
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
it( 'workflow.js: allows import from @growthxlabs/workflows_catalog', async () => {
|
|
130
|
+
const dir = mkdtempSync( join( tmpdir(), 'wf-catalog-allow-' ) );
|
|
131
|
+
writeMockGrowthxlabsCatalog( dir, 'cat.allow' );
|
|
132
|
+
const src = [
|
|
133
|
+
'import { sumNumbers } from "@growthxlabs/workflows_catalog";',
|
|
134
|
+
'const x = 1;'
|
|
135
|
+
].join( '\n' );
|
|
136
|
+
const result = await runLoader( join( dir, 'workflow.js' ), src );
|
|
137
|
+
expect( result.warnings ).toHaveLength( 0 );
|
|
138
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
139
|
+
} );
|
|
140
|
+
|
|
79
141
|
it( 'steps.js: warns when calling another step inside fn', async () => {
|
|
80
142
|
const dir = mkdtempSync( join( tmpdir(), 'steps-call-reject-' ) );
|
|
81
143
|
// Can only test same-type components since cross-type declarations are now blocked by instantiation validation
|