@outputai/core 0.3.3-next.e8eff63.0 → 0.4.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.
@@ -0,0 +1,374 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { parse } from './tools.js';
6
+ import {
7
+ isBareNpmSpecifier,
8
+ resolveBareImportSpecifiersAsWorkflows,
9
+ resolveBareDestructuredRequireAsWorkflows,
10
+ resolveBareDefaultRequireAsWorkflow
11
+ } from './npm_workflow_export_resolve.js';
12
+
13
+ /**
14
+ * @param {(dir: string) => void} fn
15
+ */
16
+ const withTempProjectDir = fn => {
17
+ const dir = mkdtempSync( join( tmpdir(), 'npm-resolve-' ) );
18
+ try {
19
+ fn( dir );
20
+ } finally {
21
+ rmSync( dir, { recursive: true, force: true } );
22
+ }
23
+ };
24
+
25
+ /**
26
+ * @param {string} source - Single import declaration source line (no newline required).
27
+ * @returns {import('@babel/types').ImportDeclaration['specifiers']}
28
+ */
29
+ const importSpecifiersFromSource = source => {
30
+ const ast = parse( `${source}\n`, 'stub.js' );
31
+ const decl = ast.program.body[0];
32
+ if ( decl.type !== 'ImportDeclaration' ) {
33
+ throw new Error( 'expected ImportDeclaration' );
34
+ }
35
+ return decl.specifiers;
36
+ };
37
+
38
+ /**
39
+ * @param {string} source - `const { ... } = require('...');`
40
+ */
41
+ const destructuredRequirePropertiesFromSource = source => {
42
+ const ast = parse( `${source}\n`, 'stub.js' );
43
+ const stmt = ast.program.body[0];
44
+ if ( stmt.type !== 'VariableDeclaration' || !stmt.declarations[0] ) {
45
+ throw new Error( 'expected VariableDeclaration' );
46
+ }
47
+ const pat = stmt.declarations[0].id;
48
+ if ( pat.type !== 'ObjectPattern' ) {
49
+ throw new Error( 'expected ObjectPattern' );
50
+ }
51
+ return pat.properties;
52
+ };
53
+
54
+ /**
55
+ * Writes a minimal package under `root/node_modules/<name>` with given files (relative paths).
56
+ *
57
+ * @param {string} root - Project root containing `node_modules`.
58
+ * @param {string} name - Package name e.g. `@test/catalog`.
59
+ * @param {string} main - Relative main entry from package root.
60
+ * @param {Record<string, string>} files - Relative path -> file contents.
61
+ * @param {object} [extraPackageJson] - Extra fields to merge into package.json.
62
+ */
63
+ const writeNodeModulesPackage = ( root, name, main, files, extraPackageJson = {} ) => {
64
+ const pkgRoot = name.startsWith( '@' ) ?
65
+ join( root, 'node_modules', ...name.split( '/' ) ) :
66
+ join( root, 'node_modules', name );
67
+ mkdirSync( pkgRoot, { recursive: true } );
68
+ writeFileSync(
69
+ join( pkgRoot, 'package.json' ),
70
+ JSON.stringify( { name, version: '1.0.0', main, ...extraPackageJson }, null, 2 )
71
+ );
72
+ for ( const [ rel, content ] of Object.entries( files ) ) {
73
+ const abs = join( pkgRoot, rel );
74
+ mkdirSync( dirname( abs ), { recursive: true } );
75
+ writeFileSync( abs, content );
76
+ }
77
+ return pkgRoot;
78
+ };
79
+
80
+ describe( 'isBareNpmSpecifier', () => {
81
+ it( 'returns false for empty or non-string', () => {
82
+ expect( isBareNpmSpecifier( '' ) ).toBe( false );
83
+ expect( isBareNpmSpecifier( undefined ) ).toBe( false );
84
+ } );
85
+
86
+ it( 'returns false for relative and absolute paths', () => {
87
+ expect( isBareNpmSpecifier( './x' ) ).toBe( false );
88
+ expect( isBareNpmSpecifier( '../x' ) ).toBe( false );
89
+ expect( isBareNpmSpecifier( '/abs' ) ).toBe( false );
90
+ } );
91
+
92
+ it( 'returns false for node:, file:, data:, http(s):', () => {
93
+ expect( isBareNpmSpecifier( 'node:fs' ) ).toBe( false );
94
+ expect( isBareNpmSpecifier( 'file:///x' ) ).toBe( false );
95
+ expect( isBareNpmSpecifier( 'data:,x' ) ).toBe( false );
96
+ expect( isBareNpmSpecifier( 'http://x' ) ).toBe( false );
97
+ expect( isBareNpmSpecifier( 'https://x' ) ).toBe( false );
98
+ } );
99
+
100
+ it( 'returns true for bare package names', () => {
101
+ expect( isBareNpmSpecifier( 'lodash' ) ).toBe( true );
102
+ expect( isBareNpmSpecifier( '@scope/pkg' ) ).toBe( true );
103
+ } );
104
+ } );
105
+
106
+ describe( 'resolveBareImportSpecifiersAsWorkflows', () => {
107
+ it( 'returns none when the specifier does not resolve', () => {
108
+ withTempProjectDir( dir => {
109
+ const wf = join( dir, 'workflow.js' );
110
+ writeFileSync( wf, 'export default workflow({ name: \'local\' });\n' );
111
+
112
+ const out = resolveBareImportSpecifiersAsWorkflows( {
113
+ fromAbsoluteFile: wf,
114
+ specifier: '@missing-scope/missing-pkg-xyz',
115
+ specifiers: importSpecifiersFromSource( 'import x from \'@missing-scope/missing-pkg-xyz\'' ),
116
+ workflowNameCache: new Map()
117
+ } );
118
+ expect( out ).toEqual( { type: 'none' } );
119
+ } );
120
+ } );
121
+
122
+ it( 'throws for namespace imports from workflow packages', () => {
123
+ withTempProjectDir( dir => {
124
+ const wf = join( dir, 'workflow.js' );
125
+ writeFileSync( wf, 'export default workflow({ name: \'local\' });\n' );
126
+ writeNodeModulesPackage( dir, '@test/ns', './index.js', {
127
+ 'index.js': 'export { default as nsWorkflow } from \'./workflow.js\';\n',
128
+ 'workflow.js': 'export default workflow({ name: \'ns.wf\' });\n'
129
+ } );
130
+
131
+ expect( () => resolveBareImportSpecifiersAsWorkflows( {
132
+ fromAbsoluteFile: wf,
133
+ specifier: '@test/ns',
134
+ specifiers: importSpecifiersFromSource( 'import * as ns from \'@test/ns\'' ),
135
+ workflowNameCache: new Map()
136
+ } ) ).toThrow(
137
+ 'Namespace imports from workflow package "@test/ns" are not supported. ' +
138
+ 'Use named imports instead, e.g. import { myWorkflow } from \'@test/ns\'.'
139
+ );
140
+ } );
141
+ } );
142
+
143
+ it( 'returns none for namespace imports from non-workflow packages', () => {
144
+ withTempProjectDir( dir => {
145
+ const wf = join( dir, 'workflow.js' );
146
+ writeFileSync( wf, 'export default workflow({ name: \'local\' });\n' );
147
+ writeNodeModulesPackage( dir, '@test/helper', './index.js', {
148
+ 'index.js': 'export const helper = () => 1;\n'
149
+ } );
150
+
151
+ const out = resolveBareImportSpecifiersAsWorkflows( {
152
+ fromAbsoluteFile: wf,
153
+ specifier: '@test/helper',
154
+ specifiers: importSpecifiersFromSource( 'import * as helper from \'@test/helper\'' ),
155
+ workflowNameCache: new Map()
156
+ } );
157
+ expect( out ).toEqual( { type: 'none' } );
158
+ } );
159
+ } );
160
+
161
+ it( 'resolves default import to the package default workflow name', () => {
162
+ withTempProjectDir( dir => {
163
+ const importing = join( dir, 'workflow.js' );
164
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
165
+ writeNodeModulesPackage( dir, '@test/wfdef', './workflow.js', {
166
+ 'workflow.js': 'export default workflow({ name: \'pkg.default\' });\n'
167
+ } );
168
+
169
+ const out = resolveBareImportSpecifiersAsWorkflows( {
170
+ fromAbsoluteFile: importing,
171
+ specifier: '@test/wfdef',
172
+ specifiers: importSpecifiersFromSource( 'import PkgDef from \'@test/wfdef\'' ),
173
+ workflowNameCache: new Map()
174
+ } );
175
+ expect( out ).toEqual( {
176
+ type: 'all',
177
+ bindings: [ { localName: 'PkgDef', workflowName: 'pkg.default' } ]
178
+ } );
179
+ } );
180
+ } );
181
+
182
+ it( 'prefers the output workflow bundle export condition over the default entry', () => {
183
+ withTempProjectDir( dir => {
184
+ const importing = join( dir, 'workflow.js' );
185
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
186
+ writeNodeModulesPackage( dir, '@test/conditional', './node-entry.js', {
187
+ 'node-entry.js': 'export const helper = () => 1;\n',
188
+ 'bundle/workflow.js': 'export default workflow({ name: \'bundle.workflow\' });\n'
189
+ }, {
190
+ exports: {
191
+ '.': {
192
+ 'output-workflow-bundle': './bundle/workflow.js',
193
+ default: './node-entry.js'
194
+ }
195
+ }
196
+ } );
197
+
198
+ const out = resolveBareImportSpecifiersAsWorkflows( {
199
+ fromAbsoluteFile: importing,
200
+ specifier: '@test/conditional',
201
+ specifiers: importSpecifiersFromSource( 'import BundleWorkflow from \'@test/conditional\'' ),
202
+ workflowNameCache: new Map()
203
+ } );
204
+ expect( out ).toEqual( {
205
+ type: 'all',
206
+ bindings: [ { localName: 'BundleWorkflow', workflowName: 'bundle.workflow' } ]
207
+ } );
208
+ } );
209
+ } );
210
+
211
+ it( 'supports root conditional exports without an explicit dot key', () => {
212
+ withTempProjectDir( dir => {
213
+ const importing = join( dir, 'workflow.js' );
214
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
215
+ writeNodeModulesPackage( dir, '@test/root-conditional', './node-entry.js', {
216
+ 'node-entry.js': 'export const helper = () => 1;\n',
217
+ 'workflow.js': 'export default workflow({ name: \'root.conditional\' });\n'
218
+ }, {
219
+ exports: {
220
+ 'output-workflow-bundle': './workflow.js',
221
+ default: './node-entry.js'
222
+ }
223
+ } );
224
+
225
+ const out = resolveBareImportSpecifiersAsWorkflows( {
226
+ fromAbsoluteFile: importing,
227
+ specifier: '@test/root-conditional',
228
+ specifiers: importSpecifiersFromSource( 'import RootWorkflow from \'@test/root-conditional\'' ),
229
+ workflowNameCache: new Map()
230
+ } );
231
+ expect( out ).toEqual( {
232
+ type: 'all',
233
+ bindings: [ { localName: 'RootWorkflow', workflowName: 'root.conditional' } ]
234
+ } );
235
+ } );
236
+ } );
237
+
238
+ it( 'uses webpack export condition when workflow bundle condition is absent', () => {
239
+ withTempProjectDir( dir => {
240
+ const importing = join( dir, 'workflow.js' );
241
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
242
+ writeNodeModulesPackage( dir, '@test/webpack-conditional', './node-entry.js', {
243
+ 'node-entry.js': 'export const helper = () => 1;\n',
244
+ 'webpack/workflow.js': 'export default workflow({ name: \'webpack.workflow\' });\n'
245
+ }, {
246
+ exports: {
247
+ '.': {
248
+ webpack: './webpack/workflow.js',
249
+ default: './node-entry.js'
250
+ }
251
+ }
252
+ } );
253
+
254
+ const out = resolveBareImportSpecifiersAsWorkflows( {
255
+ fromAbsoluteFile: importing,
256
+ specifier: '@test/webpack-conditional',
257
+ specifiers: importSpecifiersFromSource( 'import WebpackWorkflow from \'@test/webpack-conditional\'' ),
258
+ workflowNameCache: new Map()
259
+ } );
260
+ expect( out ).toEqual( {
261
+ type: 'all',
262
+ bindings: [ { localName: 'WebpackWorkflow', workflowName: 'webpack.workflow' } ]
263
+ } );
264
+ } );
265
+ } );
266
+
267
+ it( 'follows re-exports to workflow.js for a named import', () => {
268
+ withTempProjectDir( dir => {
269
+ const importing = join( dir, 'workflow.js' );
270
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
271
+ writeNodeModulesPackage( dir, '@test/catalog', './src/index.js', {
272
+ 'src/index.js': 'export { default as sumNumbers } from \'./wf/workflow.js\';\n',
273
+ 'src/wf/workflow.js': 'export default workflow({ name: \'sum.numbers\' });\n'
274
+ } );
275
+
276
+ const out = resolveBareImportSpecifiersAsWorkflows( {
277
+ fromAbsoluteFile: importing,
278
+ specifier: '@test/catalog',
279
+ specifiers: importSpecifiersFromSource( 'import { sumNumbers } from \'@test/catalog\'' ),
280
+ workflowNameCache: new Map()
281
+ } );
282
+ expect( out ).toEqual( {
283
+ type: 'all',
284
+ bindings: [ { localName: 'sumNumbers', workflowName: 'sum.numbers' } ]
285
+ } );
286
+ } );
287
+ } );
288
+
289
+ it( 'returns partial when one named import does not resolve to a workflow', () => {
290
+ withTempProjectDir( dir => {
291
+ const importing = join( dir, 'workflow.js' );
292
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
293
+ writeNodeModulesPackage( dir, '@test/partial', './index.js', {
294
+ 'index.js': 'export { default as Good } from \'./wf/workflow.js\';\n',
295
+ 'wf/workflow.js': 'export default workflow({ name: \'good.def\' });\n'
296
+ } );
297
+
298
+ const out = resolveBareImportSpecifiersAsWorkflows( {
299
+ fromAbsoluteFile: importing,
300
+ specifier: '@test/partial',
301
+ specifiers: importSpecifiersFromSource( 'import { Good, MissingExport } from \'@test/partial\'' ),
302
+ workflowNameCache: new Map()
303
+ } );
304
+ expect( out.type ).toBe( 'partial' );
305
+ } );
306
+ } );
307
+ } );
308
+
309
+ describe( 'resolveBareDestructuredRequireAsWorkflows', () => {
310
+ it( 'resolves destructured keys to workflow names', () => {
311
+ withTempProjectDir( dir => {
312
+ const importing = join( dir, 'workflow.js' );
313
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
314
+ writeNodeModulesPackage( dir, '@test/destr', './index.js', {
315
+ 'index.js': 'export { default as alpha } from \'./wf/workflow.js\';\n',
316
+ 'wf/workflow.js': 'export default workflow({ name: \'alpha.wf\' });\n'
317
+ } );
318
+
319
+ const props = destructuredRequirePropertiesFromSource(
320
+ 'const { alpha: A } = require(\'@test/destr\');'
321
+ );
322
+ const out = resolveBareDestructuredRequireAsWorkflows( {
323
+ fromAbsoluteFile: importing,
324
+ specifier: '@test/destr',
325
+ properties: props,
326
+ workflowNameCache: new Map()
327
+ } );
328
+ expect( out ).toEqual( {
329
+ type: 'all',
330
+ bindings: [ { localName: 'A', workflowName: 'alpha.wf' } ]
331
+ } );
332
+ } );
333
+ } );
334
+ } );
335
+
336
+ describe( 'resolveBareDefaultRequireAsWorkflow', () => {
337
+ it( 'returns binding when default resolves to a workflow', () => {
338
+ withTempProjectDir( dir => {
339
+ const importing = join( dir, 'workflow.js' );
340
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
341
+ writeNodeModulesPackage( dir, '@test/defreq', './entry.js', {
342
+ 'entry.js': 'export { default } from \'./wf/workflow.js\';\n',
343
+ 'wf/workflow.js': 'export default workflow({ name: \'chain.def\' });\n'
344
+ } );
345
+
346
+ const out = resolveBareDefaultRequireAsWorkflow(
347
+ importing,
348
+ '@test/defreq',
349
+ 'Cat',
350
+ new Map()
351
+ );
352
+ expect( out ).toEqual( {
353
+ type: 'binding',
354
+ localName: 'Cat',
355
+ workflowName: 'chain.def'
356
+ } );
357
+ } );
358
+ } );
359
+
360
+ it( 'returns none when nothing resolves', () => {
361
+ withTempProjectDir( dir => {
362
+ const importing = join( dir, 'workflow.js' );
363
+ writeFileSync( importing, 'export default workflow({ name: \'local\' });\n' );
364
+
365
+ const out = resolveBareDefaultRequireAsWorkflow(
366
+ importing,
367
+ '@ghost/no-such-pkg',
368
+ 'X',
369
+ new Map()
370
+ );
371
+ expect( out ).toEqual( { type: 'none' } );
372
+ } );
373
+ } );
374
+ } );
@@ -195,6 +195,15 @@ export const isAnyEvaluatorsPath = value =>
195
195
  */
