diff --git a/.changeset/tidy-states-see.md b/.changeset/tidy-states-see.md new file mode 100644 index 000000000..0b26a975b --- /dev/null +++ b/.changeset/tidy-states-see.md @@ -0,0 +1,6 @@ +--- +"@workflow/swc-plugin": patch +"@workflow/core": patch +--- + +Support serializing step function references diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 1ead410bf..006aa64ea 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -687,4 +687,36 @@ describe('e2e', () => { expect(run2Data.status).toBe('completed'); } ); + + test( + 'stepFunctionPassingWorkflow - step function references can be passed as arguments', + { timeout: 60_000 }, + async () => { + // This workflow passes a step function reference to another step + // The receiving step calls the passed function and returns the result + const run = await triggerWorkflow('stepFunctionPassingWorkflow', []); + const returnValue = await getWorkflowReturnValue(run.runId); + + // doubleNumber(10) = 20, then multiply by 2 = 40 + expect(returnValue).toBe(40); + + // Verify the run completed successfully + const { json: runData } = await cliInspectJson( + `runs ${run.runId} --withData` + ); + expect(runData.status).toBe('completed'); + expect(runData.output).toBe(40); + + // Verify that exactly 2 steps were executed: + // 1. stepWithStepFunctionArg(doubleNumber) + // (doubleNumber(10) is run inside the stepWithStepFunctionArg step) + const { json: eventsData } = await cliInspectJson( + `events --run ${run.runId} --json` + ); + const stepCompletedEvents = eventsData.filter( + (event) => event.eventType === 'step_completed' + ); + expect(stepCompletedEvents).toHaveLength(1); + } + ); }); diff --git a/packages/core/package.json b/packages/core/package.json index 791e2f227..bd1efacb5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,7 +57,7 @@ "@workflow/world-local": "workspace:*", "@workflow/world-vercel": "workspace:*", "debug": "4.4.3", - "devalue": "5.4.1", + "devalue": "5.5.0", "ms": "2.1.3", "nanoid": "5.1.6", "seedrandom": "3.0.5", diff --git a/packages/core/src/runtime/world.ts b/packages/core/src/runtime/world.ts index 664944ee3..758f84917 100644 --- a/packages/core/src/runtime/world.ts +++ b/packages/core/src/runtime/world.ts @@ -1,10 +1,10 @@ import { createRequire } from 'node:module'; -import Path from 'node:path'; +import { join } from 'node:path'; import type { World } from '@workflow/world'; import { createEmbeddedWorld } from '@workflow/world-local'; import { createVercelWorld } from '@workflow/world-vercel'; -const require = createRequire(Path.join(process.cwd(), 'index.js')); +const require = createRequire(join(process.cwd(), 'index.js')); let worldCache: World | undefined; let stubbedWorldCache: World | undefined; diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts index d07bc69bc..69f317033 100644 --- a/packages/core/src/schemas.ts +++ b/packages/core/src/schemas.ts @@ -63,4 +63,5 @@ export type Serializable = | Uint8ClampedArray | Uint16Array | Uint32Array - | WritableStream; + | WritableStream + | ((...args: Serializable[]) => Promise); // Step function diff --git a/packages/core/src/serialization.test.ts b/packages/core/src/serialization.test.ts index 746467aac..f5c736261 100644 --- a/packages/core/src/serialization.test.ts +++ b/packages/core/src/serialization.test.ts @@ -1,15 +1,18 @@ import { runInContext } from 'node:vm'; import type { WorkflowRuntimeError } from '@workflow/errors'; import { describe, expect, it } from 'vitest'; +import { getStepFunction, registerStepFunction } from './private.js'; import { dehydrateStepArguments, dehydrateStepReturnValue, dehydrateWorkflowArguments, dehydrateWorkflowReturnValue, + getCommonRevivers, getStreamType, + getWorkflowReducers, hydrateWorkflowArguments, } from './serialization.js'; -import { STREAM_NAME_SYMBOL } from './symbols.js'; +import { STEP_FUNCTION_NAME_SYMBOL, STREAM_NAME_SYMBOL } from './symbols.js'; import { createContext } from './vm/index.js'; describe('getStreamType', () => { @@ -783,3 +786,130 @@ describe('step return value', () => { ); }); }); + +describe('step function serialization', () => { + const { globalThis: vmGlobalThis } = createContext({ + seed: 'test', + fixedTimestamp: 1714857600000, + }); + + it('should detect step function by checking for STEP_FUNCTION_NAME_SYMBOL', () => { + const stepName = 'myStep'; + const stepFn = async (x: number) => x * 2; + + // Attach the symbol like useStep() does + Object.defineProperty(stepFn, STEP_FUNCTION_NAME_SYMBOL, { + value: stepName, + writable: false, + enumerable: false, + configurable: false, + }); + + // Verify the symbol is attached correctly + expect((stepFn as any)[STEP_FUNCTION_NAME_SYMBOL]).toBe(stepName); + }); + + it('should not have STEP_FUNCTION_NAME_SYMBOL on regular functions', () => { + const regularFn = async (x: number) => x * 2; + + // Regular functions should not have the symbol + expect((regularFn as any)[STEP_FUNCTION_NAME_SYMBOL]).toBeUndefined(); + }); + + it('should lookup registered step function by name', () => { + const stepName = 'myRegisteredStep'; + const stepFn = async (x: number) => x * 2; + + // Register the step function + registerStepFunction(stepName, stepFn); + + // Should be retrievable by name + const retrieved = getStepFunction(stepName); + expect(retrieved).toBe(stepFn); + }); + + it('should return undefined for non-existent registered step function', () => { + const retrieved = getStepFunction('nonExistentStep'); + expect(retrieved).toBeUndefined(); + }); + + it('should deserialize step function name through reviver', () => { + const stepName = 'testStep'; + const stepFn = async () => 42; + + // Register the step function + registerStepFunction(stepName, stepFn); + + // Get the reviver and test it directly + const revivers = getCommonRevivers(vmGlobalThis); + const result = revivers.StepFunction(stepName); + + expect(result).toBe(stepFn); + }); + + it('should throw error when reviver cannot find registered step function', () => { + const revivers = getCommonRevivers(vmGlobalThis); + + let err: Error | undefined; + try { + revivers.StepFunction('nonExistentStep'); + } catch (err_) { + err = err_ as Error; + } + + expect(err).toBeDefined(); + expect(err?.message).toContain('Step function "nonExistentStep" not found'); + expect(err?.message).toContain('Make sure the step function is registered'); + }); + + it('should dehydrate step function passed as argument to a step', () => { + const stepName = 'step//workflows/test.ts//myStep'; + const stepFn = async (x: number) => x * 2; + + // Register the step function + registerStepFunction(stepName, stepFn); + + // Attach the symbol to the function (like the SWC compiler would) + Object.defineProperty(stepFn, STEP_FUNCTION_NAME_SYMBOL, { + value: stepName, + writable: false, + enumerable: false, + configurable: false, + }); + + // Simulate passing a step function as an argument within a workflow + // When calling a step from within a workflow context + const args = [stepFn, 42]; + + // This should serialize the step function by its name using the reducer + const dehydrated = dehydrateStepArguments(args, globalThis); + + // Verify it dehydrated successfully + expect(dehydrated).toBeDefined(); + expect(Array.isArray(dehydrated)).toBe(true); + // The dehydrated structure is the flattened format from devalue + // It should contain the step function serialized as its name + expect(dehydrated).toContain(stepName); + expect(dehydrated).toContain(42); + }); + + it('should serialize step function to name through reducer', () => { + const stepName = 'step//workflows/test.ts//anotherStep'; + const stepFn = async () => 'result'; + + // Attach the symbol to the function (like the SWC compiler would) + Object.defineProperty(stepFn, STEP_FUNCTION_NAME_SYMBOL, { + value: stepName, + writable: false, + enumerable: false, + configurable: false, + }); + + // Get the reducer and verify it detects the step function + const reducer = getWorkflowReducers(globalThis).StepFunction; + const result = reducer(stepFn); + + // Should return the step name + expect(result).toBe(stepName); + }); +}); diff --git a/packages/core/src/serialization.ts b/packages/core/src/serialization.ts index c67bcac7c..08871db83 100644 --- a/packages/core/src/serialization.ts +++ b/packages/core/src/serialization.ts @@ -1,8 +1,10 @@ import { WorkflowRuntimeError } from '@workflow/errors'; import * as devalue from 'devalue'; +import { getStepFunction } from './private.js'; import { getWorld } from './runtime/world.js'; import { BODY_INIT_SYMBOL, + STEP_FUNCTION_NAME_SYMBOL, STREAM_NAME_SYMBOL, STREAM_TYPE_SYMBOL, WEBHOOK_RESPONSE_WRITABLE, @@ -169,6 +171,7 @@ export interface SerializableSpecial { redirected: boolean; }; Set: any[]; + StepFunction: string; // step function name/ID URL: string; URLSearchParams: string; Uint8Array: string; // base64 string @@ -275,6 +278,11 @@ function getCommonReducers(global: Record = globalThis) { }; }, Set: (value) => value instanceof global.Set && Array.from(value), + StepFunction: (value) => { + if (typeof value !== 'function') return false; + const stepName = value[STEP_FUNCTION_NAME_SYMBOL]; + return typeof stepName === 'string' ? stepName : false; + }, URL: (value) => value instanceof global.URL && value.href, URLSearchParams: (value) => { if (!(value instanceof global.URLSearchParams)) return false; @@ -464,7 +472,7 @@ function getStepReducers( }; } -function getCommonRevivers(global: Record = globalThis) { +export function getCommonRevivers(global: Record = globalThis) { function reviveArrayBuffer(value: string) { // Handle sentinel value for zero-length buffers const base64 = value === '.' ? '' : value; @@ -516,6 +524,15 @@ function getCommonRevivers(global: Record = globalThis) { Map: (value) => new global.Map(value), RegExp: (value) => new global.RegExp(value.source, value.flags), Set: (value) => new global.Set(value), + StepFunction: (value) => { + const stepFn = getStepFunction(value); + if (!stepFn) { + throw new Error( + `Step function "${value}" not found. Make sure the step function is registered.` + ); + } + return stepFn; + }, URL: (value) => new global.URL(value), URLSearchParams: (value) => new global.URLSearchParams(value === '.' ? '' : value), diff --git a/packages/core/src/symbols.ts b/packages/core/src/symbols.ts index 1df8ac074..f5957f322 100644 --- a/packages/core/src/symbols.ts +++ b/packages/core/src/symbols.ts @@ -9,3 +9,6 @@ export const BODY_INIT_SYMBOL = Symbol.for('BODY_INIT'); export const WEBHOOK_RESPONSE_WRITABLE = Symbol.for( 'WEBHOOK_RESPONSE_WRITABLE' ); +export const STEP_FUNCTION_NAME_SYMBOL = Symbol.for( + 'WORKFLOW_STEP_FUNCTION_NAME' +); diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 60f2e43af..cab0c6965 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -1,4 +1,5 @@ import { defineNuxtModule } from '@nuxt/kit'; +import type { NuxtModule } from '@nuxt/schema'; import type { ModuleOptions as NitroModuleOptions } from '@workflow/nitro'; // Module options TypeScript interface definition @@ -30,4 +31,4 @@ export default defineNuxtModule({ nuxt.options.nitro.modules.push('@workflow/nitro'); } }, -}); +}) satisfies NuxtModule; diff --git a/packages/nuxt/tsconfig.json b/packages/nuxt/tsconfig.json index 1760a52dd..b06d679c1 100644 --- a/packages/nuxt/tsconfig.json +++ b/packages/nuxt/tsconfig.json @@ -1,4 +1,8 @@ { - "extends": "./.nuxt/tsconfig.json", - "exclude": ["dist", "node_modules"] + "extends": "@workflow/tsconfig/base.json", + "exclude": ["dist", "node_modules"], + "compilerOptions": { + "declaration": false, + "declarationMap": false + } } diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index 1ab67a0ae..25a4afe74 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -789,6 +789,119 @@ impl StepTransform { }) } + // Mark a step function with the STEP_FUNCTION_NAME_SYMBOL in workflow mode + fn create_step_function_marking(&self, fn_name: &str, span: swc_core::common::Span) -> Stmt { + let step_id = self.create_id(Some(fn_name), span, false); + + // Create: Object.defineProperty(functionName, Symbol.for('WORKFLOW_STEP_FUNCTION_NAME'), { + // value: "stepId", + // writable: false, + // enumerable: false, + // configurable: false + // }) + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + "Object".into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Ident(IdentName::new("defineProperty".into(), DUMMY_SP)), + }))), + args: vec![ + // First argument: functionName + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Ident(Ident::new( + fn_name.into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + }, + // Second argument: Symbol.for('WORKFLOW_STEP_FUNCTION_NAME') + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + "Symbol".into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Ident(IdentName::new("for".into(), DUMMY_SP)), + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: "WORKFLOW_STEP_FUNCTION_NAME".into(), + raw: None, + }))), + }], + type_args: None, + })), + }, + // Third argument: property descriptor object + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![ + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new("value".into(), DUMMY_SP)), + value: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: step_id.into(), + raw: None, + }))), + }))), + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new( + "writable".into(), + DUMMY_SP, + )), + value: Box::new(Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value: false, + }))), + }))), + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new( + "enumerable".into(), + DUMMY_SP, + )), + value: Box::new(Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value: false, + }))), + }))), + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new( + "configurable".into(), + DUMMY_SP, + )), + value: Box::new(Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value: false, + }))), + }))), + ], + })), + }, + ], + type_args: None, + })), + }) + } + // Create a registration call for step mode fn create_registration_call(&mut self, name: &str, span: swc_core::common::Span) { // Only register each function once @@ -2020,6 +2133,67 @@ impl VisitMut for StepTransform { } } + // In workflow mode, mark all step functions with the STEP_FUNCTION_NAME_SYMBOL + if self.mode == TransformMode::Workflow { + let step_functions: Vec<_> = self.step_function_names.iter().cloned().collect(); + + // Collect function marking statements first (to avoid borrow conflicts) + let mut step_marking_statements = Vec::new(); + for item in items.iter() { + match item { + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { + let fn_name = fn_decl.ident.sym.to_string(); + if step_functions.contains(&fn_name) { + step_marking_statements.push( + self.create_step_function_marking(&fn_name, fn_decl.function.span), + ); + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { + if let Decl::Fn(fn_decl) = &export_decl.decl { + let fn_name = fn_decl.ident.sym.to_string(); + if step_functions.contains(&fn_name) { + step_marking_statements.push( + self.create_step_function_marking( + &fn_name, + fn_decl.function.span, + ), + ); + } + } else if let Decl::Var(var_decl) = &export_decl.decl { + // Handle exported variable declarations like `export const stepArrow = async () => {}` + for declarator in &var_decl.decls { + if let Pat::Ident(binding) = &declarator.name { + let name = binding.id.sym.to_string(); + if step_functions.contains(&name) { + if let Some(init) = &declarator.init { + let span = match &**init { + Expr::Fn(fn_expr) => fn_expr.function.span, + Expr::Arrow(arrow_expr) => arrow_expr.span, + _ => declarator.span, + }; + step_marking_statements.push( + self.create_step_function_marking( + &name, + span, + ), + ); + } + } + } + } + } + } + _ => {} + } + } + + // Now add all the marking statements + for stmt in step_marking_statements { + items.push(ModuleItem::Stmt(stmt)); + } + } + // Clear the workflow_functions_needing_id since we've already processed them self.workflow_functions_needing_id.clear(); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/conflicting-directives/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/conflicting-directives/output-workflow.js index 3c0dca241..d5bd33d25 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/conflicting-directives/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/conflicting-directives/output-workflow.js @@ -5,3 +5,9 @@ export async function test() { return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//test")(); } +Object.defineProperty(test, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//test", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js index ac7650ce5..9f533648a 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js @@ -12,3 +12,15 @@ class TestClass extends BaseClass { return super.method(); } } +Object.defineProperty(stepWithThis, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//stepWithThis", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(stepWithArguments, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//stepWithArguments", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js index 2a7f4cfc2..c684ad55a 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js @@ -13,3 +13,9 @@ export * from './other'; export async function validStep() { return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//validStep")(); } +Object.defineProperty(validStep, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//validStep", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/misplaced-function-directive/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/misplaced-function-directive/output-workflow.js index 5ce34928e..92e43e692 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/misplaced-function-directive/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/misplaced-function-directive/output-workflow.js @@ -8,3 +8,9 @@ export const badWorkflow = async ()=>{ 'use workflow'; return true; }; +Object.defineProperty(badStep, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//badStep", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/non-async-functions/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/non-async-functions/output-workflow.js index 260788cba..51ddfc5b8 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/non-async-functions/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/non-async-functions/output-workflow.js @@ -24,3 +24,9 @@ export const validWorkflow = async ()=>{ return 'test'; }; validWorkflow.workflowId = "workflow//input.js//validWorkflow"; +Object.defineProperty(validStep, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//validStep", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/destructuring/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/destructuring/output-workflow.js index 1ed07b495..262e7d6c7 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/destructuring/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/destructuring/output-workflow.js @@ -44,3 +44,45 @@ export async function multiple({ a, b }, { c, d }) { export async function rest_top_level(a, b, ...rest) { return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//rest_top_level")(a, b, ...rest); } +Object.defineProperty(destructure, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//destructure", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(process_array, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//process_array", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(nested_destructure, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//nested_destructure", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(with_defaults, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//with_defaults", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(with_rest, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//with_rest", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(multiple, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//multiple", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(rest_top_level, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//rest_top_level", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/mixed-functions/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/mixed-functions/output-workflow.js index de3235a96..a299fd8ce 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/mixed-functions/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/mixed-functions/output-workflow.js @@ -9,3 +9,9 @@ export async function normalFunction(a, b) { return a * b; } workflowFunction.workflowId = "workflow//input.js//workflowFunction"; +Object.defineProperty(stepFunction, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//stepFunction", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/module-level-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/module-level-step/output-workflow.js index 127712b65..e96ec552d 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/module-level-step/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/module-level-step/output-workflow.js @@ -7,3 +7,15 @@ export async function step(input) { return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//step")(input); } export const stepArrow = async (input)=>globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepArrow")(input); +Object.defineProperty(step, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//step", + writable: false, + enumerable: false, + configurable: false +}); +Object.defineProperty(stepArrow, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//stepArrow", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/single-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/single-step/output-workflow.js index 607db576f..d5fe6cc26 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/single-step/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/single-step/output-workflow.js @@ -2,3 +2,9 @@ export async function add(a, b) { return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//add")(a, b); } +Object.defineProperty(add, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//add", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/step-arrow-function/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-arrow-function/output-workflow.js index 115cbcb4d..01301f1dc 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/step-arrow-function/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-arrow-function/output-workflow.js @@ -1,2 +1,8 @@ /**__internal_workflows{"steps":{"input.js":{"multiply":{"stepId":"step//input.js//multiply"}}}}*/; export const multiply = async (a, b)=>globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//multiply")(a, b); +Object.defineProperty(multiply, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//multiply", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/step-with-imports/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-imports/output-workflow.js index 93bcdd6cb..b0608409e 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/step-with-imports/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-imports/output-workflow.js @@ -10,3 +10,9 @@ export function normalFunction() { useful.doSomething(); return usefulHelper(); } +Object.defineProperty(processData, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//processData", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/unused-exports/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/unused-exports/output-workflow.js index 1137f50ed..5e774f86e 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/unused-exports/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/unused-exports/output-workflow.js @@ -21,3 +21,9 @@ function internalHelper(value) { export function calculate(x) { return internalHelper(x); } +Object.defineProperty(processData, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//processData", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/unused-variables-and-types/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/unused-variables-and-types/output-workflow.js index 1ecf355c6..522771658 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/unused-variables-and-types/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/unused-variables-and-types/output-workflow.js @@ -8,3 +8,9 @@ export const sendRecipientEmail = async ({ recipientEmail, cardImage, cardText, export function normalFunction() { return 'this stays because it is exported'; } +Object.defineProperty(sendRecipientEmail, Symbol.for("WORKFLOW_STEP_FUNCTION_NAME"), { + value: "step//input.js//sendRecipientEmail", + writable: false, + enumerable: false, + configurable: false +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dde20e42c..1000648b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -445,8 +445,8 @@ importers: specifier: 4.4.3 version: 4.4.3(supports-color@8.1.1) devalue: - specifier: 5.4.1 - version: 5.4.1 + specifier: 5.5.0 + version: 5.5.0 ms: specifier: 2.1.3 version: 2.1.3 @@ -6869,8 +6869,8 @@ packages: peerDependencies: typescript: ^5.4.4 - devalue@5.4.1: - resolution: {integrity: sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ==} + devalue@5.5.0: + resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -15724,7 +15724,7 @@ snapshots: '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.1 + devalue: 5.5.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -15746,7 +15746,7 @@ snapshots: '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.1 + devalue: 5.5.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -17634,7 +17634,7 @@ snapshots: transitivePeerDependencies: - supports-color - devalue@5.4.1: {} + devalue@5.5.0: {} devlop@1.1.0: dependencies: @@ -20372,7 +20372,7 @@ snapshots: cookie-es: 2.0.0 defu: 6.1.4 destr: 2.0.5 - devalue: 5.4.1 + devalue: 5.5.0 errx: 0.1.0 esbuild: 0.25.11 escape-string-regexp: 5.0.0 @@ -20498,7 +20498,7 @@ snapshots: cookie-es: 2.0.0 defu: 6.1.4 destr: 2.0.5 - devalue: 5.4.1 + devalue: 5.5.0 errx: 0.1.0 esbuild: 0.25.11 escape-string-regexp: 5.0.0 @@ -23130,7 +23130,7 @@ snapshots: std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) @@ -23172,7 +23172,7 @@ snapshots: std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index 7a7f31627..bd4f1faa7 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -491,3 +491,24 @@ export async function hookCleanupTestWorkflow( hookCleanupTestData: 'workflow_completed', }; } + +////////////////////////////////////////////////////////// + +export async function stepFunctionPassingWorkflow() { + 'use workflow'; + // Pass a step function reference to another step + const result = await stepWithStepFunctionArg(doubleNumber); + return result; +} + +async function stepWithStepFunctionArg(stepFn: (x: number) => Promise) { + 'use step'; + // Call the passed step function reference + const result = await stepFn(10); + return result * 2; +} + +async function doubleNumber(x: number) { + 'use step'; + return x * 2; +}