@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,70 +1,21 @@
|
|
|
1
|
-
import { resolve, sep } from 'path';
|
|
1
|
+
import { dirname, resolve, sep } from 'path';
|
|
2
2
|
import { pathToFileURL } from 'url';
|
|
3
3
|
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
|
-
import { readdirSync } from 'fs';
|
|
4
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'fs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @property {string} path - The file path
|
|
9
|
-
* @property {string} url - The resolved url of the file, ready to be imported
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {object} Component
|
|
13
|
-
* @property {Function} fn - The loaded component function
|
|
14
|
-
* @property {object} metadata - Associated metadata with the component
|
|
15
|
-
* @property {string} path - Associated metadata with the component
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Recursive traverse directories collection files with paths that match one of the given matches.
|
|
7
|
+
* Returns the real path for symlink
|
|
20
8
|
*
|
|
21
|
-
*
|
|
22
|
-
* @param {function[]} matchers - Boolean functions to match files to add to collection
|
|
23
|
-
* @returns {CollectedFile[]} An array containing the collected files
|
|
24
|
-
*/
|
|
25
|
-
const findByNameRecursively = ( parentPath, matchers, ignoreDirNames = [ 'vendor', 'node_modules' ] ) => {
|
|
26
|
-
const collection = [];
|
|
27
|
-
for ( const entry of readdirSync( parentPath, { withFileTypes: true } ) ) {
|
|
28
|
-
if ( ignoreDirNames.includes( entry.name ) ) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const path = resolve( parentPath, entry.name );
|
|
33
|
-
if ( entry.isDirectory() ) {
|
|
34
|
-
collection.push( ...findByNameRecursively( path, matchers ) );
|
|
35
|
-
} else if ( matchers.some( m => m( path ) ) ) {
|
|
36
|
-
collection.push( { path, url: pathToFileURL( path ).href } );
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return collection;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Scan a path for files testing each path against a matching function.
|
|
45
|
-
*
|
|
46
|
-
* For each file found, dynamic import it and for each exports on that file, yields it.
|
|
9
|
+
* If the link is broken, returns null
|
|
47
10
|
*
|
|
48
|
-
* @
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @generator
|
|
52
|
-
* @async
|
|
53
|
-
* @function importComponents
|
|
54
|
-
* @param {string} target - Place to look for files
|
|
55
|
-
* @param {function[]} matchers - Boolean functions to match files
|
|
56
|
-
* @yields {Component}
|
|
11
|
+
* @param {string} link - The symlink to resolve
|
|
12
|
+
* @returns {string|null} The real path or null if it is unresolvable
|
|
57
13
|
*/
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if ( !metadata ) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
yield { fn, metadata, path };
|
|
67
|
-
}
|
|
14
|
+
export const resolveSymlink = link => {
|
|
15
|
+
try {
|
|
16
|
+
return realpathSync( link );
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
68
19
|
}
|
|
69
20
|
};
|
|
70
21
|
|
|
@@ -130,3 +81,224 @@ export const staticMatchers = {
|
|
|
130
81
|
*/
|
|
131
82
|
sharedEvaluatorsDir: v => v.includes( `${sep}shared${sep}evaluators${sep}` ) && v.endsWith( '.js' )
|
|
132
83
|
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {object} File
|
|
87
|
+
* @property {string} path - The file path
|
|
88
|
+
* @property {string} url - The resolved url of the file, ready to be imported
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* @typedef {object} Component
|
|
92
|
+
* @property {Function} fn - The loaded component function
|
|
93
|
+
* @property {object} metadata - Associated metadata with the component
|
|
94
|
+
* @property {string} path - Associated metadata with the component
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Recursive traverse directories collection files with paths that match one of the given matches.
|
|
99
|
+
*
|
|
100
|
+
* Follows symlinks to directories
|
|
101
|
+
*
|
|
102
|
+
* @param {string} parentPath - The path to scan
|
|
103
|
+
* @param {function[]} matchers - Boolean functions to match files to add to collection
|
|
104
|
+
* @returns {File[]} An array containing the collected files
|
|
105
|
+
*/
|
|
106
|
+
export const matchFiles = ( parentPath, matchers, ignoreDirNames = [ 'vendor', 'node_modules' ] ) => {
|
|
107
|
+
const collection = [];
|
|
108
|
+
for ( const entry of readdirSync( parentPath, { withFileTypes: true } ) ) {
|
|
109
|
+
if ( ignoreDirNames.includes( entry.name ) ) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const path = resolve( parentPath, entry.name );
|
|
113
|
+
const realPath = entry.isSymbolicLink() ? resolveSymlink( path ) : path;
|
|
114
|
+
if ( !realPath ) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const stat = lstatSync( realPath );
|
|
118
|
+
if ( stat.isDirectory() ) {
|
|
119
|
+
collection.push( ...matchFiles( realPath, matchers ) );
|
|
120
|
+
} else if ( stat.isFile() && matchers.some( m => m( realPath ) ) ) {
|
|
121
|
+
collection.push( { path, url: pathToFileURL( realPath ).href } );
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return collection;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Returns true if given package.json indicates that its workflows are exposed for external usage.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} pkgJsonPath
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export const packageExposesWorkflows = pkgJsonPath => {
|
|
134
|
+
if ( !existsSync( pkgJsonPath ) ) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const pkgJsonRawContent = readFileSync( pkgJsonPath );
|
|
138
|
+
try {
|
|
139
|
+
const packageContent = JSON.parse( pkgJsonRawContent );
|
|
140
|
+
return packageContent['outputai']?.workflows?.expose === true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Normalize path separators for cross-platform path matching.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} path
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
const normalizeSlashes = path => path.replace( /\\/g, '/' );
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns true if the given path is inside a node_modules tree.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} path
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
export const isPathDescendentFromNodeModules = path =>
|
|
161
|
+
/(^|\/)node_modules(\/|$)/.test( normalizeSlashes( path ) );
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns true if the given path is an installed package root inside node_modules.
|
|
165
|
+
*
|
|
166
|
+
* Matches both unscoped packages and scoped packages.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} path
|
|
169
|
+
* @returns {boolean}
|
|
170
|
+
*/
|
|
171
|
+
export const isPackageRoot = path =>
|
|
172
|
+
/\/node_modules\/(?:@[^/]+\/[^/]+|[^/@][^/]*)$/.test( normalizeSlashes( path ) );
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Walk upward from a file path to find the closest installed package root under node_modules.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} path
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
export const findPackageRoot = path => {
|
|
181
|
+
if ( isPackageRoot( path ) && existsSync( resolve( path, 'package.json' ) ) ) {
|
|
182
|
+
return path;
|
|
183
|
+
}
|
|
184
|
+
const parent = dirname( path );
|
|
185
|
+
return parent !== path ? findPackageRoot( parent ) : null;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resolves the closest node_modules directory
|
|
190
|
+
*
|
|
191
|
+
* @param {string} targetPath - A reference path to start the search, can be a dir or a file path
|
|
192
|
+
* @returns {string|null} The closest node_modules/ or null
|
|
193
|
+
*/
|
|
194
|
+
export const resolveNodeModulesPath = targetPath => {
|
|
195
|
+
if ( !existsSync( targetPath ) ) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const path = lstatSync( targetPath ).isDirectory() ? targetPath : dirname( targetPath );
|
|
199
|
+
const nodeModulesPath = resolve( path, 'node_modules' );
|
|
200
|
+
if ( existsSync( nodeModulesPath ) ) {
|
|
201
|
+
const stat = lstatSync( nodeModulesPath );
|
|
202
|
+
if ( stat.isDirectory() ) {
|
|
203
|
+
return nodeModulesPath;
|
|
204
|
+
}
|
|
205
|
+
if ( stat.isSymbolicLink() ) {
|
|
206
|
+
const symlinkTarget = resolveSymlink( nodeModulesPath );
|
|
207
|
+
if ( symlinkTarget && lstatSync( symlinkTarget ).isDirectory() ) {
|
|
208
|
+
return symlinkTarget;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const parentPath = resolve( path, '..' );
|
|
214
|
+
return parentPath !== path ? resolveNodeModulesPath( parentPath ) : null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Scans a node_modules/ path and look for all projects that contain workflows.
|
|
219
|
+
*
|
|
220
|
+
* A project contains workflows when packageExposesWorkflows() resolves to true.
|
|
221
|
+
*
|
|
222
|
+
* For each of these projects, load workflows using matchFiles().
|
|
223
|
+
*
|
|
224
|
+
* @param {string} nodeModulesPath
|
|
225
|
+
* @returns {File[]} An array containing the collected files
|
|
226
|
+
*
|
|
227
|
+
*/
|
|
228
|
+
export const findWorkflowsInPackages = nodeModulesPath => {
|
|
229
|
+
const collection = [];
|
|
230
|
+
for ( const entry of readdirSync( nodeModulesPath, { withFileTypes: true } ) ) {
|
|
231
|
+
const path = resolve( nodeModulesPath, entry.name );
|
|
232
|
+
const realPath = entry.isSymbolicLink() ? resolveSymlink( path ) : path;
|
|
233
|
+
|
|
234
|
+
if ( realPath && lstatSync( realPath ).isDirectory() ) {
|
|
235
|
+
if ( entry.name.startsWith( '@' ) ) { // scoped package root
|
|
236
|
+
collection.push( ...findWorkflowsInPackages( realPath ) );
|
|
237
|
+
} else if ( packageExposesWorkflows( resolve( realPath, 'package.json' ) ) ) { // is a package folder
|
|
238
|
+
collection.push( ...matchFiles( realPath, [ staticMatchers.workflowFile ] ) );
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return collection;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Recursive traverse the closest node_modules/ directory loading workflows.
|
|
247
|
+
*
|
|
248
|
+
* Deduplicates by file url.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} parentPath - The starting path
|
|
251
|
+
* @returns {File[]} An array containing the collected files
|
|
252
|
+
*/
|
|
253
|
+
export const findWorkflowsInNodeModules = parentPath => {
|
|
254
|
+
const nodeModulesPath = resolveNodeModulesPath( parentPath );
|
|
255
|
+
if ( !nodeModulesPath ) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const collection = findWorkflowsInPackages( nodeModulesPath );
|
|
259
|
+
|
|
260
|
+
// deduplicate collection by .url in case symlinked packages end up resolving the same dependencies more than once
|
|
261
|
+
return collection.reduce( ( map, value ) => map.set( value.url, value ), new Map() ).values().toArray();
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Based on workflow urls, traverse those projects looking for shared activities (steps, evaluators).
|
|
266
|
+
*
|
|
267
|
+
* @param {Component[]} workflows
|
|
268
|
+
* @returns {File[]}
|
|
269
|
+
*/
|
|
270
|
+
export const findSharedActivitiesFromWorkflows = workflows => {
|
|
271
|
+
const paths = workflows.map( wf => findPackageRoot( wf.path ) );
|
|
272
|
+
const uniquePaths = new Set( paths.filter( p => !!p ) ).values().toArray();
|
|
273
|
+
const files = [];
|
|
274
|
+
for ( const path of uniquePaths ) {
|
|
275
|
+
files.push( ...matchFiles( path, [ staticMatchers.sharedStepsDir, staticMatchers.sharedEvaluatorsDir ] ) );
|
|
276
|
+
}
|
|
277
|
+
return files;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Receives an array of Files and import each one.
|
|
282
|
+
*
|
|
283
|
+
* For each exported function from that file that has metadata, yields the path, metadata and the function itself.
|
|
284
|
+
*
|
|
285
|
+
* metadata is accessible thru METADATA_ACCESS_SYMBOL.
|
|
286
|
+
*
|
|
287
|
+
* @generator
|
|
288
|
+
* @async
|
|
289
|
+
* @function importComponents
|
|
290
|
+
* @param {File} Files - Collected files to load
|
|
291
|
+
* @yields {Component}
|
|
292
|
+
*/
|
|
293
|
+
export async function *importComponents( files ) {
|
|
294
|
+
for ( const { url, path } of files ) {
|
|
295
|
+
const imported = await import( url );
|
|
296
|
+
for ( const fn of Object.values( imported ) ) {
|
|
297
|
+
const metadata = fn[METADATA_ACCESS_SYMBOL];
|
|
298
|
+
if ( !metadata ) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
yield { fn, metadata, path };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|