Skip to content

Commit b4113da

Browse files
authored
Enable custom class serialization transformations for "client" mode (#860)
1 parent b59559b commit b4113da

8 files changed

Lines changed: 140 additions & 40 deletions

File tree

‎.changeset/modern-clubs-own.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/swc-plugin": patch
3+
---
4+
5+
Enable custom class serialization transformations for "client" mode

‎packages/swc-plugin-workflow/spec.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,46 @@ export async function myWorkflow(data) {
315315
myWorkflow.workflowId = "workflow//input.js//myWorkflow";
316316
```
317317

318+
### Custom Serialization in Client Mode
319+
320+
Classes with custom serialization methods are also registered in client mode so that they can be properly serialized when passed to `start(workflow)`:
321+
322+
Input:
323+
```javascript
324+
export class Point {
325+
constructor(x, y) {
326+
this.x = x;
327+
this.y = y;
328+
}
329+
330+
static [Symbol.for("workflow-serialize")](instance) {
331+
return { x: instance.x, y: instance.y };
332+
}
333+
334+
static [Symbol.for("workflow-deserialize")](data) {
335+
return new Point(data.x, data.y);
336+
}
337+
}
338+
```
339+
340+
Output (Client Mode):
341+
```javascript
342+
import { registerSerializationClass } from "workflow/internal/class-serialization";
343+
export class Point {
344+
constructor(x, y) {
345+
this.x = x;
346+
this.y = y;
347+
}
348+
static [Symbol.for("workflow-serialize")](instance) {
349+
return { x: instance.x, y: instance.y };
350+
}
351+
static [Symbol.for("workflow-deserialize")](data) {
352+
return new Point(data.x, data.y);
353+
}
354+
}
355+
registerSerializationClass("class//input.js//Point", Point);
356+
```
357+
318358
---
319359

320360
## Static Methods

‎packages/swc-plugin-workflow/transform/src/lib.rs‎

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ mod naming;
33
use serde::Deserialize;
44
use std::collections::{HashMap, HashSet};
55
use swc_core::{
6-
common::{DUMMY_SP, SyntaxContext, errors::HANDLER},
6+
common::{errors::HANDLER, SyntaxContext, DUMMY_SP},
77
ecma::{
88
ast::*,
9-
visit::{VisitMut, VisitMutWith, noop_visit_mut_type},
9+
visit::{noop_visit_mut_type, VisitMut, VisitMutWith},
1010
},
1111
};
1212

@@ -2230,6 +2230,45 @@ impl StepTransform {
22302230
}))
22312231
}
22322232

2233+
// Create a registration call statement: registerSerializationClass("class//...", ClassName)
2234+
// Used in workflow mode and client mode to register classes for serialization
2235+
fn create_class_serialization_registration(&self, class_name: &str) -> Stmt {
2236+
let class_id = naming::format_name("class", &self.filename, class_name);
2237+
Stmt::Expr(ExprStmt {
2238+
span: DUMMY_SP,
2239+
expr: Box::new(Expr::Call(CallExpr {
2240+
span: DUMMY_SP,
2241+
ctxt: SyntaxContext::empty(),
2242+
callee: Callee::Expr(Box::new(Expr::Ident(Ident::new(
2243+
"registerSerializationClass".into(),
2244+
DUMMY_SP,
2245+
SyntaxContext::empty(),
2246+
)))),
2247+
args: vec![
2248+
// First argument: class ID
2249+
ExprOrSpread {
2250+
spread: None,
2251+
expr: Box::new(Expr::Lit(Lit::Str(Str {
2252+
span: DUMMY_SP,
2253+
value: class_id.into(),
2254+
raw: None,
2255+
}))),
2256+
},
2257+
// Second argument: ClassName
2258+
ExprOrSpread {
2259+
spread: None,
2260+
expr: Box::new(Expr::Ident(Ident::new(
2261+
class_name.into(),
2262+
DUMMY_SP,
2263+
SyntaxContext::empty(),
2264+
))),
2265+
},
2266+
],
2267+
type_args: None,
2268+
})),
2269+
})
2270+
}
2271+
22332272
// Create a proxy reference: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id", closure_fn) (workflow mode)
22342273
fn create_step_proxy_reference(&self, step_id: &str, closure_vars: &[String]) -> Expr {
22352274
let mut args = vec![ExprOrSpread {
@@ -3239,7 +3278,13 @@ impl VisitMut for StepTransform {
32393278
}
32403279
}
32413280
TransformMode::Client => {
3242-
// No imports needed for client mode since step functions are not transformed
3281+
// In client mode, we still need class serialization registration
3282+
// so that classes can be serialized when passed to start(workflow)
3283+
let needs_class_serialization =
3284+
!self.classes_needing_serialization.is_empty();
3285+
if needs_class_serialization {
3286+
imports_to_add.push(self.create_class_serialization_import());
3287+
}
32433288
}
32443289
}
32453290

@@ -3710,43 +3755,22 @@ impl VisitMut for StepTransform {
37103755
self.classes_needing_serialization.drain().collect();
37113756
sorted_classes.sort();
37123757
for class_name in sorted_classes {
3713-
// Generate class ID: class//filename//ClassName
3714-
let class_id = naming::format_name("class", &self.filename, &class_name);
3758+
let registration_call =
3759+
self.create_class_serialization_registration(&class_name);
3760+
module.body.push(ModuleItem::Stmt(registration_call));
3761+
}
3762+
}
37153763

3716-
// Create: registerSerializationClass("class//...", ClassName)
3717-
let registration_call = Stmt::Expr(ExprStmt {
3718-
span: DUMMY_SP,
3719-
expr: Box::new(Expr::Call(CallExpr {
3720-
span: DUMMY_SP,
3721-
ctxt: SyntaxContext::empty(),
3722-
callee: Callee::Expr(Box::new(Expr::Ident(Ident::new(
3723-
"registerSerializationClass".into(),
3724-
DUMMY_SP,
3725-
SyntaxContext::empty(),
3726-
)))),
3727-
args: vec![
3728-
// First argument: class ID
3729-
ExprOrSpread {
3730-
spread: None,
3731-
expr: Box::new(Expr::Lit(Lit::Str(Str {
3732-
span: DUMMY_SP,
3733-
value: class_id.into(),
3734-
raw: None,
3735-
}))),
3736-
},
3737-
// Second argument: ClassName
3738-
ExprOrSpread {
3739-
spread: None,
3740-
expr: Box::new(Expr::Ident(Ident::new(
3741-
class_name.into(),
3742-
DUMMY_SP,
3743-
SyntaxContext::empty(),
3744-
))),
3745-
},
3746-
],
3747-
type_args: None,
3748-
})),
3749-
});
3764+
// Add class serialization registrations for client mode
3765+
// In client mode, we need classes to be registered so that serialization works
3766+
// when passing class instances to start(workflow)
3767+
if matches!(self.mode, TransformMode::Client) {
3768+
let mut sorted_classes: Vec<_> =
3769+
self.classes_needing_serialization.drain().collect();
3770+
sorted_classes.sort();
3771+
for class_name in sorted_classes {
3772+
let registration_call =
3773+
self.create_class_serialization_registration(&class_name);
37503774
module.body.push(ModuleItem::Stmt(registration_call));
37513775
}
37523776
}
@@ -3935,7 +3959,13 @@ impl VisitMut for StepTransform {
39353959
}
39363960
}
39373961
TransformMode::Client => {
3938-
// No imports needed for workflow mode
3962+
// In client mode, we still need class serialization registration
3963+
// so that classes can be serialized when passed to start(workflow)
3964+
let needs_class_serialization =
3965+
!self.classes_needing_serialization.is_empty();
3966+
if needs_class_serialization {
3967+
module_items.push(self.create_class_serialization_import());
3968+
}
39393969
}
39403970
}
39413971

@@ -3951,6 +3981,18 @@ impl VisitMut for StepTransform {
39513981
}
39523982
}
39533983

3984+
// Add class serialization registrations for client mode (Script case)
3985+
if matches!(self.mode, TransformMode::Client) {
3986+
let mut sorted_classes: Vec<_> =
3987+
self.classes_needing_serialization.drain().collect();
3988+
sorted_classes.sort();
3989+
for class_name in sorted_classes {
3990+
let registration_call =
3991+
self.create_class_serialization_registration(&class_name);
3992+
module_items.push(ModuleItem::Stmt(registration_call));
3993+
}
3994+
}
3995+
39543996
// Note: workflowId assignments are now handled in visit_mut_module_items
39553997

39563998
// Add metadata comment at the beginning of the module

‎packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { registerSerializationClass } from "workflow/internal/class-serialization";
12
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}}}*/;
23
export class TestClass {
34
// Error: instance methods can't have directives
@@ -15,3 +16,4 @@ export class TestClass {
1516
return 'allowed';
1617
}
1718
}
19+
registerSerializationClass("class//input.js//TestClass", TestClass);

‎packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-imported/output-client.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { registerSerializationClass } from "workflow/internal/class-serialization";
12
// Test custom serialization with imported symbols from '@workflow/serde'
23
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
34
// Class using imported symbols
@@ -37,3 +38,5 @@ export class Color {
3738
return new Color(data.r, data.g, data.b);
3839
}
3940
}
41+
registerSerializationClass("class//input.js//Color", Color);
42+
registerSerializationClass("class//input.js//Vector", Vector);

‎packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-local-const/output-client.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { registerSerializationClass } from "workflow/internal/class-serialization";
12
// Test custom serialization with locally defined symbols using Symbol.for()
23
const WORKFLOW_SERIALIZE = Symbol.for('workflow-serialize');
34
const WORKFLOW_DESERIALIZE = Symbol.for('workflow-deserialize');
@@ -53,3 +54,6 @@ export class Triangle {
5354
return new Triangle(data.a, data.b, data.c);
5455
}
5556
}
57+
registerSerializationClass("class//input.js//Circle", Circle);
58+
registerSerializationClass("class//input.js//Rectangle", Rectangle);
59+
registerSerializationClass("class//input.js//Triangle", Triangle);

‎packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization/output-client.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { registerSerializationClass } from "workflow/internal/class-serialization";
12
// Class with custom serialization methods using symbols
23
export class Point {
34
constructor(x, y){
@@ -28,3 +29,4 @@ export class OnlySerialize {
2829
};
2930
}
3031
}
32+
registerSerializationClass("class//input.js//Point", Point);

‎packages/swc-plugin-workflow/transform/tests/fixture/static-method-step/output-client.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { registerSerializationClass } from "workflow/internal/class-serialization";
12
/**__internal_workflows{"steps":{"input.js":{"MyService.process":{"stepId":"step//input.js//MyService.process"},"MyService.transform":{"stepId":"step//input.js//MyService.transform"}}}}*/;
23
export class MyService {
34
static async process(data) {
@@ -11,3 +12,4 @@ export class MyService {
1112
return 'regular';
1213
}
1314
}
15+
registerSerializationClass("class//input.js//MyService", MyService);

0 commit comments

Comments
 (0)