@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.
- package/package.json +7 -2
- 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/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
|
@@ -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
|
-
|
|
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 ) ||
|
|
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 ) ||
|
|
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 } =
|
|
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 ) {
|