From 5df67393a1ab8f65aa49143bf0a5e4bdde0e1d0b Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Jan 2026 16:46:26 -0800 Subject: [PATCH 1/5] Add support for "use step" functions in class instance methods --- .changeset/legal-parts-happen.md | 5 + packages/core/e2e/e2e.test.ts | 88 ++++++ .../swc-plugin-workflow/transform/src/lib.rs | 290 ++++++++++++++++-- .../forbidden-expressions/output-step.js | 6 +- .../forbidden-expressions/output-step.stderr | 10 - .../forbidden-expressions/output-workflow.js | 10 +- .../output-workflow.stderr | 10 - .../tests/errors/instance-methods/input.js | 10 +- .../errors/instance-methods/output-client.js | 13 +- .../instance-methods/output-client.stderr | 12 +- .../errors/instance-methods/output-step.js | 14 +- .../instance-methods/output-step.stderr | 12 +- .../instance-methods/output-workflow.js | 12 +- .../instance-methods/output-workflow.stderr | 12 +- .../fixture/instance-method-step/input.js | 25 ++ .../instance-method-step/output-client.js | 23 ++ .../instance-method-step/output-step.js | 26 ++ .../instance-method-step/output-workflow.js | 19 ++ workbench/example/workflows/99_e2e.ts | 81 +++++ 19 files changed, 567 insertions(+), 111 deletions(-) create mode 100644 .changeset/legal-parts-happen.md create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js diff --git a/.changeset/legal-parts-happen.md b/.changeset/legal-parts-happen.md new file mode 100644 index 0000000000..214b97c63e --- /dev/null +++ b/.changeset/legal-parts-happen.md @@ -0,0 +1,5 @@ +--- +"@workflow/swc-plugin": patch +--- + +Add support for "use step" functions in class instance methods diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index f055cbb8eb..e8a180df7b 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -1413,6 +1413,94 @@ describe('e2e', () => { scaledAgain: { x: 18, y: 24 }, sum: { x: 9, y: 12 }, }); + }); + + test( + 'instanceMethodStepWorkflow - instance methods with "use step" directive', + { timeout: 60_000 }, + async () => { + // This workflow tests instance methods marked with "use step". + // The Counter class has custom serialization so the `this` context + // (the Counter instance) can be serialized across the workflow/step boundary. + // + // instanceMethodStepWorkflow(5) should: + // 1. Create Counter(5) + // 2. counter.add(10) -> 5 + 10 = 15 + // 3. counter.multiply(3) -> 5 * 3 = 15 + // 4. counter.describe('test counter') -> { label: 'test counter', value: 5 } + // 5. Create Counter(100), call counter2.add(50) -> 100 + 50 = 150 + const run = await triggerWorkflow('instanceMethodStepWorkflow', [5]); + const returnValue = await getWorkflowReturnValue(run.runId); + + expect(returnValue).toEqual({ + initialValue: 5, + added: 15, // 5 + 10 + multiplied: 15, // 5 * 3 + description: { label: 'test counter', value: 5 }, + added2: 150, // 100 + 50 + }); + + // Verify the run completed successfully + const { json: runData } = await cliInspectJson( + `runs ${run.runId} --withData` + ); + expect(runData.status).toBe('completed'); + expect(runData.output).toEqual({ + initialValue: 5, + added: 15, + multiplied: 15, + description: { label: 'test counter', value: 5 }, + added2: 150, + }); + + // Verify the steps were executed (should have 4 steps: add, multiply, describe, add) + const { json: steps } = await cliInspectJson( + `steps --runId ${run.runId}` + ); + // Filter to only Counter instance method steps + const counterSteps = steps.filter( + (s: any) => + s.stepName.includes('Counter#add') || + s.stepName.includes('Counter#multiply') || + s.stepName.includes('Counter#describe') + ); + expect(counterSteps.length).toBe(4); // add, multiply, describe, add (from counter2) + expect(counterSteps.every((s: any) => s.status === 'completed')).toBe( + true + ); + } + ); +}); + +// ==================== PAGES ROUTER TESTS ==================== +// Tests for Next.js Pages Router API endpoint (only runs for nextjs-turbopack and nextjs-webpack) +const isNextJsApp = + process.env.APP_NAME === 'nextjs-turbopack' || + process.env.APP_NAME === 'nextjs-webpack'; + +describe.skipIf(!isNextJsApp)('pages router', () => { + test('addTenWorkflow via pages router', { timeout: 60_000 }, async () => { + const run = await triggerWorkflow( + { + workflowFile: 'workflows/99_e2e.ts', + workflowFn: 'addTenWorkflow', + }, + [123], + { usePagesRouter: true } + ); + const returnValue = await getWorkflowReturnValue(run.runId); + expect(returnValue).toBe(133); + }); + + test( + 'promiseAllWorkflow via pages router', + { timeout: 60_000 }, + async () => { + const run = await triggerWorkflow('promiseAllWorkflow', [], { + usePagesRouter: true, + }); + const returnValue = await getWorkflowReturnValue(run.runId); + expect(returnValue).toBe('ABC'); } ); diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index 549b153eb5..fff66c6510 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -368,6 +368,12 @@ pub struct StepTransform { // Track static step methods to strip from class and assign as properties (workflow mode) // (class_name, method_name, step_id) static_step_methods_to_strip: Vec<(String, String, String)>, + // Track instance method steps that need registration after the class declaration + // (class_name, method_name, step_id, span) + instance_method_step_registrations: Vec<(String, String, String, swc_core::common::Span)>, + // Track instance step methods to strip from class and assign as properties (workflow mode) + // (class_name, method_name, step_id) + instance_step_methods_to_strip: Vec<(String, String, String)>, // Track classes that need serialization registration (for `this` serialization in static methods) // Set of class names that have static step/workflow methods classes_needing_serialization: HashSet, @@ -1239,6 +1245,8 @@ impl StepTransform { static_method_step_registrations: Vec::new(), static_method_workflow_registrations: Vec::new(), static_step_methods_to_strip: Vec::new(), + instance_method_step_registrations: Vec::new(), + instance_step_methods_to_strip: Vec::new(), classes_needing_serialization: HashSet::new(), serialization_symbol_identifiers: HashMap::new(), classes_for_manifest: HashSet::new(), @@ -3514,7 +3522,8 @@ impl VisitMut for StepTransform { let needs_register_import = !self.registration_calls.is_empty() || !self.object_property_step_functions.is_empty() || !self.nested_step_functions.is_empty() - || !self.static_method_step_registrations.is_empty(); + || !self.static_method_step_registrations.is_empty() + || !self.instance_method_step_registrations.is_empty(); // Check if any nested steps have closure variables let needs_closure_import = self @@ -3876,6 +3885,61 @@ impl VisitMut for StepTransform { module.body.push(ModuleItem::Stmt(registration_call)); } + // Add instance method step registrations + // For instance methods, we register ClassName.prototype.methodName + for (class_name, method_name, step_id, _span) in + self.instance_method_step_registrations.drain(..) + { + let registration_call = Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Ident(Ident::new( + "registerStepFunction".into(), + DUMMY_SP, + SyntaxContext::empty(), + )))), + args: vec![ + // First argument: step ID + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: step_id.into(), + raw: None, + }))), + }, + // Second argument: ClassName.prototype.methodName + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + class_name.into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Ident(IdentName::new( + "prototype".into(), + DUMMY_SP, + )), + })), + prop: MemberProp::Ident(IdentName::new( + method_name.into(), + DUMMY_SP, + )), + })), + }, + ], + type_args: None, + })), + }); + module.body.push(ModuleItem::Stmt(registration_call)); + } + // Add class serialization registrations for step mode // In step mode, we need: // 1. registerSerializationClass(classId, ClassName) - for deserialization @@ -4008,6 +4072,95 @@ impl VisitMut for StepTransform { module.body.push(ModuleItem::Stmt(assignment)); } + // Add instance step method property assignments (workflow mode) + // These methods were stripped from the class and need to be assigned as prototype properties + for (class_name, method_name, step_id) in + self.instance_step_methods_to_strip.drain(..) + { + // Create: ClassName.prototype.methodName = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id") + let proxy_expr = 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( + "globalThis".into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Computed(ComputedPropName { + 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( + "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_USE_STEP".into(), + raw: None, + }))), + }], + type_args: None, + })), + }), + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: step_id.into(), + raw: None, + }))), + }], + type_args: None, + }); + + // Create: ClassName.prototype.methodName = proxy_expr + let assignment = Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + left: AssignTarget::Simple(SimpleAssignTarget::Member( + MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + class_name.into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Ident(IdentName::new( + "prototype".into(), + DUMMY_SP, + )), + })), + prop: MemberProp::Ident(IdentName::new( + method_name.into(), + DUMMY_SP, + )), + }, + )), + op: AssignOp::Assign, + right: Box::new(proxy_expr), + })), + }); + module.body.push(ModuleItem::Stmt(assignment)); + } + // Add class serialization registrations for workflow mode // This is now the same as step mode - using registerSerializationClass() // which sets both classId and registers in the globalThis Map @@ -6349,22 +6502,31 @@ impl VisitMut for StepTransform { // Visit the class body (this populates static_step_methods_to_strip) class_decl.class.visit_mut_with(self); - // In workflow mode, remove static step methods from the class body + // In workflow mode, remove static and instance step methods from the class body if matches!(self.mode, TransformMode::Workflow) { - let methods_to_strip: Vec<_> = self + let static_methods_to_strip: Vec<_> = self .static_step_methods_to_strip .iter() .filter(|(cn, _, _)| cn == &class_name) .map(|(_, mn, _)| mn.clone()) .collect(); - if !methods_to_strip.is_empty() { + let instance_methods_to_strip: Vec<_> = self + .instance_step_methods_to_strip + .iter() + .filter(|(cn, _, _)| cn == &class_name) + .map(|(_, mn, _)| mn.clone()) + .collect(); + + if !static_methods_to_strip.is_empty() || !instance_methods_to_strip.is_empty() { class_decl.class.body.retain(|member| { if let ClassMember::Method(method) = member { - if method.is_static { - if let PropName::Ident(ident) = &method.key { - let method_name = ident.sym.to_string(); - return !methods_to_strip.contains(&method_name); + if let PropName::Ident(ident) = &method.key { + let method_name = ident.sym.to_string(); + if method.is_static { + return !static_methods_to_strip.contains(&method_name); + } else { + return !instance_methods_to_strip.contains(&method_name); } } } @@ -6405,22 +6567,31 @@ impl VisitMut for StepTransform { // Visit the class body (this populates static_step_methods_to_strip) class_expr.class.visit_mut_with(self); - // In workflow mode, remove static step methods from the class body + // In workflow mode, remove static and instance step methods from the class body if matches!(self.mode, TransformMode::Workflow) { - let methods_to_strip: Vec<_> = self + let static_methods_to_strip: Vec<_> = self .static_step_methods_to_strip .iter() .filter(|(cn, _, _)| cn == &internal_class_name) .map(|(_, mn, _)| mn.clone()) .collect(); - if !methods_to_strip.is_empty() { + let instance_methods_to_strip: Vec<_> = self + .instance_step_methods_to_strip + .iter() + .filter(|(cn, _, _)| cn == &internal_class_name) + .map(|(_, mn, _)| mn.clone()) + .collect(); + + if !static_methods_to_strip.is_empty() || !instance_methods_to_strip.is_empty() { class_expr.class.body.retain(|member| { if let ClassMember::Method(method) = member { - if method.is_static { - if let PropName::Ident(ident) = &method.key { - let method_name = ident.sym.to_string(); - return !methods_to_strip.contains(&method_name); + if let PropName::Ident(ident) = &method.key { + let method_name = ident.sym.to_string(); + if method.is_static { + return !static_methods_to_strip.contains(&method_name); + } else { + return !instance_methods_to_strip.contains(&method_name); } } } @@ -6436,20 +6607,12 @@ impl VisitMut for StepTransform { // Handle class methods fn visit_mut_class_method(&mut self, method: &mut ClassMethod) { if !method.is_static { - // Instance methods can't be step/workflow functions + // Instance methods can have "use step" (but not "use workflow") let has_step = self.has_use_step_directive(&method.function.body); let has_workflow = self.has_use_workflow_directive(&method.function.body); - if has_step { - HANDLER.with(|handler| { - handler - .struct_span_err( - method.span, - "Instance methods cannot be marked with \"use step\". Only static methods, functions, and object methods are supported.", - ) - .emit() - }); - } else if has_workflow { + if has_workflow { + // Workflows on instance methods don't make sense (workflows are entry points) HANDLER.with(|handler| { handler .struct_span_err( @@ -6458,6 +6621,81 @@ impl VisitMut for StepTransform { ) .emit() }); + } else if has_step { + // Instance methods with "use step" are supported + // Validate async + if !method.function.is_async { + emit_error(WorkflowErrorKind::NonAsyncFunction { + span: method.function.span, + directive: "use step", + }); + return; + } + + // Get method name + let method_name = match &method.key { + PropName::Ident(ident) => ident.sym.to_string(), + PropName::Str(s) => s.value.to_string_lossy().to_string(), + _ => { + // Complex key - skip + method.visit_mut_children_with(self); + return; + } + }; + + // Get class name (must be set by visit_mut_class) + let class_name = match &self.current_class_name { + Some(name) => name.clone(), + None => { + // No class context - shouldn't happen, but fall back + method.visit_mut_children_with(self); + return; + } + }; + + // Generate full qualified name using # for instance methods: ClassName#methodName + let full_name = format!("{}#{}", class_name, method_name); + + self.step_function_names.insert(full_name.clone()); + + // Track class for serialization (needed for `this` serialization) + self.classes_needing_serialization + .insert(class_name.clone()); + + // Generate step ID + let step_id = self.create_id(Some(&full_name), method.function.span, false); + + match self.mode { + TransformMode::Step => { + // Remove directive + self.remove_use_step_directive(&mut method.function.body); + + // Track for registration after class (will use prototype) + self.instance_method_step_registrations.push(( + class_name.clone(), + method_name.clone(), + step_id, + method.function.span, + )); + } + TransformMode::Workflow => { + // Remove directive for consistency with other modes + self.remove_use_step_directive(&mut method.function.body); + + // Track this method to be stripped from the class and assigned as a property + self.instance_step_methods_to_strip.push(( + class_name.clone(), + method_name.clone(), + step_id, + )); + } + TransformMode::Client => { + // Just remove directive, keep the function body + self.remove_use_step_directive(&mut method.function.body); + } + } + } else { + method.visit_mut_children_with(self); } } else { // Static methods can be step/workflow functions diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js index 15148cb4d9..1439dcd333 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js @@ -1,5 +1,6 @@ import { registerStepFunction } from "workflow/internal/private"; -/**__internal_workflows{"steps":{"input.js":{"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}}}*/; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +/**__internal_workflows{"steps":{"input.js":{"TestClass#stepMethod":{"stepId":"step//input.js//TestClass#stepMethod"},"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export async function stepWithThis() { // Error: this is not allowed return this.value; @@ -10,10 +11,11 @@ export async function stepWithArguments() { } class TestClass extends BaseClass { async stepMethod() { - 'use step'; // Error: super is not allowed return super.method(); } } registerStepFunction("step//input.js//stepWithThis", stepWithThis); registerStepFunction("step//input.js//stepWithArguments", stepWithArguments); +registerStepFunction("step//input.js//TestClass#stepMethod", TestClass.prototype.stepMethod); +registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr index b105eb0105..e69de29bb2 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr @@ -1,10 +0,0 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:14:1] - 13 | class TestClass extends BaseClass { - 14 | ,-> async stepMethod() { - 15 | | 'use step'; - 16 | | // Error: super is not allowed - 17 | | return super.method(); - 18 | `-> } - 19 | } - `---- 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 ad4ff9d306..3d77751b04 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 @@ -1,10 +1,8 @@ -/**__internal_workflows{"steps":{"input.js":{"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}}}*/; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +/**__internal_workflows{"steps":{"input.js":{"TestClass#stepMethod":{"stepId":"step//input.js//TestClass#stepMethod"},"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export var stepWithThis = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWithThis"); export var stepWithArguments = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWithArguments"); class TestClass extends BaseClass { - async stepMethod() { - 'use step'; - // Error: super is not allowed - return super.method(); - } } +TestClass.prototype.stepMethod = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#stepMethod"); +registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr index b105eb0105..e69de29bb2 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr @@ -1,10 +0,0 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:14:1] - 13 | class TestClass extends BaseClass { - 14 | ,-> async stepMethod() { - 15 | | 'use step'; - 16 | | // Error: super is not allowed - 17 | | return super.method(); - 18 | `-> } - 19 | } - `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js index 8bb091ca30..02d3ebd368 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js @@ -1,17 +1,17 @@ export class TestClass { - // Error: instance methods can't have directives + // OK: instance methods can have "use step" directive async instanceMethod() { 'use step'; - return 'not allowed'; + return 'allowed'; } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } - // This is ok - static methods can have directives + // OK: static methods can have directives static async staticMethod() { 'use step'; return 'allowed'; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js index c172cdc750..510a55b473 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js @@ -1,17 +1,16 @@ import { registerSerializationClass } from "workflow/internal/class-serialization"; -/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"TestClass#instanceMethod":{"stepId":"step//input.js//TestClass#instanceMethod"},"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export class TestClass { - // Error: instance methods can't have directives + // OK: instance methods can have "use step" directive async instanceMethod() { - 'use step'; - return 'not allowed'; + return 'allowed'; } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } - // This is ok - static methods can have directives + // OK: static methods can have directives static async staticMethod() { return 'allowed'; } diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr index 815e44d29c..4f8b62f939 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr @@ -1,16 +1,8 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:3:1] - 2 | // Error: instance methods can't have directives - 3 | ,-> async instanceMethod() { - 4 | | 'use step'; - 5 | | return 'not allowed'; - 6 | `-> } - `---- x Instance methods cannot be marked with "use workflow". Only static methods, functions, and object methods are supported. ,-[input.js:9:1] - 8 | // Error: instance methods can't have workflow directive either + 8 | // Error: instance methods can't have "use workflow" directive 9 | ,-> async anotherInstance() { 10 | | 'use workflow'; - 11 | | return 'also not allowed'; + 11 | | return 'not allowed'; 12 | `-> } `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js index 9d576d861b..5197214566 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js @@ -1,21 +1,21 @@ import { registerStepFunction } from "workflow/internal/private"; import { registerSerializationClass } from "workflow/internal/class-serialization"; -/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"TestClass#instanceMethod":{"stepId":"step//input.js//TestClass#instanceMethod"},"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export class TestClass { - // Error: instance methods can't have directives + // OK: instance methods can have "use step" directive async instanceMethod() { - 'use step'; - return 'not allowed'; + return 'allowed'; } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } - // This is ok - static methods can have directives + // OK: static methods can have directives static async staticMethod() { return 'allowed'; } } registerStepFunction("step//input.js//TestClass.staticMethod", TestClass.staticMethod); +registerStepFunction("step//input.js//TestClass#instanceMethod", TestClass.prototype.instanceMethod); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr index 815e44d29c..4f8b62f939 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr @@ -1,16 +1,8 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:3:1] - 2 | // Error: instance methods can't have directives - 3 | ,-> async instanceMethod() { - 4 | | 'use step'; - 5 | | return 'not allowed'; - 6 | `-> } - `---- x Instance methods cannot be marked with "use workflow". Only static methods, functions, and object methods are supported. ,-[input.js:9:1] - 8 | // Error: instance methods can't have workflow directive either + 8 | // Error: instance methods can't have "use workflow" directive 9 | ,-> async anotherInstance() { 10 | | 'use workflow'; - 11 | | return 'also not allowed'; + 11 | | return 'not allowed'; 12 | `-> } `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js index 1ea89bfe3b..d9d219540d 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js @@ -1,16 +1,12 @@ import { registerSerializationClass } from "workflow/internal/class-serialization"; -/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"TestClass#instanceMethod":{"stepId":"step//input.js//TestClass#instanceMethod"},"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export class TestClass { - // Error: instance methods can't have directives - async instanceMethod() { - 'use step'; - return 'not allowed'; - } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } } TestClass.staticMethod = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass.staticMethod"); +TestClass.prototype.instanceMethod = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#instanceMethod"); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr index 815e44d29c..4f8b62f939 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr @@ -1,16 +1,8 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:3:1] - 2 | // Error: instance methods can't have directives - 3 | ,-> async instanceMethod() { - 4 | | 'use step'; - 5 | | return 'not allowed'; - 6 | `-> } - `---- x Instance methods cannot be marked with "use workflow". Only static methods, functions, and object methods are supported. ,-[input.js:9:1] - 8 | // Error: instance methods can't have workflow directive either + 8 | // Error: instance methods can't have "use workflow" directive 9 | ,-> async anotherInstance() { 10 | | 'use workflow'; - 11 | | return 'also not allowed'; + 11 | | return 'not allowed'; 12 | `-> } `---- diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/input.js new file mode 100644 index 0000000000..88af31bf34 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/input.js @@ -0,0 +1,25 @@ +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; + +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { multiplier: instance.multiplier }; + } + + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + + constructor(multiplier) { + this.multiplier = multiplier; + } + + async multiply(value) { + 'use step'; + return value * this.multiplier; + } + + async add(a, b) { + 'use step'; + return a + b + this.multiplier; + } +} diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js new file mode 100644 index 0000000000..dc0c021ef5 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js @@ -0,0 +1,23 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Calculator#add":{"stepId":"step//input.js//Calculator#add"},"Calculator#multiply":{"stepId":"step//input.js//Calculator#multiply"}}},"classes":{"input.js":{"Calculator":{"classId":"class//input.js//Calculator"}}}}*/; +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { + multiplier: instance.multiplier + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + constructor(multiplier){ + this.multiplier = multiplier; + } + async multiply(value) { + return value * this.multiplier; + } + async add(a, b) { + return a + b + this.multiplier; + } +} +registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js new file mode 100644 index 0000000000..9d4b2be349 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js @@ -0,0 +1,26 @@ +import { registerStepFunction } from "workflow/internal/private"; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Calculator#add":{"stepId":"step//input.js//Calculator#add"},"Calculator#multiply":{"stepId":"step//input.js//Calculator#multiply"}}},"classes":{"input.js":{"Calculator":{"classId":"class//input.js//Calculator"}}}}*/; +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { + multiplier: instance.multiplier + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + constructor(multiplier){ + this.multiplier = multiplier; + } + async multiply(value) { + return value * this.multiplier; + } + async add(a, b) { + return a + b + this.multiplier; + } +} +registerStepFunction("step//input.js//Calculator#multiply", Calculator.prototype.multiply); +registerStepFunction("step//input.js//Calculator#add", Calculator.prototype.add); +registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js new file mode 100644 index 0000000000..f0c9fe78df --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js @@ -0,0 +1,19 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Calculator#add":{"stepId":"step//input.js//Calculator#add"},"Calculator#multiply":{"stepId":"step//input.js//Calculator#multiply"}}},"classes":{"input.js":{"Calculator":{"classId":"class//input.js//Calculator"}}}}*/; +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { + multiplier: instance.multiplier + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + constructor(multiplier){ + this.multiplier = multiplier; + } +} +Calculator.prototype.multiply = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#multiply"); +Calculator.prototype.add = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#add"); +registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index c367072d46..917717cf4d 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -1147,3 +1147,84 @@ export async function crossContextSerdeWorkflow() { arraySum: { x: arraySum.x, y: arraySum.y, z: arraySum.z }, }; } + +////////////////////////////////////////////////////////// +// Instance Method Step Tests +////////////////////////////////////////////////////////// + +/** + * A class with instance methods that are marked as steps. + * This tests the new "use step" support for instance methods. + * The class uses custom serialization so the `this` value can be + * serialized across the workflow/step boundary. + */ +export class Counter { + constructor(public value: number) {} + + /** Custom serialization - converts instance to plain object */ + static [Symbol.for('workflow-serialize')](instance: Counter) { + return { value: instance.value }; + } + + /** Custom deserialization - reconstructs instance from plain object */ + static [Symbol.for('workflow-deserialize')](data: { value: number }) { + return new Counter(data.value); + } + + /** + * Instance method step: adds the given amount to the counter's value. + * The `this` context (the Counter instance) is serialized and passed + * to the step handler, then deserialized before the method is called. + */ + async add(amount: number): Promise { + 'use step'; + return this.value + amount; + } + + /** + * Instance method step: multiplies the counter's value by the given factor. + */ + async multiply(factor: number): Promise { + 'use step'; + return this.value * factor; + } + + /** + * Instance method step: returns an object with both the original and computed values. + * This tests that `this` is correctly preserved through the step execution. + */ + async describe(label: string): Promise<{ label: string; value: number }> { + 'use step'; + return { label, value: this.value }; + } +} + +/** + * Workflow that tests instance method steps. + * Creates Counter instances and calls their instance methods as steps. + * The `this` context (the Counter instance) should be serialized and + * correctly restored when the step executes. + */ +export async function instanceMethodStepWorkflow(initialValue: number) { + 'use workflow'; + + // Create a Counter instance + const counter = new Counter(initialValue); + + // Call instance method steps + const added = await counter.add(10); + const multiplied = await counter.multiply(3); + const description = await counter.describe('test counter'); + + // Create another counter to verify different instances work + const counter2 = new Counter(100); + const added2 = await counter2.add(50); + + return { + initialValue, + added, // initialValue + 10 + multiplied, // initialValue * 3 + description, // { label: 'test counter', value: initialValue } + added2, // 100 + 50 = 150 + }; +} From 5abaf330006fe9bd8a215db96e6008971f470089 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 14 Jan 2026 11:54:48 -0800 Subject: [PATCH 2/5] Vade suggestion --- .../swc-plugin-workflow/transform/src/lib.rs | 20 +++++++++++++ .../instance-method-nested-step/input.js | 29 +++++++++++++++++++ .../output-client.js | 24 +++++++++++++++ .../output-step.js | 28 ++++++++++++++++++ .../output-workflow.js | 18 ++++++++++++ 5 files changed, 119 insertions(+) create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index fff66c6510..6a84d8b7bc 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -6677,6 +6677,9 @@ impl VisitMut for StepTransform { step_id, method.function.span, )); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } TransformMode::Workflow => { // Remove directive for consistency with other modes @@ -6688,10 +6691,15 @@ impl VisitMut for StepTransform { method_name.clone(), step_id, )); + // Note: No need to visit children in Workflow mode since the method body + // will be stripped and replaced with a proxy call } TransformMode::Client => { // Just remove directive, keep the function body self.remove_use_step_directive(&mut method.function.body); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } } } else { @@ -6760,6 +6768,9 @@ impl VisitMut for StepTransform { step_id, method.function.span, )); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } TransformMode::Workflow => { // Remove directive for consistency with other modes @@ -6775,10 +6786,15 @@ impl VisitMut for StepTransform { method_name.clone(), step_id, )); + // Note: No need to visit children in Workflow mode since the method body + // will be stripped and replaced with a proxy call } TransformMode::Client => { // Just remove directive, keep the function body self.remove_use_step_directive(&mut method.function.body); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } } } else if has_workflow { @@ -6800,9 +6816,13 @@ impl VisitMut for StepTransform { workflow_id, method.function.span, )); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } TransformMode::Step | TransformMode::Client => { // Remove directive and replace body with error + // No need to visit children since the body is replaced self.remove_use_workflow_directive(&mut method.function.body); // Generate workflow ID diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/input.js new file mode 100644 index 0000000000..180d4cea4a --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/input.js @@ -0,0 +1,29 @@ +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; + +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { value: instance.value }; + } + + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + + constructor(value) { + this.value = value; + } + + // Instance method step that contains a nested step function + async process(input) { + 'use step'; + + // This nested step should be transformed + const helper = async (x) => { + 'use step'; + return x * 2; + }; + + const doubled = await helper(input); + return doubled + this.value; + } +} diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js new file mode 100644 index 0000000000..9c4dc9da22 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js @@ -0,0 +1,24 @@ +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}}}*/; +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { + value: instance.value + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + constructor(value){ + this.value = value; + } + // Instance method step that contains a nested step function + async process(input) { + // This nested step should be transformed + const helper = async (x)=>{ + return x * 2; + }; + const doubled = await helper(input); + return doubled + this.value; + } +} diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js new file mode 100644 index 0000000000..2e4b95a952 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js @@ -0,0 +1,28 @@ +import { registerStepFunction } from "workflow/internal/private"; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}}}*/; +var helper = async (x)=>x * 2; +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { + value: instance.value + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + constructor(value){ + this.value = value; + } + // Instance method step that contains a nested step function + async process(input) { + // This nested step should be transformed + const helper = helper; + const doubled = await helper(input); + return doubled + this.value; + } +} +registerStepFunction("step//input.js//helper", helper); +registerStepFunction("step//input.js//Service#process", Service.prototype.process); +registerSerializationClass("class//input.js//Service", Service); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js new file mode 100644 index 0000000000..d8b65b6c4d --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js @@ -0,0 +1,18 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"}}}}*/; +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { + value: instance.value + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + constructor(value){ + this.value = value; + } +} +Service.prototype.process = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Service#process"); +registerSerializationClass("class//input.js//Service", Service); From 543bfa7467368edddb75be70654d7a2fddf0c83e Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 19 Jan 2026 14:47:52 -0800 Subject: [PATCH 3/5] . --- packages/core/e2e/e2e.test.ts | 36 ++--------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index e8a180df7b..d1be775de9 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -1413,7 +1413,8 @@ describe('e2e', () => { scaledAgain: { x: 18, y: 24 }, sum: { x: 9, y: 12 }, }); - }); + } + ); test( 'instanceMethodStepWorkflow - instance methods with "use step" directive', @@ -1470,39 +1471,6 @@ describe('e2e', () => { ); } ); -}); - -// ==================== PAGES ROUTER TESTS ==================== -// Tests for Next.js Pages Router API endpoint (only runs for nextjs-turbopack and nextjs-webpack) -const isNextJsApp = - process.env.APP_NAME === 'nextjs-turbopack' || - process.env.APP_NAME === 'nextjs-webpack'; - -describe.skipIf(!isNextJsApp)('pages router', () => { - test('addTenWorkflow via pages router', { timeout: 60_000 }, async () => { - const run = await triggerWorkflow( - { - workflowFile: 'workflows/99_e2e.ts', - workflowFn: 'addTenWorkflow', - }, - [123], - { usePagesRouter: true } - ); - const returnValue = await getWorkflowReturnValue(run.runId); - expect(returnValue).toBe(133); - }); - - test( - 'promiseAllWorkflow via pages router', - { timeout: 60_000 }, - async () => { - const run = await triggerWorkflow('promiseAllWorkflow', [], { - usePagesRouter: true, - }); - const returnValue = await getWorkflowReturnValue(run.runId); - expect(returnValue).toBe('ABC'); - } - ); test( 'crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context', From ea87a016757ed37da4ddaf42dd199a3bc40316c4 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 27 Jan 2026 16:31:05 -0800 Subject: [PATCH 4/5] . --- .../fixture/instance-method-nested-step/output-client.js | 4 +++- .../tests/fixture/instance-method-nested-step/output-step.js | 2 +- .../fixture/instance-method-nested-step/output-workflow.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js index 9c4dc9da22..6aec6db9df 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js @@ -1,5 +1,6 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; -/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}},"classes":{"input.js":{"Service":{"classId":"class//input.js//Service"}}}}*/; export class Service { static [WORKFLOW_SERIALIZE](instance) { return { @@ -22,3 +23,4 @@ export class Service { return doubled + this.value; } } +registerSerializationClass("class//input.js//Service", Service); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js index 2e4b95a952..522c60f518 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js @@ -1,7 +1,7 @@ import { registerStepFunction } from "workflow/internal/private"; import { registerSerializationClass } from "workflow/internal/class-serialization"; import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; -/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}},"classes":{"input.js":{"Service":{"classId":"class//input.js//Service"}}}}*/; var helper = async (x)=>x * 2; export class Service { static [WORKFLOW_SERIALIZE](instance) { diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js index d8b65b6c4d..2614c8e2cd 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js @@ -1,6 +1,6 @@ import { registerSerializationClass } from "workflow/internal/class-serialization"; import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; -/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"}}},"classes":{"input.js":{"Service":{"classId":"class//input.js//Service"}}}}*/; export class Service { static [WORKFLOW_SERIALIZE](instance) { return { From 16dd8598a5321a88114bffe8dd632b7418840993 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 2 Feb 2026 23:48:30 -0800 Subject: [PATCH 5/5] Address review feedback --- packages/swc-plugin-workflow/spec.md | 55 ++++++++++++++++++- .../swc-plugin-workflow/transform/src/lib.rs | 54 ++++++++++++++---- .../forbidden-expressions/output-step.stderr | 0 .../output-workflow.stderr | 0 .../errors/instance-methods/output-step.js | 2 +- .../instance-methods/output-workflow.js | 2 +- .../output-step.js | 8 +-- .../output-workflow.js | 2 +- .../instance-method-step/output-step.js | 4 +- .../instance-method-step/output-workflow.js | 4 +- .../step-with-this-arguments-super}/input.js | 6 +- .../output-client.js | 17 ++++++ .../output-step.js | 8 +-- .../output-workflow.js | 2 +- workbench/example/workflows/99_e2e.ts | 2 +- 15 files changed, 135 insertions(+), 31 deletions(-) delete mode 100644 packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr delete mode 100644 packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr rename packages/swc-plugin-workflow/transform/tests/{errors/forbidden-expressions => fixture/step-with-this-arguments-super}/input.js (67%) create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js rename packages/swc-plugin-workflow/transform/tests/{errors/forbidden-expressions => fixture/step-with-this-arguments-super}/output-step.js (85%) rename packages/swc-plugin-workflow/transform/tests/{errors/forbidden-expressions => fixture/step-with-this-arguments-super}/output-workflow.js (85%) diff --git a/packages/swc-plugin-workflow/spec.md b/packages/swc-plugin-workflow/spec.md index ff803b1a87..11ac2da33a 100644 --- a/packages/swc-plugin-workflow/spec.md +++ b/packages/swc-plugin-workflow/spec.md @@ -42,6 +42,7 @@ Examples: - `step//src/jobs/order.ts//fetchData` - `step//src/jobs/order.ts//processOrder/innerStep` (nested step) - `step//src/jobs/order.ts//MyClass.staticMethod` (static method) +- `step//src/jobs/order.ts//MyClass#instanceMethod` (instance method) - `class//src/models/Point.ts//Point` (serialization class) --- @@ -171,6 +172,57 @@ function wrapper(multiplier) { registerStepFunction("step//input.js//wrapper/_anonymousStep0", wrapper$_anonymousStep0); ``` +### Instance Method Step + +Instance methods can use `"use step"` if the class provides custom serialization methods. The `this` context is serialized when calling the step and deserialized before execution. + +Input: +```javascript +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; + +export class Counter { + static [WORKFLOW_SERIALIZE](instance) { + return { value: instance.value }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Counter(data.value); + } + constructor(value) { + this.value = value; + } + async add(amount) { + 'use step'; + return this.value + amount; + } +} +``` + +Output: +```javascript +import { registerStepFunction } from "workflow/internal/private"; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Counter#add":{"stepId":"step//input.js//Counter#add"}}},"classes":{"input.js":{"Counter":{"classId":"class//input.js//Counter"}}}}*/; +export class Counter { + static [WORKFLOW_SERIALIZE](instance) { + return { value: instance.value }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Counter(data.value); + } + constructor(value) { + this.value = value; + } + async add(amount) { + return this.value + amount; + } +} +registerStepFunction("step//input.js//Counter#add", Counter.prototype["add"]); +registerSerializationClass("class//input.js//Counter", Counter); +``` + +Note: Instance methods use `#` in the step ID (e.g., `Counter#add`) and are registered via `ClassName.prototype["methodName"]`. + ### Module-Level Directive Input: @@ -591,7 +643,7 @@ The plugin emits errors for invalid usage: | Error | Description | |-------|-------------| | Non-async function | Functions with `"use step"` or `"use workflow"` must be async | -| Instance methods | Only static methods can have directives (not instance methods) | +| Instance methods with `"use workflow"` | Only static methods can have `"use workflow"` (not instance methods) | | Misplaced directive | Directive must be at top of file or start of function body | | Conflicting directives | Cannot have both `"use step"` and `"use workflow"` at module level | | Invalid exports | Module-level directive files can only export async functions | @@ -610,6 +662,7 @@ The plugin supports various function declaration styles: - `const name = async function() { "use step"; }` - Function expression - `{ async method() { "use step"; } }` - Object method - `static async method() { "use step"; }` - Static class method +- `async method() { "use step"; }` - Instance class method (requires custom serialization) --- diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index 6a84d8b7bc..f5889ad028 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -3927,10 +3927,14 @@ impl VisitMut for StepTransform { DUMMY_SP, )), })), - prop: MemberProp::Ident(IdentName::new( - method_name.into(), - DUMMY_SP, - )), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: method_name.into(), + raw: None, + }))), + }), })), }, ], @@ -4148,10 +4152,14 @@ impl VisitMut for StepTransform { DUMMY_SP, )), })), - prop: MemberProp::Ident(IdentName::new( - method_name.into(), - DUMMY_SP, - )), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: method_name.into(), + raw: None, + }))), + }), }, )), op: AssignOp::Assign, @@ -6521,8 +6529,14 @@ impl VisitMut for StepTransform { if !static_methods_to_strip.is_empty() || !instance_methods_to_strip.is_empty() { class_decl.class.body.retain(|member| { if let ClassMember::Method(method) = member { - if let PropName::Ident(ident) = &method.key { - let method_name = ident.sym.to_string(); + // Handle both identifier and string keys for method names + let method_name = match &method.key { + PropName::Ident(ident) => Some(ident.sym.to_string()), + PropName::Str(s) => Some(s.value.to_string_lossy().to_string()), + _ => None, + }; + + if let Some(method_name) = method_name { if method.is_static { return !static_methods_to_strip.contains(&method_name); } else { @@ -6656,6 +6670,9 @@ impl VisitMut for StepTransform { // Generate full qualified name using # for instance methods: ClassName#methodName let full_name = format!("{}#{}", class_name, method_name); + // For nested step hoisting, use $ instead of # to produce valid JS identifiers + let hoisted_parent_name = format!("{}${}", class_name, method_name); + self.step_function_names.insert(full_name.clone()); // Track class for serialization (needed for `this` serialization) @@ -6678,8 +6695,17 @@ impl VisitMut for StepTransform { method.function.span, )); + // Set current_parent_function_name for nested step hoisting + // This prevents self-referential aliases like `const helper = helper;` + // Use $ instead of # to produce valid JS identifiers + let old_parent = self.current_parent_function_name.clone(); + self.current_parent_function_name = Some(hoisted_parent_name.clone()); + // Visit children to process nested step functions method.visit_mut_children_with(self); + + // Restore parent function name + self.current_parent_function_name = old_parent; } TransformMode::Workflow => { // Remove directive for consistency with other modes @@ -6698,8 +6724,16 @@ impl VisitMut for StepTransform { // Just remove directive, keep the function body self.remove_use_step_directive(&mut method.function.body); + // Set current_parent_function_name for nested step hoisting + // Use $ instead of # to produce valid JS identifiers + let old_parent = self.current_parent_function_name.clone(); + self.current_parent_function_name = Some(hoisted_parent_name.clone()); + // Visit children to process nested step functions method.visit_mut_children_with(self); + + // Restore parent function name + self.current_parent_function_name = old_parent; } } } else { diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js index 5197214566..d60b75811b 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js @@ -17,5 +17,5 @@ export class TestClass { } } registerStepFunction("step//input.js//TestClass.staticMethod", TestClass.staticMethod); -registerStepFunction("step//input.js//TestClass#instanceMethod", TestClass.prototype.instanceMethod); +registerStepFunction("step//input.js//TestClass#instanceMethod", TestClass.prototype["instanceMethod"]); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js index d9d219540d..4cc2445caa 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js @@ -8,5 +8,5 @@ export class TestClass { } } TestClass.staticMethod = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass.staticMethod"); -TestClass.prototype.instanceMethod = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#instanceMethod"); +TestClass.prototype["instanceMethod"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#instanceMethod"); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js index 522c60f518..cf03d037b7 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js @@ -2,7 +2,7 @@ import { registerStepFunction } from "workflow/internal/private"; import { registerSerializationClass } from "workflow/internal/class-serialization"; import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; /**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}},"classes":{"input.js":{"Service":{"classId":"class//input.js//Service"}}}}*/; -var helper = async (x)=>x * 2; +var Service$process$helper = async (x)=>x * 2; export class Service { static [WORKFLOW_SERIALIZE](instance) { return { @@ -18,11 +18,11 @@ export class Service { // Instance method step that contains a nested step function async process(input) { // This nested step should be transformed - const helper = helper; + const helper = Service$process$helper; const doubled = await helper(input); return doubled + this.value; } } -registerStepFunction("step//input.js//helper", helper); -registerStepFunction("step//input.js//Service#process", Service.prototype.process); +registerStepFunction("step//input.js//Service$process/helper", Service$process$helper); +registerStepFunction("step//input.js//Service#process", Service.prototype["process"]); registerSerializationClass("class//input.js//Service", Service); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js index 2614c8e2cd..dd8ef1973f 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js @@ -14,5 +14,5 @@ export class Service { this.value = value; } } -Service.prototype.process = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Service#process"); +Service.prototype["process"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Service#process"); registerSerializationClass("class//input.js//Service", Service); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js index 9d4b2be349..242354e374 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js @@ -21,6 +21,6 @@ export class Calculator { return a + b + this.multiplier; } } -registerStepFunction("step//input.js//Calculator#multiply", Calculator.prototype.multiply); -registerStepFunction("step//input.js//Calculator#add", Calculator.prototype.add); +registerStepFunction("step//input.js//Calculator#multiply", Calculator.prototype["multiply"]); +registerStepFunction("step//input.js//Calculator#add", Calculator.prototype["add"]); registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js index f0c9fe78df..bd6d5c91ba 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js @@ -14,6 +14,6 @@ export class Calculator { this.multiplier = multiplier; } } -Calculator.prototype.multiply = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#multiply"); -Calculator.prototype.add = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#add"); +Calculator.prototype["multiply"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#multiply"); +Calculator.prototype["add"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#add"); registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/input.js similarity index 67% rename from packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/input.js rename to packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/input.js index 7a8afca5a0..aea032cd64 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/input.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/input.js @@ -1,19 +1,19 @@ export async function stepWithThis() { 'use step'; - // Error: this is not allowed + // `this` is allowed in step functions return this.value; } export async function stepWithArguments() { 'use step'; - // Error: arguments is not allowed + // `arguments` is allowed in step functions return arguments[0]; } class TestClass extends BaseClass { async stepMethod() { 'use step'; - // Error: super is not allowed + // `super` is allowed in step functions return super.method(); } } diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js new file mode 100644 index 0000000000..9e9ff263f9 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js @@ -0,0 +1,17 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +/**__internal_workflows{"steps":{"input.js":{"TestClass#stepMethod":{"stepId":"step//input.js//TestClass#stepMethod"},"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +export async function stepWithThis() { + // `this` is allowed in step functions + return this.value; +} +export async function stepWithArguments() { + // `arguments` is allowed in step functions + return arguments[0]; +} +class TestClass extends BaseClass { + async stepMethod() { + // `super` is allowed in step functions + return super.method(); + } +} +registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.js similarity index 85% rename from packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js rename to packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.js index 1439dcd333..382c64e772 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.js @@ -2,20 +2,20 @@ import { registerStepFunction } from "workflow/internal/private"; import { registerSerializationClass } from "workflow/internal/class-serialization"; /**__internal_workflows{"steps":{"input.js":{"TestClass#stepMethod":{"stepId":"step//input.js//TestClass#stepMethod"},"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export async function stepWithThis() { - // Error: this is not allowed + // `this` is allowed in step functions return this.value; } export async function stepWithArguments() { - // Error: arguments is not allowed + // `arguments` is allowed in step functions return arguments[0]; } class TestClass extends BaseClass { async stepMethod() { - // Error: super is not allowed + // `super` is allowed in step functions return super.method(); } } registerStepFunction("step//input.js//stepWithThis", stepWithThis); registerStepFunction("step//input.js//stepWithArguments", stepWithArguments); -registerStepFunction("step//input.js//TestClass#stepMethod", TestClass.prototype.stepMethod); +registerStepFunction("step//input.js//TestClass#stepMethod", TestClass.prototype["stepMethod"]); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.js similarity index 85% rename from packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js rename to packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.js index 3d77751b04..53ad0110e6 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.js @@ -4,5 +4,5 @@ export var stepWithThis = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//inp export var stepWithArguments = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWithArguments"); class TestClass extends BaseClass { } -TestClass.prototype.stepMethod = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#stepMethod"); +TestClass.prototype["stepMethod"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#stepMethod"); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index 917717cf4d..0c2f362406 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -1172,7 +1172,7 @@ export class Counter { } /** - * Instance method step: adds the given amount to the counter's value. + * Instance method step: returns the sum of the counter's value and the given amount. * The `this` context (the Counter instance) is serialized and passed * to the step handler, then deserialized before the method is called. */