196
196
  export const isWorkflowPath = value => /(^|\/)workflow\.js$/.test( value );
197
197
 
198
+ /**
199
+ * True when `resourcePath` is an absolute path to a `workflow.js` file (slashes normalized).
200
+ *
201
+ * @param {string|null|undefined} resourcePath - Webpack `resourcePath` or similar.
202
+ * @returns {boolean}
203
+ */
204
+ export const isAbsoluteWorkflowJsResource = resourcePath =>
205
+ typeof resourcePath === 'string' && isWorkflowPath( resourcePath.replace( /\\/g, '/' ) );
206
+
198
207
  /**
199
208
  * Check if a path is a component file (steps, evaluators, or workflow).
200
209
  * @param {string} value - Module path or request string.
@@ -16,6 +16,7 @@ import {
16
16
  isEvaluatorsPath,
17
17
  isSharedEvaluatorsPath,
18
18
  isWorkflowPath,
19
+ isAbsoluteWorkflowJsResource,
19
20
  createThisMethodCall,
20
21
  resolveNameFromArg,
21
22
  resolveNameFromOptions,
@@ -191,6 +192,12 @@ describe( 'workflow_rewriter tools', () => {
191
192
  expect( isWorkflowPath( 'steps.js' ) ).toBe( false );
192
193
  } );
193
194
 
195
+ it( 'isAbsoluteWorkflowJsResource: normalizes slashes', () => {
196
+ expect( isAbsoluteWorkflowJsResource( '/a/b/workflow.js' ) ).toBe( true );
197
+ expect( isAbsoluteWorkflowJsResource( 'C:\\\\a\\\\workflow.js' ) ).toBe( true );
198
+ expect( isAbsoluteWorkflowJsResource( '/a/b/steps.js' ) ).toBe( false );
199
+ } );
200
+
194
201
  it( 'isEvaluatorsPath: matches local evaluators.js but excludes shared', () => {
195
202
  expect( isEvaluatorsPath( 'evaluators.js' ) ).toBe( true );
196
203
  expect( isEvaluatorsPath( './evaluators.js' ) ).toBe( true );
@@ -1,18 +1,27 @@
1
+ import { join } from 'node:path';
1
2
  import traverseModule from '@babel/traverse';
2
3
  import {
3
4
  buildWorkflowNameMap,
5
+ buildEvaluatorsNameMap,
6
+ buildSharedEvaluatorsNameMap,
7
+ buildSharedStepsNameMap,
8
+ buildStepsNameMap,
4
9
  getLocalNameFromDestructuredProperty,
10
+ isAbsoluteWorkflowJsResource,
5
11
  isEvaluatorsPath,
6
12
  isSharedEvaluatorsPath,
7
13
  isSharedStepsPath,
8
14
  isStepsPath,
9
15
  isWorkflowPath,
10
- buildStepsNameMap,
11
- buildSharedStepsNameMap,
12
- buildEvaluatorsNameMap,
13
- buildSharedEvaluatorsNameMap,
14
16
  toAbsolutePath
15
17
  } from '../tools.js';
18
+ import {
19
+ isBareNpmSpecifier,
20
+ resolveBareImportSpecifiersAsWorkflows,
21
+ resolveBareDestructuredRequireAsWorkflows,
22
+ resolveBareDefaultRequireAsWorkflow
23
+ } from '../npm_workflow_export_resolve.js';
24
+
16
25
  import {
17
26
  isCallExpression,
18
27
  isIdentifier,
@@ -35,6 +44,12 @@ const unresolvedImportError = ( name, fileLabel, filePath ) =>
35
44
  '(e.g. step() in steps files, evaluator() in evaluators files, workflow() in workflow files).'
36
45
  );
37
46
 
47
+ const mixedBareWorkflowImportError = ( specifier, resourcePath ) => new Error(
48
+ `Workflow file '${resourcePath}': import from '${specifier}' mixes workflow exports with ` +
49
+ 'non-workflow exports, or could not resolve every binding to a workflow.js module. ' +
50
+ 'Split npm imports so each declaration only imports workflows, or only non-workflows.'
51
+ );
52
+
38
53
  const removeRequireDeclarator = path => {
39
54
  if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
40
55
  path.parentPath.remove();
@@ -77,13 +92,17 @@ const collectDestructuredRequires = ( path, absolutePath, req, descriptors ) =>
77
92
  * @param {string} fileDir - Absolute directory of the file represented by `ast`.
78
93
  * @param {{ stepsNameCache: Map<string,Map<string,string>>, workflowNameCache: Map<string,{default:(string|null),named:Map<string,string>}> }} caches
79
94
  * Resolved-name caches to avoid re-reading same modules.
95
+ * @param {string} [resourcePath] - Absolute path of the file being transformed; used to resolve
96
+ * npm package imports from `workflow.js` via Node resolution and export following.
80
97
  * @returns {{ stepImports: Array<{localName:string,stepName:string}>,
81
98
  * flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
82
99
  */
