@outputai/core 0.3.3-next.b4a190e.0 → 0.3.3-next.d63dd0d.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.
@@ -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 { parse, toAbsolutePath, getFileKind, isAnyStepsPath, isAnyEvaluatorsPath, isWorkflowPath } from '../tools.js';
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