@output.ai/core 0.1.4 → 0.1.6
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 +1 -1
- package/src/consts.js +1 -0
- package/src/interface/workflow.js +14 -2
- package/src/internal_activities/index.js +10 -1
- package/src/tracing/processors/local/index.js +21 -4
- package/src/tracing/processors/s3/index.js +11 -0
- package/src/tracing/tools/utils.js +0 -7
- package/src/tracing/trace_engine.js +2 -1
- package/src/utils/resolve_invocation_dir.js +1 -4
- package/src/utils/utils.js +7 -0
- package/src/worker/bundler_options.js +32 -0
- package/src/worker/index.js +4 -28
- package/src/worker/interceptors.js +10 -0
- package/src/worker/loader.js +4 -2
- package/src/worker/loader.spec.js +6 -2
- package/src/worker/webpack_loaders/consts.js +19 -0
- package/src/worker/webpack_loaders/{workflow_rewriter/tools.js → tools.js} +32 -1
- package/src/worker/webpack_loaders/{workflow_rewriter/tools.spec.js → tools.spec.js} +46 -1
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +1 -1
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +1 -1
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +1 -1
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +2 -6
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +1 -1
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +196 -0
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +248 -0
- package/src/worker/webpack_loaders/workflow_rewriter/consts.js +0 -3
package/package.json
CHANGED
package/src/consts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const ACTIVITY_SEND_WEBHOOK = '__internal#sendWebhook';
|
|
2
|
+
export const ACTIVITY_GET_TRACE_DESTINATIONS = '__internal#getTraceDestinations';
|
|
2
3
|
export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
3
4
|
export const SHARED_STEP_PREFIX = '__shared#';
|
|
4
5
|
export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo } from '@temporalio/workflow';
|
|
3
3
|
import { validateWorkflow } from './validations/static.js';
|
|
4
4
|
import { validateWithSchema } from './validations/runtime.js';
|
|
5
|
-
import { SHARED_STEP_PREFIX } from '#consts';
|
|
5
|
+
import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
|
|
6
6
|
import { mergeActivityOptions, resolveInvocationDir, setMetadata } from '#utils';
|
|
7
7
|
import { FatalError, ValidationError } from '#errors';
|
|
8
8
|
|
|
@@ -36,6 +36,9 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
36
36
|
|
|
37
37
|
const { workflowId, memo, startTime } = workflowInfo();
|
|
38
38
|
|
|
39
|
+
// Root workflows will not have the execution context yet, since it is set here.
|
|
40
|
+
const isRoot = !memo.executionContext;
|
|
41
|
+
|
|
39
42
|
// Create the execution context object or preserve if it already exists:
|
|
40
43
|
// It will always contains the information about the root workflow
|
|
41
44
|
// It will be used to as context for tracing (connecting events)
|
|
@@ -57,12 +60,21 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
57
60
|
invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${workflowPath}#${evaluatorName}`]( input, options ),
|
|
58
61
|
|
|
59
62
|
startWorkflow: async ( childName, input ) => {
|
|
60
|
-
return executeChild( childName, {
|
|
63
|
+
return executeChild( childName, {
|
|
64
|
+
args: input ? [ input ] : [],
|
|
65
|
+
workflowId: `${workflowId}-${childName}-${Date.now()}`,
|
|
66
|
+
memo: { executionContext, parentId: workflowId }
|
|
67
|
+
} );
|
|
61
68
|
}
|
|
62
69
|
}, input );
|
|
63
70
|
|
|
64
71
|
validateWithSchema( outputSchema, output, `Workflow ${name} output` );
|
|
65
72
|
|
|
73
|
+
if ( isRoot ) {
|
|
74
|
+
const destinations = await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( { startTime, workflowId, workflowName: name } );
|
|
75
|
+
return { output, trace: { destinations } };
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
return output;
|
|
67
79
|
};
|
|
68
80
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { FatalError } from '#errors';
|
|
2
|
-
import { setMetadata } from '#utils';
|
|
2
|
+
import { setMetadata, isStringboolTrue } from '#utils';
|
|
3
3
|
import { ComponentType } from '#consts';
|
|
4
|
+
import * as localProcessor from '../tracing/processors/local/index.js';
|
|
5
|
+
import * as s3Processor from '../tracing/processors/s3/index.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Send a post to a given URL
|
|
@@ -37,3 +39,10 @@ export const sendWebhook = async ( { url, workflowId, payload } ) => {
|
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
setMetadata( sendWebhook, { type: ComponentType.INTERNAL_STEP } );
|
|
42
|
+
|
|
43
|
+
export const getTraceDestinations = ( { startTime, workflowId, workflowName } ) => ( {
|
|
44
|
+
local: isStringboolTrue( process.env.TRACE_LOCAL_ON ) ? localProcessor.getDestination( { startTime, workflowId, workflowName } ) : null,
|
|
45
|
+
remote: isStringboolTrue( process.env.TRACE_REMOTE_ON ) ? s3Processor.getDestination( { startTime, workflowId, workflowName } ) : null
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
setMetadata( getTraceDestinations, { type: ComponentType.INTERNAL_STEP } );
|
|
@@ -27,6 +27,13 @@ export const init = () => {
|
|
|
27
27
|
cleanupOldTempFiles();
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const getOutputDir = workflowName => join( process.argv[2], 'logs', 'runs', workflowName );
|
|
31
|
+
|
|
32
|
+
const buildOutputFileName = ( { startTime, workflowId } ) => {
|
|
33
|
+
const timestamp = new Date( startTime ).toISOString().replace( /[:T.]/g, '-' );
|
|
34
|
+
return `${timestamp}_${workflowId}.json`;
|
|
35
|
+
};
|
|
36
|
+
|
|
30
37
|
/**
|
|
31
38
|
* Execute this processor:
|
|
32
39
|
*
|
|
@@ -35,16 +42,26 @@ export const init = () => {
|
|
|
35
42
|
* @param {object} args
|
|
36
43
|
* @param {object} entry - Trace event phase
|
|
37
44
|
* @param {object} executionContext - Execution info: workflowId, workflowName, startTime
|
|
38
|
-
* @returns
|
|
45
|
+
* @returns {void}
|
|
39
46
|
*/
|
|
40
47
|
export const exec = ( { entry, executionContext } ) => {
|
|
41
48
|
const { workflowId, workflowName, startTime } = executionContext;
|
|
42
49
|
const content = buildTraceTree( accumulate( { entry, executionContext } ) );
|
|
43
50
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const path = join( dir, `${timestamp}_${workflowId}.json` );
|
|
51
|
+
const dir = getOutputDir( workflowName );
|
|
52
|
+
const path = join( dir, buildOutputFileName( { startTime, workflowId } ) );
|
|
47
53
|
|
|
48
54
|
mkdirSync( dir, { recursive: true } );
|
|
49
55
|
writeFileSync( path, JSON.stringify( content, undefined, 2 ) + EOL, 'utf-8' );
|
|
50
56
|
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns where the trace is saved
|
|
60
|
+
* @param {object} args
|
|
61
|
+
* @param {string} args.startTime - The start time of the workflow
|
|
62
|
+
* @param {string} args.workflowId - The id of the workflow execution
|
|
63
|
+
* @param {string} args.workflowName - The name of the workflow
|
|
64
|
+
* @returns {string} The path where the trace will be saved
|
|
65
|
+
*/
|
|
66
|
+
export const getDestination = ( { startTime, workflowId, workflowName } ) =>
|
|
67
|
+
join( getOutputDir( workflowName ), buildOutputFileName( { workflowId, startTime } ) );
|
|
@@ -49,3 +49,14 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
49
49
|
content: JSON.stringify( content, undefined, 2 ) + EOL
|
|
50
50
|
} ) : 0;
|
|
51
51
|
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns where the trace is saved
|
|
55
|
+
* @param {object} args
|
|
56
|
+
* @param {string} args.startTime - The start time of the workflow
|
|
57
|
+
* @param {string} args.workflowId - The id of the workflow execution
|
|
58
|
+
* @param {string} args.workflowName - The name of the workflow
|
|
59
|
+
* @returns {string} The S3 url of the trace file
|
|
60
|
+
*/
|
|
61
|
+
export const getDestination = ( { startTime, workflowId, workflowName } ) =>
|
|
62
|
+
`https://${process.env.TRACE_REMOTE_S3_BUCKET}.s3.amazonaws.com/${getS3Key( { workflowId, workflowName, startTime } )}`;
|
|
@@ -19,10 +19,3 @@ export const serializeError = error =>
|
|
|
19
19
|
message: error.message,
|
|
20
20
|
stack: error.stack
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Returns true if string value is stringbool and true
|
|
25
|
-
* @param {string} v
|
|
26
|
-
* @returns
|
|
27
|
-
*/
|
|
28
|
-
export const isStringboolTrue = v => [ '1', 'true', 'on' ].includes( v );
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Storage } from '#async_storage';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { serializeError
|
|
3
|
+
import { serializeError } from './tools/utils.js';
|
|
4
|
+
import { isStringboolTrue } from '#utils';
|
|
4
5
|
import * as localProcessor from './processors/local/index.js';
|
|
5
6
|
import * as s3Processor from './processors/s3/index.js';
|
|
6
7
|
|
|
@@ -28,8 +28,5 @@ export default ( additionalIgnorePaths = [] ) => {
|
|
|
28
28
|
if ( !frame ) {
|
|
29
29
|
throw new Error( `Invocation dir resolution via stack trace failed. Stack: ${stack}` );
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
const invocationDir = frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
|
|
33
|
-
console.log( 'invocationDir', invocationDir );
|
|
34
|
-
return invocationDir;
|
|
31
|
+
return frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
|
|
35
32
|
};
|
package/src/utils/utils.js
CHANGED
|
@@ -35,3 +35,10 @@ export const mergeActivityOptions = ( base = {}, ext = {} ) =>
|
|
|
35
35
|
Object.entries( ext ).reduce( ( options, [ k, v ] ) =>
|
|
36
36
|
Object.assign( options, { [k]: typeof v === 'object' ? mergeActivityOptions( options[k], v ) : v } )
|
|
37
37
|
, clone( base ) );
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if string value is stringbool and true
|
|
41
|
+
* @param {string} v
|
|
42
|
+
* @returns
|
|
43
|
+
*/
|
|
44
|
+
export const isStringboolTrue = v => [ '1', 'true', 'on' ].includes( v );
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
5
|
+
const workerDir = __dirname; // sdk/core/src/worker
|
|
6
|
+
const interfaceDir = join( __dirname, '..', 'interface' );
|
|
7
|
+
|
|
8
|
+
export const webpackConfigHook = config => {
|
|
9
|
+
config.module = config.module ?? { };
|
|
10
|
+
config.module.rules = config.module.rules ?? [];
|
|
11
|
+
|
|
12
|
+
// Validation loader (runs first)
|
|
13
|
+
config.module.rules.push( {
|
|
14
|
+
test: /\.js$/,
|
|
15
|
+
// Exclude node_modules and internal core worker files
|
|
16
|
+
exclude: resource => /node_modules/.test( resource ) || resource.startsWith( workerDir ) || resource.startsWith( interfaceDir ),
|
|
17
|
+
enforce: 'pre',
|
|
18
|
+
use: {
|
|
19
|
+
loader: join( __dirname, './webpack_loaders/workflow_validator/index.mjs' )
|
|
20
|
+
}
|
|
21
|
+
} );
|
|
22
|
+
// Use AST-based loader for rewriting steps/workflows
|
|
23
|
+
config.module.rules.push( {
|
|
24
|
+
test: /\.js$/,
|
|
25
|
+
// Exclude node_modules and internal core worker files
|
|
26
|
+
exclude: resource => /node_modules/.test( resource ) || resource.startsWith( workerDir ) || resource.startsWith( interfaceDir ),
|
|
27
|
+
use: {
|
|
28
|
+
loader: join( __dirname, './webpack_loaders/workflow_rewriter/index.mjs' )
|
|
29
|
+
}
|
|
30
|
+
} );
|
|
31
|
+
return config;
|
|
32
|
+
};
|
package/src/worker/index.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { Worker, NativeConnection } from '@temporalio/worker';
|
|
2
2
|
import { Client } from '@temporalio/client';
|
|
3
3
|
import { WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
4
|
-
import { dirname, join } from 'path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
4
|
import { address, apiKey, maxActivities, maxWorkflows, namespace, taskQueue, catalogId } from './configs.js';
|
|
7
5
|
import { loadActivities, loadWorkflows, createWorkflowsEntryPoint } from './loader.js';
|
|
8
|
-
import { ActivityExecutionInterceptor } from './interceptors/activity.js';
|
|
9
6
|
import { sinks } from './sinks.js';
|
|
10
7
|
import { createCatalog } from './catalog_workflow/index.js';
|
|
11
8
|
import { init as initTracing } from '#tracing';
|
|
12
9
|
import { WORKFLOW_CATALOG } from '#consts';
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
import { webpackConfigHook } from './bundler_options.js';
|
|
11
|
+
import { initInterceptors } from './interceptors.js';
|
|
15
12
|
|
|
16
13
|
// Get caller directory from command line arguments
|
|
17
14
|
const callerDir = process.argv[2];
|
|
@@ -44,31 +41,10 @@ const callerDir = process.argv[2];
|
|
|
44
41
|
workflowsPath,
|
|
45
42
|
activities,
|
|
46
43
|
sinks,
|
|
47
|
-
interceptors: {
|
|
48
|
-
workflowModules: [ join( __dirname, './interceptors/workflow.js' ) ],
|
|
49
|
-
activityInbound: [ () => new ActivityExecutionInterceptor( activities ) ]
|
|
50
|
-
},
|
|
44
|
+
interceptors: initInterceptors( { activities } ),
|
|
51
45
|
maxConcurrentWorkflowTaskExecutions: maxWorkflows,
|
|
52
46
|
maxConcurrentActivityTaskExecutions: maxActivities,
|
|
53
|
-
bundlerOptions: {
|
|
54
|
-
webpackConfigHook: config => {
|
|
55
|
-
if ( !config.module ) {
|
|
56
|
-
config.module = { };
|
|
57
|
-
}
|
|
58
|
-
if ( !config.module.rules ) {
|
|
59
|
-
config.module.rules = [];
|
|
60
|
-
}
|
|
61
|
-
// Use AST-based loader for rewriting steps/workflows
|
|
62
|
-
config.module.rules.push( {
|
|
63
|
-
test: /\.js$/,
|
|
64
|
-
exclude: /node_modules/,
|
|
65
|
-
use: {
|
|
66
|
-
loader: join( __dirname, './webpack_loaders/workflow_rewriter/index.mjs' )
|
|
67
|
-
}
|
|
68
|
-
} );
|
|
69
|
-
return config;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
47
|
+
bundlerOptions: { webpackConfigHook }
|
|
72
48
|
} );
|
|
73
49
|
|
|
74
50
|
console.log( '[Core]', 'Starting catalog workflow...' );
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dirname, join } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { ActivityExecutionInterceptor } from './interceptors/activity.js';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
6
|
+
|
|
7
|
+
export const initInterceptors = ( { activities } ) => ( {
|
|
8
|
+
workflowModules: [ join( __dirname, './interceptors/workflow.js' ) ],
|
|
9
|
+
activityInbound: [ () => new ActivityExecutionInterceptor( activities ) ]
|
|
10
|
+
} );
|
package/src/worker/loader.js
CHANGED
|
@@ -2,14 +2,15 @@ import { basename, dirname, join } from 'node:path';
|
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { EOL } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { sendWebhook } from '#internal_activities';
|
|
5
|
+
import { getTraceDestinations, sendWebhook } from '#internal_activities';
|
|
6
6
|
import { importComponents } from './loader_tools.js';
|
|
7
7
|
import {
|
|
8
8
|
ACTIVITY_SEND_WEBHOOK,
|
|
9
9
|
ACTIVITY_OPTIONS_FILENAME,
|
|
10
10
|
SHARED_STEP_PREFIX,
|
|
11
11
|
WORKFLOWS_INDEX_FILENAME,
|
|
12
|
-
WORKFLOW_CATALOG
|
|
12
|
+
WORKFLOW_CATALOG,
|
|
13
|
+
ACTIVITY_GET_TRACE_DESTINATIONS
|
|
13
14
|
} from '#consts';
|
|
14
15
|
|
|
15
16
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
@@ -50,6 +51,7 @@ export async function loadActivities( target ) {
|
|
|
50
51
|
|
|
51
52
|
// system activities
|
|
52
53
|
activities[ACTIVITY_SEND_WEBHOOK] = sendWebhook;
|
|
54
|
+
activities[ACTIVITY_GET_TRACE_DESTINATIONS] = getTraceDestinations;
|
|
53
55
|
return activities;
|
|
54
56
|
};
|
|
55
57
|
|
|
@@ -2,14 +2,18 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
vi.mock( '#consts', () => ( {
|
|
4
4
|
ACTIVITY_SEND_WEBHOOK: '__internal#sendWebhook',
|
|
5
|
+
ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations',
|
|
5
6
|
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
6
7
|
WORKFLOW_CATALOG: 'catalog',
|
|
7
|
-
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js'
|
|
8
|
+
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js',
|
|
9
|
+
SHARED_STEP_PREFIX: '/shared'
|
|
8
10
|
} ) );
|
|
9
11
|
|
|
10
12
|
const sendWebhookMock = vi.fn();
|
|
13
|
+
const getTraceDestinationsMock = vi.fn();
|
|
11
14
|
vi.mock( '#internal_activities', () => ( {
|
|
12
|
-
sendWebhook: sendWebhookMock
|
|
15
|
+
sendWebhook: sendWebhookMock,
|
|
16
|
+
getTraceDestinations: getTraceDestinationsMock
|
|
13
17
|
} ) );
|
|
14
18
|
|
|
15
19
|
const importComponentsMock = vi.fn();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const NodeType = {
|
|
2
|
+
CONST: 'const'
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const ComponentFile = {
|
|
6
|
+
STEPS: 'steps',
|
|
7
|
+
SHARED_STEPS: 'shared_steps',
|
|
8
|
+
EVALUATORS: 'evaluators',
|
|
9
|
+
WORKFLOW: 'workflow'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ExtraneousFile = {
|
|
13
|
+
TYPES: 'types'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const CoreModule = {
|
|
17
|
+
LOCAL: 'local_core',
|
|
18
|
+
NPM: '@output.ai/core'
|
|
19
|
+
};
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
thisExpression,
|
|
21
21
|
isExportDefaultDeclaration
|
|
22
22
|
} from '@babel/types';
|
|
23
|
-
import { NodeType } from './consts.js';
|
|
23
|
+
import { ComponentFile, ExtraneousFile, NodeType } from './consts.js';
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Resolve a relative module specifier against a base directory.
|
|
@@ -127,6 +127,37 @@ export const isEvaluatorsPath = value => /(^|\/)evaluators\.js$/.test( value );
|
|
|
127
127
|
*/
|
|
128
128
|
export const isWorkflowPath = value => /(^|\/)workflow\.js$/.test( value );
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Check if a module specifier or request string points to types.js.
|
|
132
|
+
* @param {string} value - Module path or request string.
|
|
133
|
+
* @returns {boolean} True if it matches types.js.
|
|
134
|
+
*/
|
|
135
|
+
export const isTypesPath = value => /(^|\/)types\.js$/.test( value );
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Determine file kind based on its path.
|
|
139
|
+
* @param {string} filename
|
|
140
|
+
* @returns {'workflow'|'steps'|'shared_steps'|'evaluators'|null}
|
|
141
|
+
*/
|
|
142
|
+
export const getFileKind = path => {
|
|
143
|
+
if ( isStepsPath( path ) ) {
|
|
144
|
+
return ComponentFile.STEPS;
|
|
145
|
+
}
|
|
146
|
+
if ( isSharedStepsPath( path ) ) {
|
|
147
|
+
return ComponentFile.SHARED_STEPS;
|
|
148
|
+
}
|
|
149
|
+
if ( isEvaluatorsPath( path ) ) {
|
|
150
|
+
return ComponentFile.EVALUATORS;
|
|
151
|
+
}
|
|
152
|
+
if ( isWorkflowPath( path ) ) {
|
|
153
|
+
return ComponentFile.WORKFLOW;
|
|
154
|
+
}
|
|
155
|
+
if ( isTypesPath( path ) ) {
|
|
156
|
+
return ExtraneousFile.TYPES;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
};
|
|
160
|
+
|
|
130
161
|
/**
|
|
131
162
|
* Create a `this.method(literalName, ...args)` CallExpression.
|
|
132
163
|
* @param {string} method - Method name on `this`.
|
|
@@ -11,12 +11,16 @@ import {
|
|
|
11
11
|
getLocalNameFromDestructuredProperty,
|
|
12
12
|
toFunctionExpression,
|
|
13
13
|
isStepsPath,
|
|
14
|
+
isSharedStepsPath,
|
|
15
|
+
isEvaluatorsPath,
|
|
14
16
|
isWorkflowPath,
|
|
15
17
|
createThisMethodCall,
|
|
16
18
|
resolveNameFromOptions,
|
|
17
19
|
buildStepsNameMap,
|
|
20
|
+
buildSharedStepsNameMap,
|
|
18
21
|
buildWorkflowNameMap,
|
|
19
|
-
buildEvaluatorsNameMap
|
|
22
|
+
buildEvaluatorsNameMap,
|
|
23
|
+
getFileKind
|
|
20
24
|
} from './tools.js';
|
|
21
25
|
|
|
22
26
|
describe( 'workflow_rewriter tools', () => {
|
|
@@ -149,6 +153,22 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
149
153
|
expect( isWorkflowPath( 'steps.js' ) ).toBe( false );
|
|
150
154
|
} );
|
|
151
155
|
|
|
156
|
+
it( 'isSharedStepsPath: matches shared_steps.js at root or subpath', () => {
|
|
157
|
+
expect( isSharedStepsPath( 'shared_steps.js' ) ).toBe( true );
|
|
158
|
+
expect( isSharedStepsPath( './shared_steps.js' ) ).toBe( true );
|
|
159
|
+
expect( isSharedStepsPath( '/a/b/shared_steps.js' ) ).toBe( true );
|
|
160
|
+
expect( isSharedStepsPath( 'shared_steps.ts' ) ).toBe( false );
|
|
161
|
+
expect( isSharedStepsPath( 'evaluators.js' ) ).toBe( false );
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
it( 'isEvaluatorsPath: matches evaluators.js at root or subpath', () => {
|
|
165
|
+
expect( isEvaluatorsPath( 'evaluators.js' ) ).toBe( true );
|
|
166
|
+
expect( isEvaluatorsPath( './evaluators.js' ) ).toBe( true );
|
|
167
|
+
expect( isEvaluatorsPath( '/a/b/evaluators.js' ) ).toBe( true );
|
|
168
|
+
expect( isEvaluatorsPath( 'evaluators.ts' ) ).toBe( false );
|
|
169
|
+
expect( isEvaluatorsPath( 'steps.js' ) ).toBe( false );
|
|
170
|
+
} );
|
|
171
|
+
|
|
152
172
|
it( 'createThisMethodCall: builds this.method(\'name\', ...args) call', () => {
|
|
153
173
|
const call = createThisMethodCall( 'invoke', 'n', [ t.numericLiteral( 1 ), t.identifier( 'x' ) ] );
|
|
154
174
|
expect( t.isCallExpression( call ) ).toBe( true );
|
|
@@ -158,5 +178,30 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
158
178
|
expect( t.isStringLiteral( call.arguments[0], { value: 'n' } ) ).toBe( true );
|
|
159
179
|
expect( call.arguments.length ).toBe( 3 );
|
|
160
180
|
} );
|
|
181
|
+
|
|
182
|
+
it( 'buildSharedStepsNameMap: reads names from shared_steps module and caches result', () => {
|
|
183
|
+
const dir = mkdtempSync( join( tmpdir(), 'tools-shared-steps-' ) );
|
|
184
|
+
const stepsPath = join( dir, 'shared_steps.js' );
|
|
185
|
+
writeFileSync( stepsPath, [
|
|
186
|
+
'export const StepA = step({ name: "shared.step.a" })',
|
|
187
|
+
'export const StepB = step({ name: "shared.step.b" })'
|
|
188
|
+
].join( '\n' ) );
|
|
189
|
+
const cache = new Map();
|
|
190
|
+
const map1 = buildSharedStepsNameMap( stepsPath, cache );
|
|
191
|
+
expect( map1.get( 'StepA' ) ).toBe( 'shared.step.a' );
|
|
192
|
+
expect( map1.get( 'StepB' ) ).toBe( 'shared.step.b' );
|
|
193
|
+
expect( cache.get( stepsPath ) ).toBe( map1 );
|
|
194
|
+
const map2 = buildSharedStepsNameMap( stepsPath, cache );
|
|
195
|
+
expect( map2 ).toBe( map1 );
|
|
196
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
197
|
+
} );
|
|
198
|
+
|
|
199
|
+
it( 'getFileKind: classifies file by its path', () => {
|
|
200
|
+
expect( getFileKind( '/p/workflow.js' ) ).toBe( 'workflow' );
|
|
201
|
+
expect( getFileKind( '/p/steps.js' ) ).toBe( 'steps' );
|
|
202
|
+
expect( getFileKind( '/p/shared_steps.js' ) ).toBe( 'shared_steps' );
|
|
203
|
+
expect( getFileKind( '/p/evaluators.js' ) ).toBe( 'evaluators' );
|
|
204
|
+
expect( getFileKind( '/p/other.js' ) ).toBe( null );
|
|
205
|
+
} );
|
|
161
206
|
} );
|
|
162
207
|
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { parse } from '
|
|
5
|
+
import { parse } from '../tools.js';
|
|
6
6
|
import collectTargetImports from './collect_target_imports.js';
|
|
7
7
|
|
|
8
8
|
function makeAst( source, filename ) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
2
|
import generatorModule from '@babel/generator';
|
|
3
|
-
import { parse } from '
|
|
3
|
+
import { parse } from '../tools.js';
|
|
4
4
|
|
|
5
5
|
import rewriteFnBodies from './rewrite_fn_bodies.js';
|
|
6
6
|
import collectTargetImports from './collect_target_imports.js';
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import traverseModule from '@babel/traverse';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
isIdentifier,
|
|
5
|
-
isFunctionExpression
|
|
6
|
-
} from '@babel/types';
|
|
7
|
-
import { toFunctionExpression, createThisMethodCall } from './tools.js';
|
|
2
|
+
import { isArrowFunctionExpression, isIdentifier, isFunctionExpression } from '@babel/types';
|
|
3
|
+
import { toFunctionExpression, createThisMethodCall } from '../tools.js';
|
|
8
4
|
|
|
9
5
|
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
10
6
|
const traverse = traverseModule.default ?? traverseModule;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import traverseModule from '@babel/traverse';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { parse, toAbsolutePath, getFileKind } from '../tools.js';
|
|
4
|
+
import { ComponentFile, CoreModule, ExtraneousFile } from '../consts.js';
|
|
5
|
+
import {
|
|
6
|
+
isCallExpression,
|
|
7
|
+
isFunctionExpression,
|
|
8
|
+
isArrowFunctionExpression,
|
|
9
|
+
isIdentifier,
|
|
10
|
+
isImportDefaultSpecifier,
|
|
11
|
+
isImportSpecifier,
|
|
12
|
+
isObjectPattern,
|
|
13
|
+
isObjectProperty,
|
|
14
|
+
isStringLiteral
|
|
15
|
+
} from '@babel/types';
|
|
16
|
+
|
|
17
|
+
// Handle CJS/ESM interop for Babel packages when executed as a webpack loader
|
|
18
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if workflow dependencies
|
|
22
|
+
*/
|
|
23
|
+
const validateWorkflowImports = ( { specifier, filename } ) => {
|
|
24
|
+
const isCore = Object.values( CoreModule ).includes( specifier );
|
|
25
|
+
const isComponent = Object.values( ComponentFile ).includes( getFileKind( specifier ) );
|
|
26
|
+
const isAllowedExtraneous = getFileKind( specifier ) === ExtraneousFile.TYPES;
|
|
27
|
+
if ( !isCore && !isComponent && !isAllowedExtraneous ) {
|
|
28
|
+
throw new Error( `Invalid dependency in workflow.js: '${specifier}'. \
|
|
29
|
+
Only evaluators, steps, shared_steps, types, workflows or @output.ai/* imports are allowed in ${filename}` );
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if evaluators, steps or shared_steps import invalid dependencies
|
|
35
|
+
*/
|
|
36
|
+
const validateStepEvaluatorImports = ( { fileKind, specifier, filename } ) => {
|
|
37
|
+
if ( Object.values( ComponentFile ).includes( getFileKind( specifier ) ) ) {
|
|
38
|
+
throw new Error( `Invalid dependency in ${fileKind}.js: '${specifier}'. \
|
|
39
|
+
Steps, shared_steps, evaluators or workflows are not allowed dependencies in ${filename}` );
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate import for evaluators, steps, shared_steps, workflow
|
|
45
|
+
*/
|
|
46
|
+
const executeImportValidations = ( { fileKind, specifier, filename } ) => {
|
|
47
|
+
if ( fileKind === ComponentFile.WORKFLOW ) {
|
|
48
|
+
validateWorkflowImports( { fileKind, specifier, filename } );
|
|
49
|
+
} else if ( Object.values( ComponentFile ).includes( fileKind ) ) {
|
|
50
|
+
validateStepEvaluatorImports( { fileKind, specifier, filename } );
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Webpack loader that validates imports and disallowed calls across modules.
|
|
56
|
+
* Returns the source unchanged unless a validation error is found.
|
|
57
|
+
*
|
|
58
|
+
* Rules enforced:
|
|
59
|
+
* - evaluators.js `fn`: at each evaluator().fn body: calling any evaluator, step, shared_step or workflow is forbidden
|
|
60
|
+
* - evaluators.js: may not import evaluators.js, steps.js/shared_steps.js, workflow.js
|
|
61
|
+
* - shared_steps.js `fn`: at each step().fn body: calling any evaluator, step, shared_step or workflow is forbidden
|
|
62
|
+
* - shared_steps.js: may not import evaluators.js, steps.js, shared_steps.js, workflow.js
|
|
63
|
+
* - steps.js: at each step().fn body: calling any evaluator, step, shared_step or workflow is forbidden
|
|
64
|
+
* - steps.js: may not import evaluators.js, steps.js, shared_steps.js, workflow.js
|
|
65
|
+
* - workflow.js: may only import components: evaluators.js, steps.js, shared_steps.js, workflow.js; and files: types.js or `@output.ai/core`
|
|
66
|
+
*
|
|
67
|
+
* @param {string|Buffer} source
|
|
68
|
+
* @param {any} inputMap
|
|
69
|
+
* @this {import('webpack').LoaderContext<{}>}
|
|
70
|
+
*/
|
|
71
|
+
export default function workflowValidatorLoader( source, inputMap ) {
|
|
72
|
+
this.cacheable?.( true );
|
|
73
|
+
const callback = this.async?.() ?? this.callback;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const filename = this.resourcePath;
|
|
77
|
+
const fileDir = dirname( filename );
|
|
78
|
+
const ast = parse( String( source ), filename );
|
|
79
|
+
|
|
80
|
+
const fileKind = getFileKind( filename );
|
|
81
|
+
|
|
82
|
+
// Collect local declarations and imported identifiers by type
|
|
83
|
+
const localStepIds = new Set();
|
|
84
|
+
const localEvaluatorIds = new Set();
|
|
85
|
+
const importedStepIds = new Set(); // includes shared_steps
|
|
86
|
+
const importedEvaluatorIds = new Set();
|
|
87
|
+
const importedWorkflowIds = new Set();
|
|
88
|
+
|
|
89
|
+
// First pass: module-level import validation + collect imported ids
|
|
90
|
+
traverse( ast, {
|
|
91
|
+
ImportDeclaration: path => {
|
|
92
|
+
const specifier = path.node.source.value;
|
|
93
|
+
|
|
94
|
+
executeImportValidations( { fileKind, specifier, filename } );
|
|
95
|
+
|
|
96
|
+
// Collect imported identifiers for later call checks
|
|
97
|
+
const accumulator = ( {
|
|
98
|
+
[ComponentFile.STEPS]: importedStepIds,
|
|
99
|
+
[ComponentFile.SHARED_STEPS]: importedStepIds,
|
|
100
|
+
[ComponentFile.EVALUATORS]: importedEvaluatorIds,
|
|
101
|
+
[ComponentFile.WORKFLOW]: importedWorkflowIds
|
|
102
|
+
} )[fileKind];
|
|
103
|
+
if ( accumulator ) {
|
|
104
|
+
for ( const s of path.node.specifiers ) {
|
|
105
|
+
if ( isImportSpecifier( s ) || isImportDefaultSpecifier( s ) ) {
|
|
106
|
+
accumulator.add( s.local.name );
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
VariableDeclarator: path => {
|
|
112
|
+
const init = path.node.init;
|
|
113
|
+
if ( !isCallExpression( init ) ) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Collect local step/evaluator declarations: const X = step({...}) / evaluator({...})
|
|
118
|
+
if ( isIdentifier( init.callee, { name: 'step' } ) && isIdentifier( path.node.id ) ) {
|
|
119
|
+
localStepIds.add( path.node.id.name );
|
|
120
|
+
}
|
|
121
|
+
if ( isIdentifier( init.callee, { name: 'evaluator' } ) && isIdentifier( path.node.id ) ) {
|
|
122
|
+
localEvaluatorIds.add( path.node.id.name );
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// CommonJS requires: validate source and collect identifiers
|
|
126
|
+
if ( isIdentifier( init.callee, { name: 'require' } ) ) {
|
|
127
|
+
const firstArg = init.arguments[0];
|
|
128
|
+
if ( !isStringLiteral( firstArg ) ) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const req = firstArg.value;
|
|
132
|
+
executeImportValidations( { fileKind, specifier: req, filename } );
|
|
133
|
+
|
|
134
|
+
// Collect imported identifiers from require patterns
|
|
135
|
+
if ( isStringLiteral( firstArg ) ) {
|
|
136
|
+
const reqType = getFileKind( toAbsolutePath( fileDir, req ) );
|
|
137
|
+
if ( [ ComponentFile.STEPS, ComponentFile.SHARED_STEPS ].includes( reqType ) && isObjectPattern( path.node.id ) ) {
|
|
138
|
+
for ( const prop of path.node.id.properties ) {
|
|
139
|
+
if ( isObjectProperty( prop ) && isIdentifier( prop.value ) ) {
|
|
140
|
+
importedStepIds.add( prop.value.name );
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if ( reqType === ComponentFile.EVALUATORS && isObjectPattern( path.node.id ) ) {
|
|
145
|
+
for ( const prop of path.node.id.properties ) {
|
|
146
|
+
if ( isObjectProperty( prop ) && isIdentifier( prop.value ) ) {
|
|
147
|
+
importedEvaluatorIds.add( prop.value.name );
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if ( reqType === ComponentFile.WORKFLOW && isIdentifier( path.node.id ) ) {
|
|
152
|
+
importedWorkflowIds.add( path.node.id.name );
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
// Function-body call validations for steps/evaluators files
|
|
160
|
+
if ( [ ComponentFile.STEPS, ComponentFile.SHARED_STEPS, ComponentFile.EVALUATORS ].includes( fileKind ) ) {
|
|
161
|
+
traverse( ast, {
|
|
162
|
+
ObjectProperty: path => {
|
|
163
|
+
if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const val = path.node.value;
|
|
167
|
+
if ( !isFunctionExpression( val ) && !isArrowFunctionExpression( val ) ) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
path.get( 'value' ).traverse( {
|
|
172
|
+
CallExpression: cPath => {
|
|
173
|
+
const callee = cPath.node.callee;
|
|
174
|
+
if ( isIdentifier( callee ) ) {
|
|
175
|
+
const { name } = callee;
|
|
176
|
+
const violation = [
|
|
177
|
+
[ 'step', localStepIds.has( name ) || importedStepIds.has( name ) ],
|
|
178
|
+
[ 'evaluator', localEvaluatorIds.has( name ) || importedEvaluatorIds.has( name ) ],
|
|
179
|
+
[ 'workflow', importedWorkflowIds.has( name ) ]
|
|
180
|
+
].find( v => v[1] )?.[0];
|
|
181
|
+
|
|
182
|
+
if ( violation ) {
|
|
183
|
+
throw new Error( `Invalid call in ${fileKind}.js fn: calling a ${violation} ('${name}') is not allowed in ${filename}` );
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} );
|
|
188
|
+
}
|
|
189
|
+
} );
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return callback( null, source, inputMap );
|
|
193
|
+
} catch ( err ) {
|
|
194
|
+
return callback( err );
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import validatorLoader from './index.mjs';
|
|
6
|
+
|
|
7
|
+
function runLoader( filename, source ) {
|
|
8
|
+
return new Promise( ( resolve, reject ) => {
|
|
9
|
+
const ctx = {
|
|
10
|
+
resourcePath: filename,
|
|
11
|
+
cacheable: () => {},
|
|
12
|
+
async: () => ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map } ) ),
|
|
13
|
+
callback: ( err, code, map ) => ( err ? reject( err ) : resolve( { code, map } ) )
|
|
14
|
+
};
|
|
15
|
+
validatorLoader.call( ctx, source, null );
|
|
16
|
+
} );
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe( 'workflow_validator loader', () => {
|
|
20
|
+
it( 'workflow.js: allows imports from steps/shared_steps/evaluators/workflow', async () => {
|
|
21
|
+
const dir = mkdtempSync( join( tmpdir(), 'wf-allow-' ) );
|
|
22
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const S = step({ name: "s" })\n' );
|
|
23
|
+
writeFileSync( join( dir, 'shared_steps.js' ), 'export const SS = step({ name: "ss" })\n' );
|
|
24
|
+
writeFileSync( join( dir, 'evaluators.js' ), 'export const E = evaluator({ name: "e" })\n' );
|
|
25
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export const W = workflow({ name: "w" })\n' );
|
|
26
|
+
|
|
27
|
+
const src = [
|
|
28
|
+
'import { S } from "./steps.js";',
|
|
29
|
+
'import { SS } from "./shared_steps.js";',
|
|
30
|
+
'import { E } from "./evaluators.js";',
|
|
31
|
+
'import { W } from "./workflow.js";',
|
|
32
|
+
'const x = 1;'
|
|
33
|
+
].join( '\n' );
|
|
34
|
+
|
|
35
|
+
await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
|
|
36
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'workflow.js: rejects external dependencies', async () => {
|
|
40
|
+
const dir = mkdtempSync( join( tmpdir(), 'wf-reject-' ) );
|
|
41
|
+
const src = 'import x from "./utils.js";';
|
|
42
|
+
await expect( runLoader( join( dir, 'workflow.js' ), src ) ).rejects.toThrow( /Invalid (import|dependency) in workflow\.js/ );
|
|
43
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
it( 'workflow.js: allows imports from @output.ai/core and local_core', async () => {
|
|
47
|
+
const dir = mkdtempSync( join( tmpdir(), 'wf-allow-external-' ) );
|
|
48
|
+
const src = [
|
|
49
|
+
'import a from "@output.ai/core";',
|
|
50
|
+
'import b from "local_core";',
|
|
51
|
+
'const z = 1;'
|
|
52
|
+
].join( '\n' );
|
|
53
|
+
await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
|
|
54
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'steps.js: rejects importing steps/shared_steps/evaluators/workflow', async () => {
|
|
58
|
+
const dir = mkdtempSync( join( tmpdir(), 'steps-reject-' ) );
|
|
59
|
+
const src = 'import { S } from "./steps.js";';
|
|
60
|
+
await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
|
|
61
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
it( 'steps.js: allows other imports', async () => {
|
|
65
|
+
const dir = mkdtempSync( join( tmpdir(), 'steps-allow-' ) );
|
|
66
|
+
const src = 'import x from "./util.js";\nconst obj = { fn: () => 1 };';
|
|
67
|
+
await expect( runLoader( join( dir, 'steps.js' ), src ) ).resolves.toBeTruthy();
|
|
68
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
it( 'evaluators.js: rejects importing evaluators/steps/shared_steps/workflow', async () => {
|
|
72
|
+
const dir = mkdtempSync( join( tmpdir(), 'evals-reject-' ) );
|
|
73
|
+
const src = 'import { E } from "./evaluators.js";';
|
|
74
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
|
|
75
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'steps.js: rejects calling another step/evaluator/workflow inside fn', async () => {
|
|
79
|
+
const dir = mkdtempSync( join( tmpdir(), 'steps-call-reject-' ) );
|
|
80
|
+
const src = [
|
|
81
|
+
'const A = step({ name: "a" });',
|
|
82
|
+
'const B = step({ name: "b" });',
|
|
83
|
+
'const obj = { fn: function() { B(); } };'
|
|
84
|
+
].join( '\n' );
|
|
85
|
+
await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
|
|
86
|
+
|
|
87
|
+
const src2 = [
|
|
88
|
+
'const E = evaluator({ name: "e" });',
|
|
89
|
+
'const obj = { fn: () => { E(); } };'
|
|
90
|
+
].join( '\n' );
|
|
91
|
+
await expect( runLoader( join( dir, 'steps.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
|
|
92
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'evaluators.js: rejects calling another evaluator/step/workflow inside fn', async () => {
|
|
96
|
+
const dir = mkdtempSync( join( tmpdir(), 'evals-call-reject-' ) );
|
|
97
|
+
const src = [
|
|
98
|
+
'const E1 = evaluator({ name: "e1" });',
|
|
99
|
+
'const E2 = evaluator({ name: "e2" });',
|
|
100
|
+
'const obj = { fn: function() { E2(); } };'
|
|
101
|
+
].join( '\n' );
|
|
102
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
|
|
103
|
+
|
|
104
|
+
const src2 = [
|
|
105
|
+
'const S = step({ name: "s" });',
|
|
106
|
+
'const obj = { fn: () => { S(); } };'
|
|
107
|
+
].join( '\n' );
|
|
108
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
|
|
109
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'steps.js/evaluators.js: allows calling unrelated local functions in fn', async () => {
|
|
113
|
+
const dir = mkdtempSync( join( tmpdir(), 'fn-allow-' ) );
|
|
114
|
+
const stepsSrc = [
|
|
115
|
+
'function helper() { return 1; }',
|
|
116
|
+
'const obj = { fn: function() { helper(); } };'
|
|
117
|
+
].join( '\n' );
|
|
118
|
+
await expect( runLoader( join( dir, 'steps.js' ), stepsSrc ) ).resolves.toBeTruthy();
|
|
119
|
+
|
|
120
|
+
const evalsSrc = [
|
|
121
|
+
'function helper() { return 1; }',
|
|
122
|
+
'const obj = { fn: () => { helper(); } };'
|
|
123
|
+
].join( '\n' );
|
|
124
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), evalsSrc ) ).resolves.toBeTruthy();
|
|
125
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
it( 'workflow.js: allows require from steps/shared_steps/evaluators/workflow; rejects others', async () => {
|
|
129
|
+
const dir = mkdtempSync( join( tmpdir(), 'wf-req-' ) );
|
|
130
|
+
writeFileSync( join( dir, 'steps.js' ), 'export const S = step({ name: "s" })\n' );
|
|
131
|
+
writeFileSync( join( dir, 'shared_steps.js' ), 'export const SS = step({ name: "ss" })\n' );
|
|
132
|
+
writeFileSync( join( dir, 'evaluators.js' ), 'export const E = evaluator({ name: "e" })\n' );
|
|
133
|
+
writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: "w" })\n' );
|
|
134
|
+
const ok = [
|
|
135
|
+
'const { S } = require("./steps.js");',
|
|
136
|
+
'const { SS } = require("./shared_steps.js");',
|
|
137
|
+
'const { E } = require("./evaluators.js");',
|
|
138
|
+
'const W = require("./workflow.js");'
|
|
139
|
+
].join( '\n' );
|
|
140
|
+
await expect( runLoader( join( dir, 'workflow.js' ), ok ) ).resolves.toBeTruthy();
|
|
141
|
+
const bad = 'const X = require("./util.js");';
|
|
142
|
+
await expect( runLoader( join( dir, 'workflow.js' ), bad ) ).rejects.toThrow( /Invalid (require|dependency) in workflow\.js/ );
|
|
143
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
it( 'steps.js: rejects importing shared_steps/evaluators/workflow variants', async () => {
|
|
147
|
+
const dir = mkdtempSync( join( tmpdir(), 'steps-reject2-' ) );
|
|
148
|
+
await expect( runLoader( join( dir, 'steps.js' ), 'import { SS } from "./shared_steps.js";' ) )
|
|
149
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
|
|
150
|
+
await expect( runLoader( join( dir, 'steps.js' ), 'import { E } from "./evaluators.js";' ) )
|
|
151
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
|
|
152
|
+
await expect( runLoader( join( dir, 'steps.js' ), 'import WF from "./workflow.js";' ) )
|
|
153
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
|
|
154
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
155
|
+
} );
|
|
156
|
+
|
|
157
|
+
it( 'evaluators.js: rejects importing steps/shared_steps/workflow variants', async () => {
|
|
158
|
+
const dir = mkdtempSync( join( tmpdir(), 'evals-reject2-' ) );
|
|
159
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), 'import { S } from "./steps.js";' ) )
|
|
160
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
|
|
161
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), 'import { SS } from "./shared_steps.js";' ) )
|
|
162
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
|
|
163
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), 'import WF from "./workflow.js";' ) )
|
|
164
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
|
|
165
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
166
|
+
} );
|
|
167
|
+
|
|
168
|
+
it( 'top-level calls outside fn are allowed in steps.js and evaluators.js', async () => {
|
|
169
|
+
const dir = mkdtempSync( join( tmpdir(), 'toplevel-allowed-' ) );
|
|
170
|
+
const stepsTop = [ 'const A = step({ name: "a" });', 'A();' ].join( '\n' );
|
|
171
|
+
await expect( runLoader( join( dir, 'steps.js' ), stepsTop ) ).resolves.toBeTruthy();
|
|
172
|
+
const evaluatorsTop = [ 'const E = evaluator({ name: "e" });', 'E();' ].join( '\n' );
|
|
173
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), evaluatorsTop ) ).resolves.toBeTruthy();
|
|
174
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
175
|
+
} );
|
|
176
|
+
|
|
177
|
+
it( 'shared_steps.js: rejects importing steps/shared_steps/evaluators/workflow', async () => {
|
|
178
|
+
const dir = mkdtempSync( join( tmpdir(), 'shared-imp-reject-' ) );
|
|
179
|
+
await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { S } from "./steps.js";' ) )
|
|
180
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
|
|
181
|
+
await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { SS } from "./shared_steps.js";' ) )
|
|
182
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
|
|
183
|
+
await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { E } from "./evaluators.js";' ) )
|
|
184
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
|
|
185
|
+
await expect( runLoader( join( dir, 'shared_steps.js' ), 'import WF from "./workflow.js";' ) )
|
|
186
|
+
.rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
|
|
187
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
it( 'shared_steps.js: rejects calling step/evaluator/workflow inside fn', async () => {
|
|
191
|
+
const dir = mkdtempSync( join( tmpdir(), 'shared-call-reject-' ) );
|
|
192
|
+
const src1 = [
|
|
193
|
+
'const A = step({ name: "a" });',
|
|
194
|
+
'const obj = { fn: function() { A(); } };'
|
|
195
|
+
].join( '\n' );
|
|
196
|
+
await expect( runLoader( join( dir, 'shared_steps.js' ), src1 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
|
|
197
|
+
|
|
198
|
+
const src2 = [
|
|
199
|
+
'const E = evaluator({ name: "e" });',
|
|
200
|
+
'const obj = { fn: () => { E(); } };'
|
|
201
|
+
].join( '\n' );
|
|
202
|
+
await expect( runLoader( join( dir, 'shared_steps.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
|
|
203
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
it( 'shared_steps.js: top-level calls outside fn are allowed', async () => {
|
|
207
|
+
const dir = mkdtempSync( join( tmpdir(), 'shared-toplevel-allowed-' ) );
|
|
208
|
+
const src = [ 'const A = step({ name: "a" });', 'A();' ].join( '\n' );
|
|
209
|
+
await expect( runLoader( join( dir, 'shared_steps.js' ), src ) ).resolves.toBeTruthy();
|
|
210
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
211
|
+
} );
|
|
212
|
+
|
|
213
|
+
it( 'workflow.js: allows importing ./types.js and bare types', async () => {
|
|
214
|
+
const dir = mkdtempSync( join( tmpdir(), 'wf-types-allow-' ) );
|
|
215
|
+
writeFileSync( join( dir, 'types.js' ), 'export const T = {}\n' );
|
|
216
|
+
const src1 = 'import { T } from "./types.js";';
|
|
217
|
+
await expect( runLoader( join( dir, 'workflow.js' ), src1 ) ).resolves.toBeTruthy();
|
|
218
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
219
|
+
} );
|
|
220
|
+
|
|
221
|
+
it( 'steps.js: rejects require of steps/shared_steps/evaluators/workflow; allows other require', async () => {
|
|
222
|
+
const dir = mkdtempSync( join( tmpdir(), 'steps-require-' ) );
|
|
223
|
+
await expect( runLoader( join( dir, 'steps.js' ), 'const { S } = require("./steps.js");' ) )
|
|
224
|
+
.rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
|
|
225
|
+
await expect( runLoader( join( dir, 'steps.js' ), 'const { SS } = require("./shared_steps.js");' ) )
|
|
226
|
+
.rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
|
|
227
|
+
await expect( runLoader( join( dir, 'steps.js' ), 'const { E } = require("./evaluators.js");' ) )
|
|
228
|
+
.rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
|
|
229
|
+
await expect( runLoader( join( dir, 'steps.js' ), 'const W = require("./workflow.js");' ) )
|
|
230
|
+
.rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
|
|
231
|
+
const ok = 'const util = require("./util.js");';
|
|
232
|
+
await expect( runLoader( join( dir, 'steps.js' ), ok ) ).resolves.toBeTruthy();
|
|
233
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
234
|
+
} );
|
|
235
|
+
|
|
236
|
+
it( 'evaluators.js: rejects require of steps/shared_steps/workflow; allows other require', async () => {
|
|
237
|
+
const dir = mkdtempSync( join( tmpdir(), 'evals-require-' ) );
|
|
238
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), 'const { S } = require("./steps.js");' ) )
|
|
239
|
+
.rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
|
|
240
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), 'const { SS } = require("./shared_steps.js");' ) )
|
|
241
|
+
.rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
|
|
242
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), 'const W = require("./workflow.js");' ) )
|
|
243
|
+
.rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
|
|
244
|
+
const ok = 'const util = require("./util.js");';
|
|
245
|
+
await expect( runLoader( join( dir, 'evaluators.js' ), ok ) ).resolves.toBeTruthy();
|
|
246
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
247
|
+
} );
|
|
248
|
+
} );
|