83
100
  export default function collectTargetImports(
84
101
  ast, fileDir,
85
- { stepsNameCache, workflowNameCache, evaluatorsNameCache, sharedStepsNameCache, sharedEvaluatorsNameCache }
102
+ { stepsNameCache, workflowNameCache, evaluatorsNameCache, sharedStepsNameCache, sharedEvaluatorsNameCache },
103
+ resourcePath
86
104
  ) {
105
+ const resolutionPath = resourcePath ?? join( fileDir, 'file.js' );
87
106
  const stepImports = [];
88
107
  const sharedStepImports = [];
89
108
  const flowImports = [];
@@ -93,9 +112,29 @@ export default function collectTargetImports(
93
112
  traverse( ast, {
94
113
  ImportDeclaration: path => {
95
114
  const src = path.node.source.value;
96
- // Ignore other imports
115
+
116
+ if ( isBareNpmSpecifier( src ) && isAbsoluteWorkflowJsResource( resolutionPath ) ) {
117
+ const outcome = resolveBareImportSpecifiersAsWorkflows( {
118
+ fromAbsoluteFile: resolutionPath,
119
+ specifier: src,
120
+ specifiers: path.node.specifiers,
121
+ workflowNameCache
122
+ } );
123
+ if ( outcome.type === 'partial' ) {
124
+ throw mixedBareWorkflowImportError( src, resolutionPath );
125
+ }
126
+ if ( outcome.type === 'all' ) {
127
+ for ( const { localName, workflowName } of outcome.bindings ) {
128
+ flowImports.push( { localName, workflowName } );
129
+ }
130
+ path.remove();
131
+ return;
132
+ }
133
+ }
134
+
97
135
  const isTargetImport = isStepsPath( src ) || isSharedStepsPath( src ) ||
98
- isWorkflowPath( src ) || isEvaluatorsPath( src ) || isSharedEvaluatorsPath( src );
136
+ isWorkflowPath( src ) ||
137
+ isEvaluatorsPath( src ) || isSharedEvaluatorsPath( src );
99
138
  if ( !isTargetImport ) {
100
139
  return;
101
140
  }
@@ -161,15 +200,46 @@ export default function collectTargetImports(
161
200
  }
162
201
 
163
202
  const req = firstArgument.value;
203
+
204
+ if ( isBareNpmSpecifier( req ) && isAbsoluteWorkflowJsResource( resolutionPath ) ) {
205
+ if ( isObjectPattern( path.node.id ) ) {
206
+ const outcome = resolveBareDestructuredRequireAsWorkflows( {
207
+ fromAbsoluteFile: resolutionPath,
208
+ specifier: req,
209
+ properties: path.node.id.properties,
210
+ workflowNameCache
211
+ } );
212
+ if ( outcome.type === 'partial' ) {
213
+ throw mixedBareWorkflowImportError( req, resolutionPath );
214
+ }
215
+ if ( outcome.type === 'all' ) {
216
+ for ( const { localName, workflowName } of outcome.bindings ) {
217
+ flowImports.push( { localName, workflowName } );
218
+ }
219
+ removeRequireDeclarator( path );
220
+ return;
221
+ }
222
+ } else if ( isIdentifier( path.node.id ) ) {
223
+ const outcome = resolveBareDefaultRequireAsWorkflow(
224
+ resolutionPath, req, path.node.id.name, workflowNameCache
225
+ );
226
+ if ( outcome.type === 'binding' ) {
227
+ flowImports.push( { localName: outcome.localName, workflowName: outcome.workflowName } );
228
+ removeRequireDeclarator( path );
229
+ return;
230
+ }
231
+ }
232
+ }
233
+
164
234
  const isTargetRequire = isStepsPath( req ) || isSharedStepsPath( req ) ||
165
- isWorkflowPath( req ) || isEvaluatorsPath( req ) || isSharedEvaluatorsPath( req );
235
+ isWorkflowPath( req ) ||
236
+ isEvaluatorsPath( req ) || isSharedEvaluatorsPath( req );
166
237
  if ( !isTargetRequire ) {
167
238
  return;
168
239
  }
169
240
 
170
241
  const absolutePath = toAbsolutePath( fileDir, req );
171
242
 
172
- // Destructured requires: const { X } = require('./steps.js')
173
243
  if ( isObjectPattern( path.node.id ) ) {
174
244
  const cjsDescriptors = [
175
245
  {
@@ -207,7 +277,6 @@ export default function collectTargetImports(
207
277
  return;
208
278
  }
209
279
 
210
- // Default workflow require: const WF = require('./workflow.js')
211
280
  if ( isWorkflowPath( req ) && isIdentifier( path.node.id ) ) {
212
281
  const { default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
213
282
  const localName = path.node.id.name;
@@ -218,4 +287,4 @@ export default function collectTargetImports(
218
287
  } );
219
288
 
220
289
  return { stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports };
221
- };
290
+ }
@@ -332,5 +332,72 @@ const obj = {};`;
332
332
 
333
333
  rmSync( dir, { recursive: true, force: true } );
334
334
  } );
335
+
336
+ it( 'collects ESM imports from @growthxlabs/workflows_catalog', () => {
337
+ const dir = mkdtempSync( join( tmpdir(), 'collect-cat-esm-' ) );
338
+ const pkgRoot = join( dir, 'node_modules', '@growthxlabs', 'workflows_catalog' );
339
+ const srcDir = join( pkgRoot, 'src' );
340
+ mkdirSync( join( srcDir, 'workflows', 'wf' ), { recursive: true } );
341
+ writeFileSync( join( pkgRoot, 'package.json' ), JSON.stringify( {
342
+ name: '@growthxlabs/workflows_catalog',
343
+ type: 'module',
344
+ main: './src/index.js',
345
+ dependencies: { '@outputai/core': '1.0.0' }
346
+ } ) );
347
+ writeFileSync( join( srcDir, 'index.js' ), 'export { default as sumNumbers } from \'./workflows/wf/workflow.js\';\n' );
348
+ writeFileSync( join( srcDir, 'workflows', 'wf', 'workflow.js' ), 'export default workflow({ name: \'cat.flow\' });\n' );
349
+
350
+ const fileDir = join( dir, 'consumer' );
351
+ mkdirSync( fileDir, { recursive: true } );
352
+ const resourcePath = join( fileDir, 'workflow.js' );
353
+
354
+ const source = `
355
+ import { sumNumbers as SN } from '@growthxlabs/workflows_catalog';
356
+ const x = 1;`;
357
+ const ast = makeAst( source, resourcePath );
358
+
359
+ const { flowImports } = collectTargetImports(
360
+ ast,
361
+ fileDir,
362
+ { stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() },
363
+ resourcePath
364
+ );
365
+ expect( flowImports ).toEqual( [ { localName: 'SN', workflowName: 'cat.flow' } ] );
366
+ expect( ast.program.body.find( n => n.type === 'ImportDeclaration' ) ).toBeUndefined();
367
+
368
+ rmSync( dir, { recursive: true, force: true } );
369
+ } );
370
+
371
+ it( 'collects CJS destructured require from @growthxlabs/workflows_catalog', () => {
372
+ const dir = mkdtempSync( join( tmpdir(), 'collect-cat-cjs-' ) );
373
+ const pkgRoot = join( dir, 'node_modules', '@growthxlabs', 'workflows_catalog' );
374
+ const srcDir = join( pkgRoot, 'src' );
375
+ mkdirSync( join( srcDir, 'workflows', 'wf' ), { recursive: true } );
376
+ writeFileSync( join( pkgRoot, 'package.json' ), JSON.stringify( {
377
+ name: '@growthxlabs/workflows_catalog',
378
+ type: 'module',
379
+ main: './src/index.js',
380
+ dependencies: { '@outputai/core': '1.0.0' }
381
+ } ) );
382
+ writeFileSync( join( srcDir, 'index.js' ), 'export { default as sumNumbers } from \'./workflows/wf/workflow.js\';\n' );
383
+ writeFileSync( join( srcDir, 'workflows', 'wf', 'workflow.js' ), 'export default workflow({ name: \'cat.flow2\' });\n' );
384
+
385
+ const fileDir = join( dir, 'consumer' );
386
+ mkdirSync( fileDir, { recursive: true } );
387
+ const resourcePath = join( fileDir, 'workflow.js' );
388
+
389
+ const source = 'const { sumNumbers } = require( \'@growthxlabs/workflows_catalog\' );\nconst x = 1;';
390
+ const ast = makeAst( source, resourcePath );
391
+
392
+ const { flowImports } = collectTargetImports(
393
+ ast,
394
+ fileDir,
395
+ { stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() },
396
+ resourcePath
397
+ );
398
+ expect( flowImports ).toEqual( [ { localName: 'sumNumbers', workflowName: 'cat.flow2' } ] );
399
+
400
+ rmSync( dir, { recursive: true, force: true } );
401
+ } );
335
402
  } );
336
403
 
@@ -34,7 +34,8 @@ export default function stepImportRewriterAstLoader( source, inputMap ) {
34
34
  const filename = this.resourcePath;
35
35
  const ast = parse( String( source ), filename );
36
36
  const fileDir = dirname( filename );
37
- const { stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
37
+ const { stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports } =
38
+ collectTargetImports( ast, fileDir, cache, filename );
38
39
 
39
40
  // No imports
40
41
  if ( [].concat( stepImports, sharedStepImports, evaluatorImports, sharedEvaluatorImports, flowImports ).length === 0 ) {