@output.ai/core 0.1.1 → 0.1.2
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/README.md +98 -8
- package/package.json +2 -1
- package/src/consts.js +2 -0
- package/src/index.d.ts +36 -4
- package/src/interface/evaluator.js +4 -4
- package/src/interface/step.js +4 -4
- package/src/interface/validations/static.js +16 -2
- package/src/interface/validations/static.spec.js +20 -0
- package/src/interface/workflow.js +18 -12
- package/src/interface/zod_integration.spec.js +6 -6
- package/src/internal_activities/index.js +1 -1
- package/src/utils.js +29 -0
- package/src/utils.spec.js +60 -0
- package/src/worker/interceptors/workflow.js +10 -1
- package/src/worker/loader.js +31 -4
- package/src/worker/loader.spec.js +14 -3
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +38 -20
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -4
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +48 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +16 -20
- package/src/worker/webpack_loaders/workflow_rewriter/tools.js +23 -0
- package/src/interface/metadata.js +0 -4
package/README.md
CHANGED
|
@@ -4,24 +4,30 @@ Provides tools to develop and run a workflow, which is a well defined logical un
|
|
|
4
4
|
|
|
5
5
|
## Structure
|
|
6
6
|
|
|
7
|
-
Workflows are defined using core functions "workflow"
|
|
7
|
+
Workflows are defined using core functions ("workflow", "step", "evaluator"), these are defined in separate files, and must be placed within the same folder:
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
└ workflows
|
|
11
11
|
└ example
|
|
12
12
|
├ workflow.ts|js <- workflow entry point
|
|
13
|
-
├ steps.ts|js <- file
|
|
13
|
+
├ steps.ts|js <- file containing steps used by the workflow
|
|
14
|
+
├ evaluators.ts|js <- file containing evaluating functions
|
|
14
15
|
└ prompt.prompt <- a prompt file
|
|
15
16
|
└ other-example
|
|
16
17
|
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
Workflows are the orchestrator and steps are executors. So the workflow only call the steps and the steps call the IO operations, like APIs, DBs, LLMs, etc. Evaluators are just another different flavor for steps, they work the same, but must return an `EvaluationResult` object.
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
+
## Components
|
|
22
23
|
|
|
23
|
-
###
|
|
24
|
+
### Workflow
|
|
24
25
|
|
|
26
|
+
The main code, must contain only deterministic orchestration code.
|
|
27
|
+
|
|
28
|
+
File: `workflow.js`
|
|
29
|
+
|
|
30
|
+
Example:
|
|
25
31
|
```js
|
|
26
32
|
import { workflow, z } from '@output.ai/workflow';
|
|
27
33
|
import { guessByName } from './steps.js';
|
|
@@ -42,8 +48,13 @@ export default workflow( {
|
|
|
42
48
|
})
|
|
43
49
|
```
|
|
44
50
|
|
|
45
|
-
###
|
|
51
|
+
### Step
|
|
52
|
+
|
|
53
|
+
Re-usable units of work that can contain IO, used by the workflow.
|
|
46
54
|
|
|
55
|
+
File: `steps.js`
|
|
56
|
+
|
|
57
|
+
Example:
|
|
47
58
|
```js
|
|
48
59
|
import { api } from './api.js'
|
|
49
60
|
|
|
@@ -58,9 +69,73 @@ export const guessByName = step( {
|
|
|
58
69
|
} )
|
|
59
70
|
```
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
### Shared Steps
|
|
73
|
+
|
|
74
|
+
By default, steps are exclusive to the workflow, so it is not passible to use these steps from elsewhere. In order to have shared steps and make them accessible in different workflows, create a shared steps file. This file can be relatively imported anywhere.
|
|
75
|
+
|
|
76
|
+
File: `shared_steps.js`
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```js
|
|
80
|
+
export const mySharedStep = step( {
|
|
81
|
+
name: 'mySharedStep',
|
|
82
|
+
...
|
|
83
|
+
} )
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
And the usage is the same as any step:
|
|
87
|
+
`workflow.js`
|
|
88
|
+
```js
|
|
89
|
+
import { mySharedStep } from '../../tools/shared_steps.js'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Evaluators
|
|
93
|
+
|
|
94
|
+
Steps that analyze LLM response, or take other measurements are contained in evaluators.
|
|
95
|
+
|
|
96
|
+
File: `evaluators.js`
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
```js
|
|
100
|
+
import { evaluator, EvaluationStringResult } from './api.js'
|
|
101
|
+
|
|
102
|
+
export const judgeResult = evaluator( {
|
|
103
|
+
name: 'judgeResult',
|
|
104
|
+
inputSchema: z.string(),
|
|
105
|
+
fn: async name => {
|
|
106
|
+
...
|
|
107
|
+
return new EvaluationStringResult({
|
|
108
|
+
value: 'good',
|
|
109
|
+
confidence: .95
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
} )
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Its usage is the same as steps:
|
|
116
|
+
`workflow.js`
|
|
117
|
+
```js
|
|
118
|
+
import { workflow, z } from '@output.ai/workflow';
|
|
119
|
+
import { judgeResult } from './evaluators.js';
|
|
62
120
|
|
|
63
|
-
|
|
121
|
+
export default workflow( {
|
|
122
|
+
name: 'guessMyProfession',
|
|
123
|
+
inputSchema: z.object( {
|
|
124
|
+
name: z.string()
|
|
125
|
+
} ),
|
|
126
|
+
outputSchema: z.object( {
|
|
127
|
+
result: z.string()
|
|
128
|
+
} ),
|
|
129
|
+
fn: async input => {
|
|
130
|
+
const judgment = await judgeResult( input.name );
|
|
131
|
+
return { result: judgement.value };
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Webhooks
|
|
137
|
+
|
|
138
|
+
Workflows can call webhooks that will stop their execution until an answer is given back.
|
|
64
139
|
|
|
65
140
|
```js
|
|
66
141
|
import { workflow, createWebhook } from '@output.ai/workflow';
|
|
@@ -107,6 +182,21 @@ POST http://locahost:3001/workflow/feedback
|
|
|
107
182
|
}
|
|
108
183
|
```
|
|
109
184
|
|
|
185
|
+
## Options
|
|
186
|
+
|
|
187
|
+
All core interface functions: workflow, step, evaluator have similar signature, with the following options:
|
|
188
|
+
- name: The function name, used to call it internally and identify it in the trace files, must be a code friendly string;
|
|
189
|
+
- description: Human description of the workflow/step, used for the catalog;
|
|
190
|
+
- inputSchema: a zod object indicating the type of the argument received by the `fn` function. It is validated. Omit if it doesn't have input arguments;
|
|
191
|
+
- outputSchema: a zod object indicating the type of that the `fn` function returns. It is validated. Omit if it is void. Evaluators do not have this option, since they must always return an EvaluationResult object;
|
|
192
|
+
- fn: The actual implementation of the workflow/step, including all its logic.
|
|
193
|
+
- options: Advanced options that will overwrite Temporal's ActivityOptions when calling activities.
|
|
194
|
+
|
|
195
|
+
If used on `workflow()` it will apply for all activities. If used on `step()` or `evaluator()` it will apply only to that underlying activity. If changed in both places, the end value will be a merge between the initial values, workflow values and the step values.
|
|
196
|
+
|
|
197
|
+
Order of precedence
|
|
198
|
+
`step options > workflow options > default options`
|
|
199
|
+
|
|
110
200
|
## Developing
|
|
111
201
|
|
|
112
202
|
To develop workflows you need the code, which will be called the worker, the API and the engine (Temporal).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@output.ai/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"#utils": "./src/utils.js",
|
|
43
43
|
"#tracing": "./src/tracing/internal_interface.js",
|
|
44
44
|
"#async_storage": "./src/async_storage.js",
|
|
45
|
+
"#temporal_options": "./src/temporal_options.js",
|
|
45
46
|
"#internal_activities": "./src/internal_activities/index.js"
|
|
46
47
|
}
|
|
47
48
|
}
|
package/src/consts.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const ACTIVITY_SEND_WEBHOOK = '__internal#sendWebhook';
|
|
2
2
|
export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
3
|
+
export const SHARED_STEP_PREFIX = '__shared#';
|
|
3
4
|
export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
|
|
5
|
+
export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
|
|
4
6
|
export const WORKFLOW_CATALOG = '$catalog';
|
|
5
7
|
export const ComponentType = {
|
|
6
8
|
EVALUATOR: 'evaluator',
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Import Zod types for dual schema support
|
|
2
2
|
import type { z } from 'zod';
|
|
3
|
+
import type { ActivityOptions } from '@temporalio/workflow';
|
|
3
4
|
|
|
4
5
|
// Re-export Zod for consumers to use
|
|
5
6
|
export { z } from 'zod';
|
|
@@ -19,6 +20,17 @@ export { z } from 'zod';
|
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
21
|
type AnyZodSchema = z.ZodType<any, any, any>;
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Activity retry options accepted by step/evaluator/workflow definitions.
|
|
25
|
+
* Uses Temporal's ActivityOptions['retry'] type.
|
|
26
|
+
*/
|
|
27
|
+
export type Options = { retry?: ActivityOptions['retry'] };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} Options
|
|
31
|
+
* @property {import('@temporalio/workflow').ActivityOptions['retry']} [retry]
|
|
32
|
+
*/
|
|
33
|
+
|
|
22
34
|
/*
|
|
23
35
|
╭─────────╮
|
|
24
36
|
│ S T E P │╮
|
|
@@ -43,6 +55,7 @@ type AnyZodSchema = z.ZodType<any, any, any>;
|
|
|
43
55
|
* @param {z.ZodType} options.inputSchema - Zod schema for the fn input
|
|
44
56
|
* @param {z.ZodType} options.outputSchema - Zod schema for the fn output
|
|
45
57
|
* @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
58
|
+
* @param {Options} [options.options] - Activity retry options
|
|
46
59
|
* @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
47
60
|
*/
|
|
48
61
|
export async function step<
|
|
@@ -54,6 +67,7 @@ export async function step<
|
|
|
54
67
|
inputSchema: InputSchema;
|
|
55
68
|
outputSchema: OutputSchema;
|
|
56
69
|
fn: ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>;
|
|
70
|
+
options?: Options;
|
|
57
71
|
} ): ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>;
|
|
58
72
|
|
|
59
73
|
/**
|
|
@@ -64,6 +78,7 @@ export async function step<
|
|
|
64
78
|
* @param {string} [options.description] - Description of the step
|
|
65
79
|
* @param {z.ZodType} options.inputSchema - Zod schema for the fn input
|
|
66
80
|
* @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<void>`
|
|
81
|
+
* @param {Options} [options.options] - Activity retry options
|
|
67
82
|
* @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<void>`
|
|
68
83
|
*/
|
|
69
84
|
export async function step<
|
|
@@ -73,6 +88,7 @@ export async function step<
|
|
|
73
88
|
description?: string;
|
|
74
89
|
inputSchema: InputSchema;
|
|
75
90
|
fn: ( input: z.infer<InputSchema> ) => Promise<void>;
|
|
91
|
+
options?: Options;
|
|
76
92
|
} ): ( input: z.infer<InputSchema> ) => Promise<void>;
|
|
77
93
|
|
|
78
94
|
/**
|
|
@@ -83,6 +99,7 @@ export async function step<
|
|
|
83
99
|
* @param {string} [options.description] - Description of the step
|
|
84
100
|
* @param {z.ZodType} options.outputSchema - Zod schema for the fn output
|
|
85
101
|
* @param {function} options.fn - The function logic: `() => Promise<z.infer<OutputSchema>>`
|
|
102
|
+
* @param {Options} [options.options] - Activity retry options
|
|
86
103
|
* @returns {function} Function with signature: `() => Promise<z.infer<OutputSchema>>`
|
|
87
104
|
*/
|
|
88
105
|
export async function step<
|
|
@@ -92,6 +109,7 @@ export async function step<
|
|
|
92
109
|
description?: string;
|
|
93
110
|
outputSchema: OutputSchema;
|
|
94
111
|
fn: () => Promise<z.infer<OutputSchema>>;
|
|
112
|
+
options?: Options;
|
|
95
113
|
} ): () => Promise<z.infer<OutputSchema>>;
|
|
96
114
|
|
|
97
115
|
/**
|
|
@@ -101,12 +119,14 @@ export async function step<
|
|
|
101
119
|
* @param {string} options.name - Human-readable step name (only letters, numbers and "_")
|
|
102
120
|
* @param {string} [options.description] - Description of the step
|
|
103
121
|
* @param {function} options.fn - The function logic: `() => Promise<void>`
|
|
122
|
+
* @param {Options} [options.options] - Activity retry options
|
|
104
123
|
* @returns {function} Function with signature: `() => Promise<void>`
|
|
105
124
|
*/
|
|
106
125
|
export async function step( options: {
|
|
107
126
|
name: string;
|
|
108
127
|
description?: string;
|
|
109
128
|
fn: () => Promise<void>;
|
|
129
|
+
options?: Options;
|
|
110
130
|
} ): () => Promise<void>;
|
|
111
131
|
|
|
112
132
|
/*
|
|
@@ -132,6 +152,7 @@ export async function step( options: {
|
|
|
132
152
|
* @param {z.ZodType} options.inputSchema - Zod schema for workflow input
|
|
133
153
|
* @param {z.ZodType} options.outputSchema - Zod schema for workflow output
|
|
134
154
|
* @param {function} options.fn - Workflow logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
155
|
+
* @param {Options} [options.options] - Activity retry options
|
|
135
156
|
* @returns {function} Callable workflow function: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
136
157
|
*/
|
|
137
158
|
export function workflow<
|
|
@@ -142,7 +163,8 @@ export function workflow<
|
|
|
142
163
|
description?: string,
|
|
143
164
|
inputSchema: InputSchema,
|
|
144
165
|
outputSchema: OutputSchema,
|
|
145
|
-
fn: ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema
|
|
166
|
+
fn: ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>,
|
|
167
|
+
options?: Options
|
|
146
168
|
} ): ( input: z.infer<InputSchema> ) => Promise<z.infer<OutputSchema>>;
|
|
147
169
|
|
|
148
170
|
/**
|
|
@@ -153,6 +175,7 @@ export function workflow<
|
|
|
153
175
|
* @param {string} [options.description] - Description of the workflow
|
|
154
176
|
* @param {z.ZodType} options.inputSchema - Zod schema for workflow input
|
|
155
177
|
* @param {function} options.fn - Workflow logic: `(input: z.infer<InputSchema>) => Promise<void>`
|
|
178
|
+
* @param {Options} [options.options] - Activity retry options
|
|
156
179
|
* @returns {function} Callable workflow function: `(input: z.infer<InputSchema>) => Promise<void>`
|
|
157
180
|
*/
|
|
158
181
|
export function workflow<
|
|
@@ -161,7 +184,8 @@ export function workflow<
|
|
|
161
184
|
name: string,
|
|
162
185
|
description?: string,
|
|
163
186
|
inputSchema: InputSchema,
|
|
164
|
-
fn: ( input: z.infer<InputSchema> ) => Promise<void
|
|
187
|
+
fn: ( input: z.infer<InputSchema> ) => Promise<void>,
|
|
188
|
+
options?: Options
|
|
165
189
|
} ): ( input: z.infer<InputSchema> ) => Promise<void>;
|
|
166
190
|
|
|
167
191
|
/**
|
|
@@ -172,6 +196,7 @@ export function workflow<
|
|
|
172
196
|
* @param {string} [options.description] - Description of the workflow
|
|
173
197
|
* @param {z.ZodType} options.outputSchema - Zod schema for workflow output
|
|
174
198
|
* @param {function} options.fn - Workflow logic: `() => Promise<z.infer<OutputSchema>>`
|
|
199
|
+
* @param {Options} [options.options] - Activity retry options
|
|
175
200
|
* @returns {function} Callable workflow function: `() => Promise<z.infer<OutputSchema>>`
|
|
176
201
|
*/
|
|
177
202
|
export function workflow<
|
|
@@ -180,7 +205,8 @@ export function workflow<
|
|
|
180
205
|
name: string,
|
|
181
206
|
description?: string,
|
|
182
207
|
outputSchema: OutputSchema,
|
|
183
|
-
fn: () => Promise<z.infer<OutputSchema
|
|
208
|
+
fn: () => Promise<z.infer<OutputSchema>>,
|
|
209
|
+
options?: Options
|
|
184
210
|
} ): () => Promise<z.infer<OutputSchema>>;
|
|
185
211
|
|
|
186
212
|
/**
|
|
@@ -190,12 +216,14 @@ export function workflow<
|
|
|
190
216
|
* @param {string} options.name - Unique workflow name
|
|
191
217
|
* @param {string} [options.description] - Description of the workflow
|
|
192
218
|
* @param {function} options.fn - Workflow logic: `() => Promise<void>`
|
|
219
|
+
* @param {Options} [options.options] - Activity retry options
|
|
193
220
|
* @returns {function} Callable workflow function: `() => Promise<void>`
|
|
194
221
|
*/
|
|
195
222
|
export function workflow( options : {
|
|
196
223
|
name: string,
|
|
197
224
|
description?: string,
|
|
198
|
-
fn: () => Promise<void
|
|
225
|
+
fn: () => Promise<void>,
|
|
226
|
+
options?: Options
|
|
199
227
|
} ): () => Promise<void>;
|
|
200
228
|
|
|
201
229
|
/*
|
|
@@ -309,6 +337,7 @@ export class EvaluationBooleanResult extends EvaluationResult {
|
|
|
309
337
|
* @param {string} [options.description] - Description of the evaluator
|
|
310
338
|
* @param {z.ZodType} options.inputSchema - Zod schema for the fn input
|
|
311
339
|
* @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
340
|
+
* @param {Options} [options.options] - Activity retry options
|
|
312
341
|
* @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
313
342
|
*/
|
|
314
343
|
export async function evaluator<
|
|
@@ -319,6 +348,7 @@ export async function evaluator<
|
|
|
319
348
|
description?: string;
|
|
320
349
|
inputSchema: InputSchema;
|
|
321
350
|
fn: ( input: z.infer<InputSchema> ) => Promise<Result>;
|
|
351
|
+
options?: Options;
|
|
322
352
|
} ): ( input: z.infer<InputSchema> ) => Promise<Result>;
|
|
323
353
|
|
|
324
354
|
/**
|
|
@@ -329,6 +359,7 @@ export async function evaluator<
|
|
|
329
359
|
* @param {string} [options.description] - Description of the evaluator
|
|
330
360
|
* @param {z.ZodType} options.inputSchema - Zod schema for the fn input
|
|
331
361
|
* @param {function} options.fn - The function logic: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
362
|
+
* @param {Options} [options.options] - Activity retry options
|
|
332
363
|
* @returns {function} Function with signature: `(input: z.infer<InputSchema>) => Promise<z.infer<OutputSchema>>`
|
|
333
364
|
*/
|
|
334
365
|
export async function evaluator<
|
|
@@ -338,6 +369,7 @@ export async function evaluator<
|
|
|
338
369
|
name: string;
|
|
339
370
|
description?: string;
|
|
340
371
|
fn: ( input: z.infer<InputSchema> ) => Promise<Result>;
|
|
372
|
+
options?: Options;
|
|
341
373
|
} ): ( input: z.infer<InputSchema> ) => Promise<Result>;
|
|
342
374
|
|
|
343
375
|
/*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { setMetadata } from './metadata.js';
|
|
2
1
|
import { validateEvaluator } from './validations/static.js';
|
|
3
2
|
import { validateWithSchema } from './validations/runtime.js';
|
|
3
|
+
import { setMetadata } from '#utils';
|
|
4
4
|
import { ValidationError } from '#errors';
|
|
5
5
|
import { ComponentType } from '#consts';
|
|
6
6
|
import * as z from 'zod';
|
|
@@ -130,8 +130,8 @@ export class EvaluationNumberResult extends EvaluationResult {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
export function evaluator( { name, description, inputSchema, fn } ) {
|
|
134
|
-
validateEvaluator( { name, description, inputSchema, fn } );
|
|
133
|
+
export function evaluator( { name, description, inputSchema, fn, options } ) {
|
|
134
|
+
validateEvaluator( { name, description, inputSchema, fn, options } );
|
|
135
135
|
|
|
136
136
|
const wrapper = async input => {
|
|
137
137
|
validateWithSchema( inputSchema, input, `Evaluator ${name} input` );
|
|
@@ -145,6 +145,6 @@ export function evaluator( { name, description, inputSchema, fn } ) {
|
|
|
145
145
|
return output;
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
-
setMetadata( wrapper, { name, description, inputSchema, type: ComponentType.EVALUATOR } );
|
|
148
|
+
setMetadata( wrapper, { name, description, inputSchema, type: ComponentType.EVALUATOR, options } );
|
|
149
149
|
return wrapper;
|
|
150
150
|
};
|
package/src/interface/step.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { setMetadata } from '
|
|
1
|
+
import { setMetadata } from '#utils';
|
|
2
2
|
import { validateStep } from './validations/static.js';
|
|
3
3
|
import { validateWithSchema } from './validations/runtime.js';
|
|
4
4
|
import { ComponentType } from '#consts';
|
|
5
5
|
|
|
6
|
-
export function step( { name, description, inputSchema, outputSchema, fn } ) {
|
|
7
|
-
validateStep( { name, description, inputSchema, outputSchema, fn } );
|
|
6
|
+
export function step( { name, description, inputSchema, outputSchema, fn, options } ) {
|
|
7
|
+
validateStep( { name, description, inputSchema, outputSchema, fn, options } );
|
|
8
8
|
|
|
9
9
|
const wrapper = async input => {
|
|
10
10
|
validateWithSchema( inputSchema, input, `Step ${name} input` );
|
|
@@ -16,6 +16,6 @@ export function step( { name, description, inputSchema, outputSchema, fn } ) {
|
|
|
16
16
|
return output;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
setMetadata( wrapper, { name, description, inputSchema, outputSchema, type: ComponentType.STEP } );
|
|
19
|
+
setMetadata( wrapper, { name, description, inputSchema, outputSchema, type: ComponentType.STEP, options } );
|
|
20
20
|
return wrapper;
|
|
21
21
|
};
|
|
@@ -17,12 +17,26 @@ const refineSchema = ( value, ctx ) => {
|
|
|
17
17
|
} );
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
export const durationStringSchema = z.string().regex(
|
|
21
|
+
/^(\d+)(ms|s|m|h|d)$/,
|
|
22
|
+
'Expected duration like "500ms", "10s", "5m", "2h", or "1d"'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const stepAndWorkflowSchema = z.strictObject( {
|
|
21
26
|
name: z.string().regex( /^[a-z_][a-z0-9_]*$/i ),
|
|
22
27
|
description: z.string().optional(),
|
|
23
28
|
inputSchema: z.any().optional().superRefine( refineSchema ),
|
|
24
29
|
outputSchema: z.any().optional().superRefine( refineSchema ),
|
|
25
|
-
fn: z.function()
|
|
30
|
+
fn: z.function(),
|
|
31
|
+
options: z.strictObject( {
|
|
32
|
+
retry: z.strictObject( {
|
|
33
|
+
initialInterval: durationStringSchema.optional(),
|
|
34
|
+
backoffCoefficient: z.number().gte( 1 ).optional(),
|
|
35
|
+
maximumInterval: durationStringSchema.optional(),
|
|
36
|
+
maximumAttempts: z.number().gte( 1 ).int().optional(),
|
|
37
|
+
nonRetryableErrorTypes: z.array( z.string() ).optional()
|
|
38
|
+
} ).optional()
|
|
39
|
+
} ).optional()
|
|
26
40
|
} );
|
|
27
41
|
|
|
28
42
|
const evaluatorSchema = stepAndWorkflowSchema.omit( { outputSchema: true } );
|
|
@@ -65,6 +65,26 @@ describe( 'interface/validator', () => {
|
|
|
65
65
|
const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at fn' );
|
|
66
66
|
expect( () => validateStep( { ...validArgs, fn: 'not-fn' } ) ).toThrow( error );
|
|
67
67
|
} );
|
|
68
|
+
|
|
69
|
+
it( 'passes with options.retry (second-level options)', () => {
|
|
70
|
+
const args = {
|
|
71
|
+
...validArgs,
|
|
72
|
+
options: {
|
|
73
|
+
retry: {
|
|
74
|
+
initialInterval: '1s',
|
|
75
|
+
backoffCoefficient: 2,
|
|
76
|
+
maximumInterval: '10s',
|
|
77
|
+
maximumAttempts: 3,
|
|
78
|
+
nonRetryableErrorTypes: [ 'SomeError' ]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
expect( () => validateStep( args ) ).not.toThrow();
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'rejects unknown top-level keys due to strictObject', () => {
|
|
86
|
+
expect( () => validateStep( { ...validArgs, extra: 123 } ) ).toThrow( StaticValidationError );
|
|
87
|
+
} );
|
|
68
88
|
} );
|
|
69
89
|
|
|
70
90
|
describe( 'validateWorkflow', () => {
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo } from '@temporalio/workflow';
|
|
3
3
|
import { getInvocationDir } from './utils.js';
|
|
4
|
-
import { setMetadata } from './metadata.js';
|
|
5
|
-
import { FatalError, ValidationError } from '#errors';
|
|
6
4
|
import { validateWorkflow } from './validations/static.js';
|
|
7
5
|
import { validateWithSchema } from './validations/runtime.js';
|
|
6
|
+
import { SHARED_STEP_PREFIX } from '#consts';
|
|
7
|
+
import { mergeActivityOptions, setMetadata } from '#utils';
|
|
8
|
+
import { FatalError, ValidationError } from '#errors';
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
startToCloseTimeout: '
|
|
10
|
+
const defaultActivityOptions = {
|
|
11
|
+
startToCloseTimeout: '20m',
|
|
11
12
|
retry: {
|
|
12
13
|
initialInterval: '10s',
|
|
13
14
|
backoffCoefficient: 2.0,
|
|
14
|
-
maximumInterval: '
|
|
15
|
+
maximumInterval: '2m',
|
|
15
16
|
maximumAttempts: 3,
|
|
16
17
|
nonRetryableErrorTypes: [ ValidationError.name, FatalError.name ]
|
|
17
18
|
}
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
export function workflow( { name, description, inputSchema, outputSchema, fn } ) {
|
|
21
|
-
validateWorkflow( { name, description, inputSchema, outputSchema, fn } );
|
|
21
|
+
export function workflow( { name, description, inputSchema, outputSchema, fn, options } ) {
|
|
22
|
+
validateWorkflow( { name, description, inputSchema, outputSchema, fn, options } );
|
|
22
23
|
const workflowPath = getInvocationDir();
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
+
const activityOptions = mergeActivityOptions( defaultActivityOptions, options );
|
|
26
|
+
const steps = proxyActivities( activityOptions );
|
|
25
27
|
|
|
26
28
|
const wrapper = async input => {
|
|
27
29
|
validateWithSchema( inputSchema, input, `Workflow ${name} input` );
|
|
@@ -44,12 +46,16 @@ export function workflow( { name, description, inputSchema, outputSchema, fn } )
|
|
|
44
46
|
startTime: startTime.getTime()
|
|
45
47
|
};
|
|
46
48
|
|
|
47
|
-
Object.assign( memo, {
|
|
49
|
+
Object.assign( memo, {
|
|
50
|
+
executionContext,
|
|
51
|
+
activityOptions: memo.activityOptions ?? activityOptions // Also preserve the original activity options
|
|
52
|
+
} );
|
|
48
53
|
|
|
49
|
-
// binds the methods called in the code that
|
|
54
|
+
// binds the methods called in the code that Webpack loader will add, they will exposed via "this"
|
|
50
55
|
const output = await fn.call( {
|
|
51
|
-
invokeStep: async ( stepName, input ) => steps[`${workflowPath}#${stepName}`]( input ),
|
|
52
|
-
|
|
56
|
+
invokeStep: async ( stepName, input, options ) => steps[`${workflowPath}#${stepName}`]( input, options ),
|
|
57
|
+
invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
|
|
58
|
+
invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${workflowPath}#${evaluatorName}`]( input, options ),
|
|
53
59
|
|
|
54
60
|
startWorkflow: async ( childName, input ) => {
|
|
55
61
|
return executeChild( childName, { args: input ? [ input ] : [], memo: { executionContext, parentId: workflowId } } );
|
|
@@ -410,12 +410,12 @@ describe( 'Zod Schema Integration Tests', () => {
|
|
|
410
410
|
inputSchema,
|
|
411
411
|
fn: async input => {
|
|
412
412
|
switch ( input.action ) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
case 'create':
|
|
414
|
+
return `Creating ${input.type}: ${input.name}`;
|
|
415
|
+
case 'delete':
|
|
416
|
+
return `Deleting item ${input.id}`;
|
|
417
|
+
default:
|
|
418
|
+
throw new Error( 'Unknown action' );
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
} );
|
package/src/utils.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node safe clone implementation that doesn't use global structuredClone()
|
|
5
|
+
* @param {object} v
|
|
6
|
+
* @returns {object}
|
|
7
|
+
*/
|
|
8
|
+
export const clone = v => JSON.parse( JSON.stringify( v ) );
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Throw given error
|
|
3
12
|
* @param {Error} e
|
|
@@ -6,3 +15,23 @@
|
|
|
6
15
|
export const throws = e => {
|
|
7
16
|
throw e;
|
|
8
17
|
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add metadata "values" property to a given object
|
|
21
|
+
* @param {object} target
|
|
22
|
+
* @param {object} values
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
export const setMetadata = ( target, values ) =>
|
|
26
|
+
Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merge two temporal activity options
|
|
30
|
+
* @param {import('@temporalio/workflow').ActivityOptions} base
|
|
31
|
+
* @param {import('@temporalio/workflow').ActivityOptions} ext
|
|
32
|
+
* @returns {import('@temporalio/workflow').ActivityOptions}
|
|
33
|
+
*/
|
|
34
|
+
export const mergeActivityOptions = ( base = {}, ext = {} ) =>
|
|
35
|
+
Object.entries( ext ).reduce( ( options, [ k, v ] ) =>
|
|
36
|
+
Object.assign( options, { [k]: typeof v === 'object' ? mergeActivityOptions( options[k], v ) : v } )
|
|
37
|
+
, clone( base ) );
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { clone, mergeActivityOptions } from './utils.js';
|
|
3
|
+
|
|
4
|
+
describe( 'clone', () => {
|
|
5
|
+
it( 'produces a deep copy without shared references', () => {
|
|
6
|
+
const original = { a: 1, nested: { b: 2 } };
|
|
7
|
+
const copied = clone( original );
|
|
8
|
+
|
|
9
|
+
copied.nested.b = 3;
|
|
10
|
+
|
|
11
|
+
expect( original.nested.b ).toBe( 2 );
|
|
12
|
+
expect( copied.nested.b ).toBe( 3 );
|
|
13
|
+
expect( copied ).not.toBe( original );
|
|
14
|
+
} );
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
describe( 'mergeActivityOptions', () => {
|
|
18
|
+
it( 'recursively merges nested objects', () => {
|
|
19
|
+
const base = {
|
|
20
|
+
taskQueue: 'q1',
|
|
21
|
+
retry: { maximumAttempts: 3, backoffCoefficient: 2 }
|
|
22
|
+
};
|
|
23
|
+
const ext = {
|
|
24
|
+
retry: { maximumAttempts: 5, initialInterval: '1s' }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const result = mergeActivityOptions( base, ext );
|
|
28
|
+
|
|
29
|
+
expect( result ).toEqual( {
|
|
30
|
+
taskQueue: 'q1',
|
|
31
|
+
retry: { maximumAttempts: 5, backoffCoefficient: 2, initialInterval: '1s' }
|
|
32
|
+
} );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
it( 'omitted properties in second do not overwrite first', () => {
|
|
36
|
+
const base = {
|
|
37
|
+
taskQueue: 'q2',
|
|
38
|
+
retry: { initialInterval: '2s', backoffCoefficient: 2 }
|
|
39
|
+
};
|
|
40
|
+
const ext = {
|
|
41
|
+
retry: { backoffCoefficient: 3 }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = mergeActivityOptions( base, ext );
|
|
45
|
+
|
|
46
|
+
expect( result.retry.initialInterval ).toBe( '2s' );
|
|
47
|
+
expect( result.retry.backoffCoefficient ).toBe( 3 );
|
|
48
|
+
expect( result.taskQueue ).toBe( 'q2' );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'handles omitted second argument by returning a clone', () => {
|
|
52
|
+
const base = { taskQueue: 'q3', retry: { maximumAttempts: 2 } };
|
|
53
|
+
|
|
54
|
+
const result = mergeActivityOptions( base );
|
|
55
|
+
|
|
56
|
+
expect( result ).toEqual( base );
|
|
57
|
+
expect( result ).not.toBe( base );
|
|
58
|
+
} );
|
|
59
|
+
} );
|
|
60
|
+
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { workflowInfo, proxySinks, ApplicationFailure } from '@temporalio/workflow';
|
|
3
3
|
import { memoToHeaders } from '../sandboxed_utils.js';
|
|
4
|
+
import { mergeActivityOptions } from '#utils';
|
|
5
|
+
// this is a dynamic generated file with activity configs overwrites
|
|
6
|
+
import stepOptions from '../temp/__activity_options.js';
|
|
4
7
|
|
|
5
8
|
/*
|
|
6
9
|
This is not an AI comment!
|
|
@@ -13,7 +16,13 @@ import { memoToHeaders } from '../sandboxed_utils.js';
|
|
|
13
16
|
*/
|
|
14
17
|
class HeadersInjectionInterceptor {
|
|
15
18
|
async scheduleActivity( input, next ) {
|
|
16
|
-
|
|
19
|
+
const memo = workflowInfo().memo ?? {};
|
|
20
|
+
Object.assign( input.headers, memoToHeaders( memo ) );
|
|
21
|
+
// apply per-invocation options passed as second argument by rewritten calls
|
|
22
|
+
const options = stepOptions[input.activityType];
|
|
23
|
+
if ( options ) {
|
|
24
|
+
input.options = mergeActivityOptions( memo.activityOptions, options );
|
|
25
|
+
}
|
|
17
26
|
return next( input );
|
|
18
27
|
}
|
|
19
28
|
};
|
package/src/worker/loader.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
import { dirname, join } from 'node:path';
|
|
1
|
+
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
5
|
import { sendWebhook } from '#internal_activities';
|
|
6
|
-
import { ACTIVITY_SEND_WEBHOOK, WORKFLOWS_INDEX_FILENAME, WORKFLOW_CATALOG } from '#consts';
|
|
7
6
|
import { importComponents } from './loader_tools.js';
|
|
7
|
+
import {
|
|
8
|
+
ACTIVITY_SEND_WEBHOOK,
|
|
9
|
+
ACTIVITY_OPTIONS_FILENAME,
|
|
10
|
+
SHARED_STEP_PREFIX,
|
|
11
|
+
WORKFLOWS_INDEX_FILENAME,
|
|
12
|
+
WORKFLOW_CATALOG
|
|
13
|
+
} from '#consts';
|
|
8
14
|
|
|
9
15
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
10
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Writes to file the activity options
|
|
19
|
+
*
|
|
20
|
+
* @param {object} optionsMap
|
|
21
|
+
*/
|
|
22
|
+
const writeActivityOptionsFile = map => {
|
|
23
|
+
const path = join( __dirname, 'temp', ACTIVITY_OPTIONS_FILENAME );
|
|
24
|
+
mkdirSync( dirname( path ), { recursive: true } );
|
|
25
|
+
writeFileSync( path, `export default ${JSON.stringify( map, undefined, 2 )};`, 'utf-8' );
|
|
26
|
+
};
|
|
27
|
+
|
|
11
28
|
/**
|
|
12
29
|
* Builds a map of activities, where the key is their path and name and the value is the function
|
|
13
30
|
*
|
|
@@ -16,11 +33,21 @@ const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
|
16
33
|
*/
|
|
17
34
|
export async function loadActivities( target ) {
|
|
18
35
|
const activities = {};
|
|
19
|
-
|
|
36
|
+
const activityOptionsMap = {};
|
|
37
|
+
for await ( const { fn, metadata, path } of importComponents( target, [ 'steps.js', 'evaluators.js', 'shared_steps.js' ] ) ) {
|
|
38
|
+
const isShared = basename( path ) === 'shared_steps.js';
|
|
39
|
+
const prefix = isShared ? SHARED_STEP_PREFIX : dirname( path );
|
|
40
|
+
|
|
20
41
|
console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
|
|
21
|
-
activities[`${
|
|
42
|
+
activities[`${prefix}#${metadata.name}`] = fn;
|
|
43
|
+
if ( metadata.options ) {
|
|
44
|
+
activityOptionsMap[`${prefix}#${metadata.name}`] = metadata.options;
|
|
45
|
+
}
|
|
22
46
|
}
|
|
23
47
|
|
|
48
|
+
// writes down the activity option overrides
|
|
49
|
+
writeActivityOptionsFile( activityOptionsMap );
|
|
50
|
+
|
|
24
51
|
// system activities
|
|
25
52
|
activities[ACTIVITY_SEND_WEBHOOK] = sendWebhook;
|
|
26
53
|
return activities;
|
|
@@ -3,7 +3,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
3
3
|
vi.mock( '#consts', () => ( {
|
|
4
4
|
ACTIVITY_SEND_WEBHOOK: '__internal#sendWebhook',
|
|
5
5
|
WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
|
|
6
|
-
WORKFLOW_CATALOG: 'catalog'
|
|
6
|
+
WORKFLOW_CATALOG: 'catalog',
|
|
7
|
+
ACTIVITY_OPTIONS_FILENAME: '__activity_options.js'
|
|
7
8
|
} ) );
|
|
8
9
|
|
|
9
10
|
const sendWebhookMock = vi.fn();
|
|
@@ -26,16 +27,26 @@ describe( 'worker/loader', () => {
|
|
|
26
27
|
vi.clearAllMocks();
|
|
27
28
|
} );
|
|
28
29
|
|
|
29
|
-
it( 'loadActivities returns map including system activity', async () => {
|
|
30
|
+
it( 'loadActivities returns map including system activity and writes options file', async () => {
|
|
30
31
|
const { loadActivities } = await import( './loader.js' );
|
|
31
32
|
|
|
32
33
|
importComponentsMock.mockImplementationOnce( async function *() {
|
|
33
|
-
yield { fn: () => {}, metadata: { name: 'Act1' }, path: '/a/steps.js' };
|
|
34
|
+
yield { fn: () => {}, metadata: { name: 'Act1', options: { retry: { maximumAttempts: 3 } } }, path: '/a/steps.js' };
|
|
34
35
|
} );
|
|
35
36
|
|
|
36
37
|
const activities = await loadActivities( '/root' );
|
|
37
38
|
expect( activities['/a#Act1'] ).toBeTypeOf( 'function' );
|
|
38
39
|
expect( activities['__internal#sendWebhook'] ).toBe( sendWebhookMock );
|
|
40
|
+
|
|
41
|
+
// options file written with the collected map
|
|
42
|
+
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
43
|
+
const [ writtenPath, contents ] = writeFileSyncMock.mock.calls[0];
|
|
44
|
+
expect( writtenPath ).toMatch( /temp\/__activity_options\.js$/ );
|
|
45
|
+
expect( contents ).toContain( 'export default' );
|
|
46
|
+
expect( JSON.parse( contents.replace( /^export default\s*/, '' ).replace( /;\s*$/, '' ) ) ).toEqual( {
|
|
47
|
+
'/a#Act1': { retry: { maximumAttempts: 3 } }
|
|
48
|
+
} );
|
|
49
|
+
expect( mkdirSyncMock ).toHaveBeenCalled();
|
|
39
50
|
} );
|
|
40
51
|
|
|
41
52
|
it( 'loadWorkflows returns array of workflows with metadata', async () => {
|
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
buildWorkflowNameMap,
|
|
4
4
|
getLocalNameFromDestructuredProperty,
|
|
5
5
|
isEvaluatorsPath,
|
|
6
|
+
isSharedStepsPath,
|
|
6
7
|
isStepsPath,
|
|
7
8
|
isWorkflowPath,
|
|
8
9
|
buildStepsNameMap,
|
|
10
|
+
buildSharedStepsNameMap,
|
|
9
11
|
buildEvaluatorsNameMap,
|
|
10
12
|
toAbsolutePath
|
|
11
13
|
} from './tools.js';
|
|
@@ -36,8 +38,9 @@ const traverse = traverseModule.default ?? traverseModule;
|
|
|
36
38
|
* @returns {{ stepImports: Array<{localName:string,stepName:string}>,
|
|
37
39
|
* flowImports: Array<{localName:string,workflowName:string}> }} Collected info mappings.
|
|
38
40
|
*/
|
|
39
|
-
export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache, evaluatorsNameCache } ) {
|
|
41
|
+
export default function collectTargetImports( ast, fileDir, { stepsNameCache, workflowNameCache, evaluatorsNameCache, sharedStepsNameCache } ) {
|
|
40
42
|
const stepImports = [];
|
|
43
|
+
const sharedStepImports = [];
|
|
41
44
|
const flowImports = [];
|
|
42
45
|
const evaluatorImports = [];
|
|
43
46
|
|
|
@@ -45,33 +48,31 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
45
48
|
ImportDeclaration: path => {
|
|
46
49
|
const src = path.node.source.value;
|
|
47
50
|
// Ignore other imports
|
|
48
|
-
if ( !isStepsPath( src ) && !isWorkflowPath( src ) && !isEvaluatorsPath( src ) ) {
|
|
51
|
+
if ( !isStepsPath( src ) && !isSharedStepsPath( src ) && !isWorkflowPath( src ) && !isEvaluatorsPath( src ) ) {
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const absolutePath = toAbsolutePath( fileDir, src );
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const importedName = s.imported.name;
|
|
57
|
-
const localName = s.local.name;
|
|
58
|
-
const stepName = nameMap.get( importedName );
|
|
59
|
-
if ( stepName ) {
|
|
60
|
-
stepImports.push( { localName, stepName } );
|
|
61
|
-
}
|
|
56
|
+
const collectNamedImports = ( match, buildMapFn, cache, targetArr, valueKey ) => {
|
|
57
|
+
if ( !match ) {
|
|
58
|
+
return;
|
|
62
59
|
}
|
|
63
|
-
|
|
64
|
-
if ( isEvaluatorsPath( src ) ) {
|
|
65
|
-
const nameMap = buildEvaluatorsNameMap( absolutePath, evaluatorsNameCache );
|
|
60
|
+
const nameMap = buildMapFn( absolutePath, cache );
|
|
66
61
|
for ( const s of path.node.specifiers.filter( s => isImportSpecifier( s ) ) ) {
|
|
67
62
|
const importedName = s.imported.name;
|
|
68
63
|
const localName = s.local.name;
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
64
|
+
const value = nameMap.get( importedName );
|
|
65
|
+
if ( value ) {
|
|
66
|
+
const entry = { localName };
|
|
67
|
+
entry[valueKey] = value;
|
|
68
|
+
targetArr.push( entry );
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
|
-
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
collectNamedImports( isStepsPath( src ), buildStepsNameMap, stepsNameCache, stepImports, 'stepName' );
|
|
74
|
+
collectNamedImports( isSharedStepsPath( src ), buildSharedStepsNameMap, sharedStepsNameCache, sharedStepImports, 'stepName' );
|
|
75
|
+
collectNamedImports( isEvaluatorsPath( src ), buildEvaluatorsNameMap, evaluatorsNameCache, evaluatorImports, 'evaluatorName' );
|
|
75
76
|
if ( isWorkflowPath( src ) ) {
|
|
76
77
|
const { named, default: defName } = buildWorkflowNameMap( absolutePath, workflowNameCache );
|
|
77
78
|
for ( const s of path.node.specifiers ) {
|
|
@@ -108,7 +109,7 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
108
109
|
|
|
109
110
|
const req = firstArgument.value;
|
|
110
111
|
// Must be steps/workflows module
|
|
111
|
-
if ( !isStepsPath( req ) && !isWorkflowPath( req ) && !isEvaluatorsPath( req ) ) {
|
|
112
|
+
if ( !isStepsPath( req ) && !isSharedStepsPath( req ) && !isWorkflowPath( req ) && !isEvaluatorsPath( req ) ) {
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -130,6 +131,23 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
130
131
|
} else {
|
|
131
132
|
path.remove();
|
|
132
133
|
}
|
|
134
|
+
} else if ( isSharedStepsPath( req ) && isObjectPattern( path.node.id ) ) {
|
|
135
|
+
const nameMap = buildSharedStepsNameMap( absolutePath, sharedStepsNameCache ?? stepsNameCache );
|
|
136
|
+
for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
|
|
137
|
+
const importedName = prop.key.name;
|
|
138
|
+
const localName = getLocalNameFromDestructuredProperty( prop );
|
|
139
|
+
if ( localName ) {
|
|
140
|
+
const stepName = nameMap.get( importedName );
|
|
141
|
+
if ( stepName ) {
|
|
142
|
+
sharedStepImports.push( { localName, stepName } );
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if ( isVariableDeclaration( path.parent ) && path.parent.declarations.length === 1 ) {
|
|
147
|
+
path.parentPath.remove();
|
|
148
|
+
} else {
|
|
149
|
+
path.remove();
|
|
150
|
+
}
|
|
133
151
|
} else if ( isEvaluatorsPath( req ) && isObjectPattern( path.node.id ) ) {
|
|
134
152
|
const nameMap = buildEvaluatorsNameMap( absolutePath, evaluatorsNameCache );
|
|
135
153
|
for ( const prop of path.node.id.properties.filter( prop => isObjectProperty( prop ) && isIdentifier( prop.key ) ) ) {
|
|
@@ -160,5 +178,5 @@ export default function collectTargetImports( ast, fileDir, { stepsNameCache, wo
|
|
|
160
178
|
}
|
|
161
179
|
} );
|
|
162
180
|
|
|
163
|
-
return { stepImports, evaluatorImports, flowImports };
|
|
181
|
+
return { stepImports, sharedStepImports, evaluatorImports, flowImports };
|
|
164
182
|
};
|
|
@@ -10,6 +10,7 @@ const generate = generatorModule.default ?? generatorModule;
|
|
|
10
10
|
|
|
11
11
|
// Caches to avoid re-reading files during a build
|
|
12
12
|
const stepsNameCache = new Map(); // path -> Map<exported, stepName>
|
|
13
|
+
const sharedStepsNameCache = new Map(); // path -> Map<exported, stepName> (shared)
|
|
13
14
|
const evaluatorsNameCache = new Map(); // path -> Map<exported, evaluatorName>
|
|
14
15
|
const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exported, flowName> }
|
|
15
16
|
|
|
@@ -26,20 +27,20 @@ const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exp
|
|
|
26
27
|
export default function stepImportRewriterAstLoader( source, inputMap ) {
|
|
27
28
|
this.cacheable?.( true );
|
|
28
29
|
const callback = this.async?.() ?? this.callback;
|
|
29
|
-
const cache = { stepsNameCache, evaluatorsNameCache, workflowNameCache };
|
|
30
|
+
const cache = { stepsNameCache, sharedStepsNameCache, evaluatorsNameCache, workflowNameCache };
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
33
|
const filename = this.resourcePath;
|
|
33
34
|
const ast = parse( String( source ), filename );
|
|
34
35
|
const fileDir = dirname( filename );
|
|
35
|
-
const { stepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
36
|
+
const { stepImports, sharedStepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
36
37
|
|
|
37
38
|
// No imports
|
|
38
|
-
if ( [].concat( stepImports, evaluatorImports, flowImports ).length === 0 ) {
|
|
39
|
+
if ( [].concat( stepImports, sharedStepImports, evaluatorImports, flowImports ).length === 0 ) {
|
|
39
40
|
return callback( null, source, inputMap );
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } );
|
|
43
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, sharedStepImports, evaluatorImports, flowImports } );
|
|
43
44
|
// No edits performed
|
|
44
45
|
if ( !rewrote ) {
|
|
45
46
|
return callback( null, source, inputMap );
|
|
@@ -51,6 +51,54 @@ describe( 'workflows_rewriter Webpack loader spec', () => {
|
|
|
51
51
|
rmSync( dir, { recursive: true, force: true } );
|
|
52
52
|
} );
|
|
53
53
|
|
|
54
|
+
it( 'rewrites ESM shared_steps imports to invokeSharedStep', async () => {
|
|
55
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-' ) );
|
|
56
|
+
writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedA = step({ name: \'shared.a\' })\n' );
|
|
57
|
+
|
|
58
|
+
const source = [
|
|
59
|
+
'import { SharedA } from \'./shared_steps.js\';',
|
|
60
|
+
'',
|
|
61
|
+
'const obj = {',
|
|
62
|
+
' fn: async (x) => {',
|
|
63
|
+
' SharedA(1);',
|
|
64
|
+
' }',
|
|
65
|
+
'}',
|
|
66
|
+
''
|
|
67
|
+
].join( '\n' );
|
|
68
|
+
|
|
69
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
70
|
+
|
|
71
|
+
expect( code ).not.toMatch( /from '\.\/shared_steps\.js'/ );
|
|
72
|
+
expect( code ).toMatch( /fn:\s*async function \(x\)/ );
|
|
73
|
+
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.a',\s*1\)/ );
|
|
74
|
+
|
|
75
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'rewrites CJS shared_steps requires to invokeSharedStep', async () => {
|
|
79
|
+
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-' ) );
|
|
80
|
+
writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedB = step({ name: \'shared.b\' })\n' );
|
|
81
|
+
|
|
82
|
+
const source = [
|
|
83
|
+
'const { SharedB } = require(\'./shared_steps.js\');',
|
|
84
|
+
'',
|
|
85
|
+
'const obj = {',
|
|
86
|
+
' fn: async (y) => {',
|
|
87
|
+
' SharedB();',
|
|
88
|
+
' }',
|
|
89
|
+
'}',
|
|
90
|
+
''
|
|
91
|
+
].join( '\n' );
|
|
92
|
+
|
|
93
|
+
const { code } = await runLoader( source, join( dir, 'file.js' ) );
|
|
94
|
+
|
|
95
|
+
expect( code ).not.toMatch( /require\('\.\/shared_steps\.js'\)/ );
|
|
96
|
+
expect( code ).toMatch( /fn:\s*async function \(y\)/ );
|
|
97
|
+
expect( code ).toMatch( /this\.invokeSharedStep\('shared\.b'\)/ );
|
|
98
|
+
|
|
99
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
100
|
+
} );
|
|
101
|
+
|
|
54
102
|
it( 'rewrites CJS requires and converts fn arrow to function', async () => {
|
|
55
103
|
const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-' ) );
|
|
56
104
|
writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: \'step.b\' })\n' );
|
|
@@ -18,11 +18,12 @@ const traverse = traverseModule.default ?? traverseModule;
|
|
|
18
18
|
* @param {object} params
|
|
19
19
|
* @param {import('@babel/types').File} params.ast - Parsed file AST.
|
|
20
20
|
* @param {Array<{localName:string,stepName:string}>} params.stepImports - Step imports.
|
|
21
|
+
* @param {Array<{localName:string,stepName:string}>} params.sharedStepImports - Shared step imports.
|
|
21
22
|
* @param {Array<{localName:string,evaluatorName:string}>} params.evaluatorImports - Evaluator imports.
|
|
22
23
|
* @param {Array<{localName:string,workflowName:string}>} params.flowImports - Workflow imports.
|
|
23
24
|
* @returns {boolean} True if the AST was modified; false otherwise.
|
|
24
25
|
*/
|
|
25
|
-
export default function rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } ) {
|
|
26
|
+
export default function rewriteFnBodies( { ast, stepImports, sharedStepImports = [], evaluatorImports, flowImports } ) {
|
|
26
27
|
const state = { rewrote: false };
|
|
27
28
|
traverse( ast, {
|
|
28
29
|
ObjectProperty: path => {
|
|
@@ -51,25 +52,20 @@ export default function rewriteFnBodies( { ast, stepImports, evaluatorImports, f
|
|
|
51
52
|
if ( !isIdentifier( callee ) ) {
|
|
52
53
|
return;
|
|
53
54
|
} // Skip: complex callee not supported
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const flow = flowImports.find( x => x.localName === callee.name );
|
|
69
|
-
if ( flow ) {
|
|
70
|
-
const args = cPath.node.arguments;
|
|
71
|
-
cPath.replaceWith( createThisMethodCall( 'startWorkflow', flow.workflowName, args ) );
|
|
72
|
-
state.rewrote = true;
|
|
55
|
+
const descriptors = [
|
|
56
|
+
{ list: stepImports, method: 'invokeStep', key: 'stepName' },
|
|
57
|
+
{ list: sharedStepImports, method: 'invokeSharedStep', key: 'stepName' },
|
|
58
|
+
{ list: evaluatorImports, method: 'invokeEvaluator', key: 'evaluatorName' },
|
|
59
|
+
{ list: flowImports, method: 'startWorkflow', key: 'workflowName' }
|
|
60
|
+
];
|
|
61
|
+
for ( const { list, method, key } of descriptors ) {
|
|
62
|
+
const found = list.find( x => x.localName === callee.name );
|
|
63
|
+
if ( found ) {
|
|
64
|
+
const args = cPath.node.arguments;
|
|
65
|
+
cPath.replaceWith( createThisMethodCall( method, found[key], args ) );
|
|
66
|
+
state.rewrote = true;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
71
|
} );
|
|
@@ -106,6 +106,13 @@ export const toFunctionExpression = arrow => {
|
|
|
106
106
|
*/
|
|
107
107
|
export const isStepsPath = value => /(^|\/)steps\.js$/.test( value );
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Check if a module specifier or request string points to shared_steps.js.
|
|
111
|
+
* @param {string} value - Module path or request string.
|
|
112
|
+
* @returns {boolean} True if it matches shared_steps.js.
|
|
113
|
+
*/
|
|
114
|
+
export const isSharedStepsPath = value => /(^|\/)shared_steps\.js$/.test( value );
|
|
115
|
+
|
|
109
116
|
/**
|
|
110
117
|
* Check if a module specifier or request string points to evaluators.js.
|
|
111
118
|
* @param {string} value - Module path or request string.
|
|
@@ -215,6 +222,22 @@ export const buildStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
|
215
222
|
invalidMessagePrefix: 'Invalid step name in'
|
|
216
223
|
} );
|
|
217
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Build a map from exported shared step identifier to declared step name.
|
|
227
|
+
* Parses `shared_steps.js` for `export const X = step({ name: '...' })`.
|
|
228
|
+
* Uses the same factory as regular steps.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} path - Absolute path to the shared steps module file.
|
|
231
|
+
* @param {Map<string, Map<string,string>>} cache - Cache of computed name maps.
|
|
232
|
+
* @returns {Map<string,string>} Exported identifier -> step name.
|
|
233
|
+
*/
|
|
234
|
+
export const buildSharedStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
235
|
+
path,
|
|
236
|
+
cache,
|
|
237
|
+
calleeName: 'step',
|
|
238
|
+
invalidMessagePrefix: 'Invalid shared step name in'
|
|
239
|
+
} );
|
|
240
|
+
|
|
218
241
|
/**
|
|
219
242
|
* Build a map from exported evaluator identifier to declared evaluator name.
|
|
220
243
|
* Parses `evaluators.js` for `export const X = evaluator({ name: '...' })`.
|