@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
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, { args: input ? [ input ] : [], memo: { executionContext, parentId: workflowId } } );
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 timestamp = new Date( startTime ).toISOString().replace( /[:T.]/g, '-' );
45
- const dir = join( process.argv[2], 'logs', 'runs', workflowName );
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, isStringboolTrue } from './tools/utils.js';
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
- console.log( 'frame', frame );
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
  };
@@ -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
+ };
@@ -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
- const __dirname = dirname( fileURLToPath( import.meta.url ) );
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
+ } );
@@ -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
 
@@ -10,7 +10,7 @@ import {
10
10
  buildSharedStepsNameMap,
11
11
  buildEvaluatorsNameMap,
12
12
  toAbsolutePath
13
- } from './tools.js';
13
+ } from '../tools.js';
14
14
  import {
15
15
  isCallExpression,
16
16
  isIdentifier,
@@ -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 './tools.js';
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 './tools.js';
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
- isArrowFunctionExpression,
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;
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { parse } from './tools.js';
2
+ import { parse } from '../tools.js';
3
3
  import rewriteFnBodies from './rewrite_fn_bodies.js';
4
4
 
5
5
  describe( 'rewrite_fn_bodies', () => {
@@ -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
+ } );
@@ -1,3 +0,0 @@
1
- export const NodeType = {
2
- CONST: 'const'
3
- };