@outputai/core 0.3.3-next.b23002f.0 → 0.3.3-next.cb14409.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,15 +1,268 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { mkdirSync,
|
|
3
|
-
import { join, sep } from 'node:path';
|
|
4
|
-
import {
|
|
2
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, sep } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
activityMatchersBuilder,
|
|
8
|
+
matchFiles,
|
|
9
|
+
findWorkflowsInNodeModules,
|
|
10
|
+
findWorkflowsInPackages,
|
|
11
|
+
findSharedActivitiesFromWorkflows,
|
|
12
|
+
importComponents,
|
|
13
|
+
findPackageRoot,
|
|
14
|
+
isPackageRoot,
|
|
15
|
+
isPathDescendentFromNodeModules,
|
|
16
|
+
resolveNodeModulesPath,
|
|
17
|
+
resolveSymlink,
|
|
18
|
+
staticMatchers,
|
|
19
|
+
packageExposesWorkflows
|
|
20
|
+
} from './loader_tools.js';
|
|
5
21
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
const TEMP_BASE = join( process.cwd(), 'sdk/core/temp_test_modules' );
|
|
23
|
+
|
|
24
|
+
afterEach( () => {
|
|
25
|
+
rmSync( TEMP_BASE, { recursive: true, force: true } );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
const fileEntry = path => ( { path, url: pathToFileURL( path ).href } );
|
|
29
|
+
|
|
30
|
+
describe( 'resolveSymlink', () => {
|
|
31
|
+
it( 'returns the canonical path for a directory', () => {
|
|
32
|
+
const dir = join( TEMP_BASE, `rs-dir-${Date.now()}` );
|
|
33
|
+
mkdirSync( dir, { recursive: true } );
|
|
34
|
+
expect( resolveSymlink( dir ) ).toBe( dir );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'returns the canonical path for a regular file', () => {
|
|
38
|
+
const dir = join( TEMP_BASE, `rs-file-${Date.now()}` );
|
|
39
|
+
mkdirSync( dir, { recursive: true } );
|
|
40
|
+
const file = join( dir, 'a.txt' );
|
|
41
|
+
writeFileSync( file, 'x' );
|
|
42
|
+
expect( resolveSymlink( file ) ).toBe( file );
|
|
43
|
+
} );
|
|
44
|
+
|
|
45
|
+
it( 'returns null for a broken symlink', () => {
|
|
46
|
+
const dir = join( TEMP_BASE, `rs-broken-${Date.now()}` );
|
|
47
|
+
mkdirSync( dir, { recursive: true } );
|
|
48
|
+
const link = join( dir, 'broken' );
|
|
49
|
+
symlinkSync( 'missing-target', link, 'file' );
|
|
50
|
+
expect( resolveSymlink( link ) ).toBe( null );
|
|
51
|
+
} );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
describe( 'resolveNodeModulesPath', () => {
|
|
55
|
+
it( 'returns node_modules when passed that directory', () => {
|
|
56
|
+
const root = join( TEMP_BASE, `rnm-direct-${Date.now()}` );
|
|
57
|
+
const nm = join( root, 'node_modules' );
|
|
58
|
+
mkdirSync( nm, { recursive: true } );
|
|
59
|
+
expect( resolveNodeModulesPath( nm ) ).toBe( nm );
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
it( 'finds node_modules from a project root directory', () => {
|
|
63
|
+
const root = join( TEMP_BASE, `rnm-root-${Date.now()}` );
|
|
64
|
+
const nm = join( root, 'node_modules' );
|
|
65
|
+
mkdirSync( nm, { recursive: true } );
|
|
66
|
+
expect( resolveNodeModulesPath( root ) ).toBe( nm );
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
it( 'walks upward from a nested path until node_modules is found', () => {
|
|
70
|
+
const root = join( TEMP_BASE, `rnm-walk-${Date.now()}` );
|
|
71
|
+
const nm = join( root, 'node_modules' );
|
|
72
|
+
const nested = join( root, 'src', 'deep' );
|
|
73
|
+
mkdirSync( nested, { recursive: true } );
|
|
74
|
+
mkdirSync( nm, { recursive: true } );
|
|
75
|
+
expect( resolveNodeModulesPath( nested ) ).toBe( nm );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'uses dirname when targetPath is a file', () => {
|
|
79
|
+
const root = join( TEMP_BASE, `rnm-file-${Date.now()}` );
|
|
80
|
+
const nm = join( root, 'node_modules' );
|
|
81
|
+
const file = join( root, 'index.js' );
|
|
82
|
+
mkdirSync( nm, { recursive: true } );
|
|
83
|
+
writeFileSync( file, '' );
|
|
84
|
+
expect( resolveNodeModulesPath( file ) ).toBe( nm );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
it( 'returns null when targetPath does not exist', () => {
|
|
88
|
+
expect( resolveNodeModulesPath( join( TEMP_BASE, 'nope', 'missing' ) ) ).toBe( null );
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
it( 'returns null when no ancestor has node_modules', () => {
|
|
92
|
+
const isolated = mkdtempSync( join( tmpdir(), 'loader-tools-rnm-' ) );
|
|
93
|
+
try {
|
|
94
|
+
const orphan = join( isolated, 'nested', 'sub' );
|
|
95
|
+
mkdirSync( orphan, { recursive: true } );
|
|
96
|
+
expect( resolveNodeModulesPath( orphan ) ).toBe( null );
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync( isolated, { recursive: true, force: true } );
|
|
99
|
+
}
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'returns the canonical directory when node_modules is a symlink', () => {
|
|
103
|
+
const root = join( TEMP_BASE, `rnm-symlink-${Date.now()}` );
|
|
104
|
+
const realNm = join( root, 'real_node_modules' );
|
|
105
|
+
const nmLink = join( root, 'node_modules' );
|
|
106
|
+
mkdirSync( realNm, { recursive: true } );
|
|
107
|
+
symlinkSync( 'real_node_modules', nmLink, 'dir' );
|
|
108
|
+
expect( resolveNodeModulesPath( root ) ).toBe( realNm );
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
it( 'walks up when node_modules is a broken symlink', () => {
|
|
112
|
+
const grandparent = join( TEMP_BASE, `rnm-walkup-${Date.now()}` );
|
|
113
|
+
const root = join( grandparent, 'proj' );
|
|
114
|
+
const nmBroken = join( root, 'node_modules' );
|
|
115
|
+
const nmUp = join( grandparent, 'node_modules' );
|
|
116
|
+
mkdirSync( nmUp, { recursive: true } );
|
|
117
|
+
mkdirSync( root, { recursive: true } );
|
|
118
|
+
symlinkSync( 'does-not-exist', nmBroken, 'dir' );
|
|
119
|
+
expect( resolveNodeModulesPath( root ) ).toBe( nmUp );
|
|
120
|
+
} );
|
|
121
|
+
} );
|
|
122
|
+
|
|
123
|
+
describe( 'node_modules package resource helpers', () => {
|
|
124
|
+
it( 'detects paths inside node_modules', () => {
|
|
125
|
+
expect( isPathDescendentFromNodeModules( `${sep}app${sep}node_modules${sep}pkg${sep}index.js` ) ).toBe( true );
|
|
126
|
+
expect( isPathDescendentFromNodeModules( 'C:\\app\\node_modules\\pkg\\index.js' ) ).toBe( true );
|
|
127
|
+
expect( isPathDescendentFromNodeModules( `${sep}app${sep}src${sep}index.js` ) ).toBe( false );
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'detects installed package roots', () => {
|
|
131
|
+
expect( isPackageRoot( `${sep}app${sep}node_modules${sep}pkg` ) ).toBe( true );
|
|
132
|
+
expect( isPackageRoot( `${sep}app${sep}node_modules${sep}@scope${sep}pkg` ) ).toBe( true );
|
|
133
|
+
expect( isPackageRoot( `${sep}app${sep}node_modules${sep}@scope` ) ).toBe( false );
|
|
134
|
+
expect( isPackageRoot( `${sep}app${sep}node_modules${sep}pkg${sep}lib` ) ).toBe( false );
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
it( 'finds the closest installed package root for a file', () => {
|
|
138
|
+
const root = join( TEMP_BASE, `pkgroot-${Date.now()}` );
|
|
139
|
+
const pkgRoot = join( root, 'node_modules', '@acme', 'wf_pkg' );
|
|
140
|
+
const deepFile = join( pkgRoot, 'lib', 'nested', 'workflow.js' );
|
|
141
|
+
mkdirSync( join( pkgRoot, 'lib', 'nested' ), { recursive: true } );
|
|
142
|
+
writeFileSync( join( pkgRoot, 'package.json' ), JSON.stringify( {
|
|
143
|
+
name: '@acme/wf_pkg',
|
|
144
|
+
dependencies: { '@outputai/core': '1.0.0' }
|
|
145
|
+
} ) );
|
|
146
|
+
writeFileSync( deepFile, 'export default {};\n' );
|
|
147
|
+
|
|
148
|
+
expect( findPackageRoot( deepFile ) ).toBe( pkgRoot );
|
|
149
|
+
} );
|
|
150
|
+
|
|
151
|
+
it( 'returns null when no installed package root is found', () => {
|
|
152
|
+
const root = join( TEMP_BASE, `pkgroot-missing-${Date.now()}` );
|
|
153
|
+
const deepFile = join( root, 'node_modules', 'plain_lib', 'index.js' );
|
|
154
|
+
mkdirSync( dirname( deepFile ), { recursive: true } );
|
|
155
|
+
writeFileSync( deepFile, 'export const x = 1;\n' );
|
|
156
|
+
|
|
157
|
+
expect( findPackageRoot( deepFile ) ).toBe( null );
|
|
158
|
+
} );
|
|
159
|
+
} );
|
|
160
|
+
|
|
161
|
+
describe( 'activityMatchersBuilder', () => {
|
|
162
|
+
const base = `${sep}app${sep}proj`;
|
|
163
|
+
|
|
164
|
+
it( 'stepsFile matches only steps.js at base', () => {
|
|
165
|
+
const m = activityMatchersBuilder( base );
|
|
166
|
+
expect( m.stepsFile( `${base}${sep}steps.js` ) ).toBe( true );
|
|
167
|
+
expect( m.stepsFile( `${base}${sep}nested${sep}steps.js` ) ).toBe( false );
|
|
10
168
|
} );
|
|
169
|
+
|
|
170
|
+
it( 'evaluatorsFile matches only evaluators.js at base', () => {
|
|
171
|
+
const m = activityMatchersBuilder( base );
|
|
172
|
+
expect( m.evaluatorsFile( `${base}${sep}evaluators.js` ) ).toBe( true );
|
|
173
|
+
expect( m.evaluatorsFile( `${base}${sep}sub${sep}evaluators.js` ) ).toBe( false );
|
|
174
|
+
} );
|
|
175
|
+
|
|
176
|
+
it( 'stepsDir matches js under steps/', () => {
|
|
177
|
+
const m = activityMatchersBuilder( base );
|
|
178
|
+
expect( m.stepsDir( `${base}${sep}steps${sep}a.js` ) ).toBe( true );
|
|
179
|
+
expect( m.stepsDir( `${base}${sep}steps${sep}sub${sep}b.js` ) ).toBe( true );
|
|
180
|
+
expect( m.stepsDir( `${base}${sep}other${sep}a.js` ) ).toBe( false );
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
it( 'evaluatorsDir matches js under evaluators/', () => {
|
|
184
|
+
const m = activityMatchersBuilder( base );
|
|
185
|
+
expect( m.evaluatorsDir( `${base}${sep}evaluators${sep}x.js` ) ).toBe( true );
|
|
186
|
+
expect( m.evaluatorsDir( `${base}${sep}evaluators${sep}y${sep}z.js` ) ).toBe( true );
|
|
187
|
+
expect( m.evaluatorsDir( `${base}${sep}steps${sep}x.js` ) ).toBe( false );
|
|
188
|
+
} );
|
|
189
|
+
} );
|
|
190
|
+
|
|
191
|
+
describe( 'matchFiles', () => {
|
|
192
|
+
it( 'collects files matching matchers', () => {
|
|
193
|
+
const root = join( TEMP_BASE, `fbnr-files-${Date.now()}` );
|
|
194
|
+
mkdirSync( root, { recursive: true } );
|
|
195
|
+
writeFileSync( join( root, 'a.txt' ), '' );
|
|
196
|
+
writeFileSync( join( root, 'b.txt' ), '' );
|
|
197
|
+
const found = matchFiles( root, [ p => p.endsWith( 'a.txt' ) ] );
|
|
198
|
+
expect( found ).toHaveLength( 1 );
|
|
199
|
+
expect( found[0].path ).toBe( join( root, 'a.txt' ) );
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
it( 'skips broken symlinks without throwing', () => {
|
|
203
|
+
const root = join( TEMP_BASE, `fbnr-broken-${Date.now()}` );
|
|
204
|
+
mkdirSync( root, { recursive: true } );
|
|
205
|
+
symlinkSync( 'nowhere', join( root, 'bad' ), 'file' );
|
|
206
|
+
writeFileSync( join( root, 'ok.txt' ), '' );
|
|
207
|
+
const found = matchFiles( root, [ p => p.endsWith( '.txt' ) ] );
|
|
208
|
+
expect( found.map( f => f.path ) ).toEqual( [ join( root, 'ok.txt' ) ] );
|
|
209
|
+
} );
|
|
210
|
+
|
|
211
|
+
it( 'follows symlinks to directories', () => {
|
|
212
|
+
const base = join( TEMP_BASE, `fbnr-symlink-dir-${Date.now()}` );
|
|
213
|
+
const targetDir = join( base, 'target' );
|
|
214
|
+
const scanRoot = join( base, 'root' );
|
|
215
|
+
mkdirSync( targetDir, { recursive: true } );
|
|
216
|
+
mkdirSync( scanRoot, { recursive: true } );
|
|
217
|
+
writeFileSync( join( targetDir, 'x.txt' ), '' );
|
|
218
|
+
symlinkSync( join( '..', 'target' ), join( scanRoot, 'link' ), 'dir' );
|
|
219
|
+
const found = matchFiles( scanRoot, [ p => p.endsWith( 'x.txt' ) ] );
|
|
220
|
+
expect( found ).toHaveLength( 1 );
|
|
221
|
+
expect( found[0].path ).toBe( join( targetDir, 'x.txt' ) );
|
|
222
|
+
} );
|
|
223
|
+
} );
|
|
224
|
+
|
|
225
|
+
describe( 'findWorkflowsInPackages', () => {
|
|
226
|
+
it( 'collects workflow.js from exposed workflow packages under node_modules', () => {
|
|
227
|
+
const root = join( TEMP_BASE, `fwp-${Date.now()}` );
|
|
228
|
+
const nm = join( root, 'node_modules' );
|
|
229
|
+
const pkg = join( nm, 'wf_pkg' );
|
|
230
|
+
mkdirSync( join( pkg, 'lib' ), { recursive: true } );
|
|
231
|
+
writeFileSync( join( pkg, 'package.json' ), JSON.stringify( {
|
|
232
|
+
name: 'wf_pkg',
|
|
233
|
+
outputai: { workflows: { expose: true } }
|
|
234
|
+
} ) );
|
|
235
|
+
const wf = join( pkg, 'lib', 'workflow.js' );
|
|
236
|
+
writeFileSync( wf, 'export default {};\n' );
|
|
237
|
+
|
|
238
|
+
const found = findWorkflowsInPackages( nm );
|
|
239
|
+
expect( found ).toHaveLength( 1 );
|
|
240
|
+
expect( found[0].path ).toBe( wf );
|
|
241
|
+
} );
|
|
242
|
+
|
|
243
|
+
it( 'lists the same workflow twice when package scan sees a canonical package and symlink alias', () => {
|
|
244
|
+
const root = join( TEMP_BASE, `fwp-symlink-${Date.now()}` );
|
|
245
|
+
const nm = join( root, 'node_modules' );
|
|
246
|
+
const realPkg = join( nm, 'real_pkg' );
|
|
247
|
+
const linkPkg = join( nm, 'link_pkg' );
|
|
248
|
+
mkdirSync( join( realPkg, 'w' ), { recursive: true } );
|
|
249
|
+
const wf = join( realPkg, 'w', 'workflow.js' );
|
|
250
|
+
writeFileSync( join( realPkg, 'package.json' ), JSON.stringify( {
|
|
251
|
+
name: 'real_pkg',
|
|
252
|
+
outputai: { workflows: { expose: true } }
|
|
253
|
+
} ) );
|
|
254
|
+
writeFileSync( wf, 'export default {};\n' );
|
|
255
|
+
symlinkSync( 'real_pkg', linkPkg, 'dir' );
|
|
256
|
+
|
|
257
|
+
const found = findWorkflowsInPackages( nm );
|
|
258
|
+
expect( found ).toHaveLength( 2 );
|
|
259
|
+
expect( new Set( found.map( f => realpathSync( f.path ) ) ).size ).toBe( 1 );
|
|
260
|
+
} );
|
|
261
|
+
} );
|
|
262
|
+
|
|
263
|
+
describe( 'importComponents', () => {
|
|
11
264
|
it( 'imports modules and yields metadata from exports tagged with METADATA_ACCESS_SYMBOL', async () => {
|
|
12
|
-
const root = join(
|
|
265
|
+
const root = join( TEMP_BASE, `meta-${Date.now()}` );
|
|
13
266
|
mkdirSync( root, { recursive: true } );
|
|
14
267
|
const file = join( root, 'meta.module.js' );
|
|
15
268
|
writeFileSync( file, [
|
|
@@ -21,7 +274,7 @@ describe( '.importComponents', () => {
|
|
|
21
274
|
].join( '\n' ) );
|
|
22
275
|
|
|
23
276
|
const collected = [];
|
|
24
|
-
for await ( const m of importComponents(
|
|
277
|
+
for await ( const m of importComponents( [ fileEntry( file ) ] ) ) {
|
|
25
278
|
collected.push( m );
|
|
26
279
|
}
|
|
27
280
|
|
|
@@ -32,12 +285,10 @@ describe( '.importComponents', () => {
|
|
|
32
285
|
expect( m.path ).toBe( file );
|
|
33
286
|
expect( typeof m.fn ).toBe( 'function' );
|
|
34
287
|
}
|
|
35
|
-
|
|
36
|
-
rmSync( root, { recursive: true, force: true } );
|
|
37
288
|
} );
|
|
38
289
|
|
|
39
290
|
it( 'ignores exports without metadata symbol', async () => {
|
|
40
|
-
const root = join(
|
|
291
|
+
const root = join( TEMP_BASE, `meta-${Date.now()}-nometa` );
|
|
41
292
|
mkdirSync( root, { recursive: true } );
|
|
42
293
|
const file = join( root, 'meta.module.js' );
|
|
43
294
|
writeFileSync( file, [
|
|
@@ -46,16 +297,15 @@ describe( '.importComponents', () => {
|
|
|
46
297
|
].join( '\n' ) );
|
|
47
298
|
|
|
48
299
|
const collected = [];
|
|
49
|
-
for await ( const m of importComponents(
|
|
300
|
+
for await ( const m of importComponents( [ fileEntry( file ) ] ) ) {
|
|
50
301
|
collected.push( m );
|
|
51
302
|
}
|
|
52
303
|
|
|
53
304
|
expect( collected.length ).toBe( 0 );
|
|
54
|
-
rmSync( root, { recursive: true, force: true } );
|
|
55
305
|
} );
|
|
56
306
|
|
|
57
307
|
it( 'skips files inside ignored directories (node_modules, vendor)', async () => {
|
|
58
|
-
const root = join(
|
|
308
|
+
const root = join( TEMP_BASE, `meta-${Date.now()}-ignoredirs` );
|
|
59
309
|
const okDir = join( root, 'ok' );
|
|
60
310
|
const nmDir = join( root, 'node_modules' );
|
|
61
311
|
const vendorDir = join( root, 'vendor' );
|
|
@@ -77,18 +327,16 @@ describe( '.importComponents', () => {
|
|
|
77
327
|
writeFileSync( vendorFile, fileContents );
|
|
78
328
|
|
|
79
329
|
const collected = [];
|
|
80
|
-
for await ( const m of importComponents( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) {
|
|
330
|
+
for await ( const m of importComponents( matchFiles( root, [ v => v.endsWith( 'meta.module.js' ) ] ) ) ) {
|
|
81
331
|
collected.push( m );
|
|
82
332
|
}
|
|
83
333
|
|
|
84
334
|
expect( collected.length ).toBe( 1 );
|
|
85
335
|
expect( collected[0].path ).toBe( okFile );
|
|
86
|
-
|
|
87
|
-
rmSync( root, { recursive: true, force: true } );
|
|
88
336
|
} );
|
|
89
337
|
|
|
90
338
|
it( 'supports partial matching by folder name', async () => {
|
|
91
|
-
const root = join(
|
|
339
|
+
const root = join( TEMP_BASE, `meta-${Date.now()}-foldermatch` );
|
|
92
340
|
const okDir = join( root, 'features', 'ok' );
|
|
93
341
|
const otherDir = join( root, 'features', 'other' );
|
|
94
342
|
mkdirSync( okDir, { recursive: true } );
|
|
@@ -104,21 +352,244 @@ describe( '.importComponents', () => {
|
|
|
104
352
|
writeFileSync( okFile, src );
|
|
105
353
|
writeFileSync( otherFile, src );
|
|
106
354
|
|
|
107
|
-
// Match any JS under a folder named "ok"
|
|
108
355
|
const matcher = v => v.includes( `${join( 'features', 'ok' )}${sep}` );
|
|
109
356
|
const collected = [];
|
|
110
|
-
for await ( const m of importComponents( root, [ matcher ] ) ) {
|
|
357
|
+
for await ( const m of importComponents( matchFiles( root, [ matcher ] ) ) ) {
|
|
111
358
|
collected.push( m );
|
|
112
359
|
}
|
|
113
360
|
expect( collected.length ).toBe( 1 );
|
|
114
361
|
expect( collected[0].path ).toBe( okFile );
|
|
362
|
+
} );
|
|
363
|
+
|
|
364
|
+
it( 'follows symlinks to directories and collects matching files inside the target', async () => {
|
|
365
|
+
const base = join( TEMP_BASE, `symlink-dir-${Date.now()}` );
|
|
366
|
+
const targetDir = join( base, 'target' );
|
|
367
|
+
const scanRoot = join( base, 'root' );
|
|
368
|
+
mkdirSync( targetDir, { recursive: true } );
|
|
369
|
+
mkdirSync( scanRoot, { recursive: true } );
|
|
370
|
+
const moduleFile = join( targetDir, 'meta.module.js' );
|
|
371
|
+
writeFileSync( moduleFile, [
|
|
372
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
373
|
+
'export const ViaLink = () => {};',
|
|
374
|
+
'ViaLink[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "via_link" };'
|
|
375
|
+
].join( '\n' ) );
|
|
376
|
+
|
|
377
|
+
symlinkSync( join( '..', 'target' ), join( scanRoot, 'pkg' ), 'dir' );
|
|
378
|
+
|
|
379
|
+
const collected = [];
|
|
380
|
+
for await ( const m of importComponents( matchFiles( scanRoot, [ v => v.endsWith( 'meta.module.js' ) ] ) ) ) {
|
|
381
|
+
collected.push( m );
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
expect( collected.length ).toBe( 1 );
|
|
385
|
+
expect( collected[0].path ).toBe( moduleFile );
|
|
386
|
+
expect( collected[0].metadata.name ).toBe( 'via_link' );
|
|
387
|
+
} );
|
|
388
|
+
|
|
389
|
+
it( 'collects a symlinked file when the matcher matches the realpath target (canonical url)', async () => {
|
|
390
|
+
const root = join( TEMP_BASE, `symlink-file-${Date.now()}` );
|
|
391
|
+
mkdirSync( root, { recursive: true } );
|
|
392
|
+
const realFile = join( root, 'real.module.js' );
|
|
393
|
+
writeFileSync( realFile, [
|
|
394
|
+
'import { METADATA_ACCESS_SYMBOL } from "#consts";',
|
|
395
|
+
'export const R = () => {};',
|
|
396
|
+
'R[METADATA_ACCESS_SYMBOL] = { kind: "step", name: "r" };'
|
|
397
|
+
].join( '\n' ) );
|
|
398
|
+
const linkPath = join( root, 'alias.module.js' );
|
|
399
|
+
symlinkSync( 'real.module.js', linkPath, 'file' );
|
|
400
|
+
|
|
401
|
+
const collected = [];
|
|
402
|
+
for await ( const m of importComponents( matchFiles( root, [ v => v.endsWith( '.module.js' ) ] ) ) ) {
|
|
403
|
+
collected.push( m );
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
expect( collected.length ).toBe( 2 );
|
|
407
|
+
expect( collected.every( m => m.metadata.name === 'r' ) ).toBe( true );
|
|
408
|
+
expect( collected[0].fn ).toBe( collected[1].fn );
|
|
409
|
+
} );
|
|
410
|
+
} );
|
|
411
|
+
|
|
412
|
+
describe( 'packageExposesWorkflows', () => {
|
|
413
|
+
it( 'returns true when outputai.workflows.expose is true', () => {
|
|
414
|
+
const dir = join( TEMP_BASE, `wf-proj-expose-${Date.now()}` );
|
|
415
|
+
mkdirSync( dir, { recursive: true } );
|
|
416
|
+
const pkg = join( dir, 'package.json' );
|
|
417
|
+
writeFileSync( pkg, JSON.stringify( { outputai: { workflows: { expose: true } } } ) );
|
|
418
|
+
expect( packageExposesWorkflows( pkg ) ).toBe( true );
|
|
419
|
+
} );
|
|
420
|
+
|
|
421
|
+
it( 'returns false when outputai.workflows.expose is false', () => {
|
|
422
|
+
const dir = join( TEMP_BASE, `wf-proj-no-expose-${Date.now()}` );
|
|
423
|
+
mkdirSync( dir, { recursive: true } );
|
|
424
|
+
const pkg = join( dir, 'package.json' );
|
|
425
|
+
writeFileSync( pkg, JSON.stringify( { outputai: { workflows: { expose: false } } } ) );
|
|
426
|
+
expect( packageExposesWorkflows( pkg ) ).toBe( false );
|
|
427
|
+
} );
|
|
428
|
+
|
|
429
|
+
it( 'returns false for the legacy output.workflows.expose field', () => {
|
|
430
|
+
const dir = join( TEMP_BASE, `wf-proj-legacy-expose-${Date.now()}` );
|
|
431
|
+
mkdirSync( dir, { recursive: true } );
|
|
432
|
+
const pkg = join( dir, 'package.json' );
|
|
433
|
+
writeFileSync( pkg, JSON.stringify( { output: { workflows: { expose: true } } } ) );
|
|
434
|
+
expect( packageExposesWorkflows( pkg ) ).toBe( false );
|
|
435
|
+
} );
|
|
436
|
+
|
|
437
|
+
it( 'returns false when only OutputAI dependencies are present', () => {
|
|
438
|
+
const dir = join( TEMP_BASE, `wf-proj-dep-only-${Date.now()}` );
|
|
439
|
+
mkdirSync( dir, { recursive: true } );
|
|
440
|
+
const pkg = join( dir, 'package.json' );
|
|
441
|
+
writeFileSync( pkg, JSON.stringify( { dependencies: { '@outputai/core': '1.0.0' } } ) );
|
|
442
|
+
expect( packageExposesWorkflows( pkg ) ).toBe( false );
|
|
443
|
+
} );
|
|
444
|
+
|
|
445
|
+
it( 'returns false when package.json is missing', () => {
|
|
446
|
+
expect( packageExposesWorkflows( join( TEMP_BASE, 'missing-package-json', 'package.json' ) ) ).toBe( false );
|
|
447
|
+
} );
|
|
115
448
|
|
|
116
|
-
|
|
449
|
+
it( 'returns false when package.json is not valid JSON', () => {
|
|
450
|
+
const dir = join( TEMP_BASE, `wf-proj-badjson-${Date.now()}` );
|
|
451
|
+
mkdirSync( dir, { recursive: true } );
|
|
452
|
+
const pkg = join( dir, 'package.json' );
|
|
453
|
+
writeFileSync( pkg, '{ not json' );
|
|
454
|
+
expect( packageExposesWorkflows( pkg ) ).toBe( false );
|
|
117
455
|
} );
|
|
118
456
|
} );
|
|
119
457
|
|
|
120
|
-
describe( '
|
|
121
|
-
|
|
458
|
+
describe( 'findWorkflowsInNodeModules', () => {
|
|
459
|
+
it( 'resolves node_modules from project root and finds workflows', () => {
|
|
460
|
+
const root = join( TEMP_BASE, `nm-from-root-${Date.now()}` );
|
|
461
|
+
const nm = join( root, 'node_modules' );
|
|
462
|
+
const pkg = join( nm, 'pkg_a' );
|
|
463
|
+
mkdirSync( join( pkg, 'w' ), { recursive: true } );
|
|
464
|
+
writeFileSync( join( pkg, 'package.json' ), JSON.stringify( {
|
|
465
|
+
name: 'pkg_a',
|
|
466
|
+
outputai: { workflows: { expose: true } }
|
|
467
|
+
} ) );
|
|
468
|
+
const wf = join( pkg, 'w', 'workflow.js' );
|
|
469
|
+
writeFileSync( wf, 'export default {};\n' );
|
|
470
|
+
|
|
471
|
+
const found = findWorkflowsInNodeModules( root );
|
|
472
|
+
expect( found.length ).toBe( 1 );
|
|
473
|
+
expect( found[0].path ).toBe( wf );
|
|
474
|
+
} );
|
|
475
|
+
|
|
476
|
+
it( 'finds workflow.js under an unscoped package with exposed workflows', () => {
|
|
477
|
+
const root = join( TEMP_BASE, `nm-unscoped-${Date.now()}` );
|
|
478
|
+
const nm = join( root, 'node_modules' );
|
|
479
|
+
const pkg = join( nm, 'catalog_pkg' );
|
|
480
|
+
mkdirSync( join( pkg, 'workflows', 'a' ), { recursive: true } );
|
|
481
|
+
writeFileSync( join( pkg, 'package.json' ), JSON.stringify( {
|
|
482
|
+
name: 'catalog_pkg',
|
|
483
|
+
outputai: { workflows: { expose: true } }
|
|
484
|
+
} ) );
|
|
485
|
+
writeFileSync( join( pkg, 'workflows', 'a', 'workflow.js' ), 'export default {};\n' );
|
|
486
|
+
|
|
487
|
+
const found = findWorkflowsInNodeModules( nm );
|
|
488
|
+
expect( found.length ).toBe( 1 );
|
|
489
|
+
expect( found[0].path ).toBe( join( pkg, 'workflows', 'a', 'workflow.js' ) );
|
|
490
|
+
expect( found[0].url ).toBe( pathToFileURL( join( pkg, 'workflows', 'a', 'workflow.js' ) ).href );
|
|
491
|
+
} );
|
|
492
|
+
|
|
493
|
+
it( 'finds workflow.js under a scoped package', () => {
|
|
494
|
+
const root = join( TEMP_BASE, `nm-scoped-${Date.now()}` );
|
|
495
|
+
const nm = join( root, 'node_modules' );
|
|
496
|
+
const pkg = join( nm, '@acme', 'wf_pkg' );
|
|
497
|
+
mkdirSync( join( pkg, 'lib' ), { recursive: true } );
|
|
498
|
+
writeFileSync( join( pkg, 'package.json' ), JSON.stringify( {
|
|
499
|
+
name: '@acme/wf_pkg',
|
|
500
|
+
outputai: { workflows: { expose: true } }
|
|
501
|
+
} ) );
|
|
502
|
+
writeFileSync( join( pkg, 'lib', 'workflow.js' ), 'export default {};\n' );
|
|
503
|
+
|
|
504
|
+
const found = findWorkflowsInNodeModules( nm );
|
|
505
|
+
expect( found.length ).toBe( 1 );
|
|
506
|
+
expect( found[0].path ).toBe( join( pkg, 'lib', 'workflow.js' ) );
|
|
507
|
+
} );
|
|
508
|
+
|
|
509
|
+
it( 'skips packages that do not expose workflows', () => {
|
|
510
|
+
const root = join( TEMP_BASE, `nm-skip-${Date.now()}` );
|
|
511
|
+
const nm = join( root, 'node_modules' );
|
|
512
|
+
const pkg = join( nm, 'plain_lib' );
|
|
513
|
+
mkdirSync( pkg, { recursive: true } );
|
|
514
|
+
writeFileSync( join( pkg, 'package.json' ), JSON.stringify( { name: 'plain_lib' } ) );
|
|
515
|
+
writeFileSync( join( pkg, 'workflow.js' ), 'export default {};\n' );
|
|
516
|
+
|
|
517
|
+
expect( findWorkflowsInNodeModules( nm ) ).toEqual( [] );
|
|
518
|
+
} );
|
|
519
|
+
|
|
520
|
+
it( 'deduplicates the same workflow when reachable via symlink alias and canonical package', () => {
|
|
521
|
+
const root = join( TEMP_BASE, `nm-dedupe-${Date.now()}` );
|
|
522
|
+
const nm = join( root, 'node_modules' );
|
|
523
|
+
const realPkg = join( nm, 'real_pkg' );
|
|
524
|
+
const linkPkg = join( nm, 'link_pkg' );
|
|
525
|
+
mkdirSync( join( realPkg, 'w' ), { recursive: true } );
|
|
526
|
+
const wf = join( realPkg, 'w', 'workflow.js' );
|
|
527
|
+
writeFileSync( join( realPkg, 'package.json' ), JSON.stringify( {
|
|
528
|
+
name: 'real_pkg',
|
|
529
|
+
outputai: { workflows: { expose: true } }
|
|
530
|
+
} ) );
|
|
531
|
+
writeFileSync( wf, 'export default {};\n' );
|
|
532
|
+
symlinkSync( 'real_pkg', linkPkg, 'dir' );
|
|
533
|
+
|
|
534
|
+
const found = findWorkflowsInNodeModules( nm );
|
|
535
|
+
expect( found ).toHaveLength( 1 );
|
|
536
|
+
expect( realpathSync( found[0].path ) ).toBe( wf );
|
|
537
|
+
} );
|
|
538
|
+
} );
|
|
539
|
+
|
|
540
|
+
describe( 'findSharedActivitiesFromWorkflows', () => {
|
|
541
|
+
it( 'finds shared steps and evaluators from external workflow package roots', () => {
|
|
542
|
+
const root = join( TEMP_BASE, `external-shared-${Date.now()}` );
|
|
543
|
+
const pkg = join( root, 'node_modules', '@acme', 'workflow_pkg' );
|
|
544
|
+
const workflowA = join( pkg, 'workflows', 'a', 'workflow.js' );
|
|
545
|
+
const workflowB = join( pkg, 'workflows', 'b', 'workflow.js' );
|
|
546
|
+
const sharedStep = join( pkg, 'shared', 'steps', 'prepare.js' );
|
|
547
|
+
const sharedEvaluator = join( pkg, 'shared', 'evaluators', 'quality.js' );
|
|
548
|
+
mkdirSync( dirname( workflowA ), { recursive: true } );
|
|
549
|
+
mkdirSync( dirname( workflowB ), { recursive: true } );
|
|
550
|
+
mkdirSync( dirname( sharedStep ), { recursive: true } );
|
|
551
|
+
mkdirSync( dirname( sharedEvaluator ), { recursive: true } );
|
|
552
|
+
writeFileSync( join( pkg, 'package.json' ), JSON.stringify( {
|
|
553
|
+
name: '@acme/workflow_pkg',
|
|
554
|
+
dependencies: { '@outputai/core': '1.0.0' }
|
|
555
|
+
} ) );
|
|
556
|
+
writeFileSync( workflowA, 'export default {};\n' );
|
|
557
|
+
writeFileSync( workflowB, 'export default {};\n' );
|
|
558
|
+
writeFileSync( sharedStep, 'export const Prepare = step({ name: "prepare" });\n' );
|
|
559
|
+
writeFileSync( sharedEvaluator, 'export const Quality = evaluator({ name: "quality" });\n' );
|
|
560
|
+
writeFileSync( join( pkg, 'shared', 'readme.md' ), '# ignored\n' );
|
|
561
|
+
|
|
562
|
+
const found = findSharedActivitiesFromWorkflows( [
|
|
563
|
+
{ path: workflowA },
|
|
564
|
+
{ path: workflowB },
|
|
565
|
+
{ path: join( root, 'local', 'workflow.js' ) }
|
|
566
|
+
] );
|
|
567
|
+
expect( found.map( f => f.path ).sort() ).toEqual( [ sharedEvaluator, sharedStep ].sort() );
|
|
568
|
+
} );
|
|
569
|
+
} );
|
|
570
|
+
|
|
571
|
+
describe( 'staticMatchers', () => {
|
|
572
|
+
describe( 'workflowFile', () => {
|
|
573
|
+
it( 'matches paths ending with path separator and workflow.js', () => {
|
|
574
|
+
expect( staticMatchers.workflowFile( `${sep}x${sep}y${sep}workflow.js` ) ).toBe( true );
|
|
575
|
+
} );
|
|
576
|
+
|
|
577
|
+
it( 'rejects workflow.ts', () => {
|
|
578
|
+
expect( staticMatchers.workflowFile( `${sep}a${sep}workflow.ts` ) ).toBe( false );
|
|
579
|
+
} );
|
|
580
|
+
} );
|
|
581
|
+
|
|
582
|
+
describe( 'workflowPathHasShared', () => {
|
|
583
|
+
it( 'matches workflow.js under a shared folder segment', () => {
|
|
584
|
+
expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}shared${sep}workflow.js` ) ).toBe( true );
|
|
585
|
+
} );
|
|
586
|
+
|
|
587
|
+
it( 'rejects workflow.js not under shared', () => {
|
|
588
|
+
expect( staticMatchers.workflowPathHasShared( `${sep}foo${sep}workflow.js` ) ).toBe( false );
|
|
589
|
+
} );
|
|
590
|
+
} );
|
|
591
|
+
|
|
592
|
+
describe( 'sharedStepsDir', () => {
|
|
122
593
|
it( 'matches .js files inside shared/steps/', () => {
|
|
123
594
|
expect( staticMatchers.sharedStepsDir( `${sep}app${sep}dist${sep}shared${sep}steps${sep}tools.js` ) ).toBe( true );
|
|
124
595
|
} );
|
|
@@ -136,7 +607,7 @@ describe( '.staticMatchers', () => {
|
|
|
136
607
|
} );
|
|
137
608
|
} );
|
|
138
609
|
|
|
139
|
-
describe( '
|
|
610
|
+
describe( 'sharedEvaluatorsDir', () => {
|
|
140
611
|
it( 'matches .js files inside shared/evaluators/', () => {
|
|
141
612
|
expect( staticMatchers.sharedEvaluatorsDir( `${sep}app${sep}dist${sep}shared${sep}evaluators${sep}quality.js` ) ).toBe( true );
|
|
142
613
|
} );
|