Skip to content

Commit 81c5a83

Browse files
authored
Add "classes" object to manifest.json file (#864)
1 parent f2ab6ee commit 81c5a83

21 files changed

Lines changed: 101 additions & 14 deletions

File tree

‎.changeset/major-mails-act.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@workflow/swc-plugin": patch
3+
"@workflow/builders": patch
4+
---
5+
6+
Add "classes" object to `manifest.json` file

‎packages/builders/src/apply-swc-transform.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export type WorkflowManifest = {
3737
};
3838
};
3939
};
40+
classes?: {
41+
[relativeFileName: string]: {
42+
[className: string]: {
43+
classId: string;
44+
};
45+
};
46+
};
4047
};
4148

4249
export async function applySwcTransform(

‎packages/builders/src/base-builder.ts‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ export abstract class BaseBuilder {
414414
fileManifest.workflows
415415
);
416416
}
417+
if (fileManifest.classes) {
418+
workflowManifest.classes = Object.assign(
419+
workflowManifest.classes || {},
420+
fileManifest.classes
421+
);
422+
}
417423
} catch (error) {
418424
// Log warning but continue - don't fail build for workflow-only file issues
419425
console.log(
@@ -904,7 +910,7 @@ export const OPTIONS = handler;`;
904910
}
905911

906912
/**
907-
* Creates a manifest JSON file containing step/workflow metadata
913+
* Creates a manifest JSON file containing step/workflow/class metadata
908914
* and graph data for visualization.
909915
*/
910916
protected async createManifest({
@@ -927,8 +933,9 @@ export const OPTIONS = handler;`;
927933
manifest.workflows,
928934
workflowGraphs
929935
);
936+
const classes = this.convertClassesManifest(manifest.classes);
930937

931-
const output = { version: '1.0.0', steps, workflows };
938+
const output = { version: '1.0.0', steps, workflows, classes };
932939

933940
await mkdir(manifestDir, { recursive: true });
934941
await writeFile(
@@ -944,9 +951,13 @@ export const OPTIONS = handler;`;
944951
(acc, w) => acc + Object.keys(w).length,
945952
0
946953
);
954+
const classCount = Object.values(classes).reduce(
955+
(acc, c) => acc + Object.keys(c).length,
956+
0
957+
);
947958

948959
console.log(
949-
`Created manifest with ${stepCount} step(s) and ${workflowCount} workflow(s)`,
960+
`Created manifest with ${stepCount} step(s), ${workflowCount} workflow(s), and ${classCount} class(es)`,
950961
`${Date.now() - buildStart}ms`
951962
);
952963
} catch (error) {
@@ -1005,4 +1016,19 @@ export const OPTIONS = handler;`;
10051016
}
10061017
return result;
10071018
}
1019+
1020+
private convertClassesManifest(
1021+
classes: WorkflowManifest['classes']
1022+
): Record<string, Record<string, { classId: string }>> {
1023+
const result: Record<string, Record<string, { classId: string }>> = {};
1024+
if (!classes) return result;
1025+
1026+
for (const [filePath, entries] of Object.entries(classes)) {
1027+
result[filePath] = {};
1028+
for (const [name, data] of Object.entries(entries)) {
1029+
result[filePath][name] = { classId: data.classId };
1030+
}
1031+
}
1032+
return result;
1033+
}
10081034
}

‎packages/builders/src/swc-esbuild-plugin.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ export function createSwcPlugin(options: SwcPluginOptions): Plugin {
218218
options.workflowManifest.steps || {},
219219
workflowManifest.steps
220220
);
221+
options.workflowManifest.classes = Object.assign(
222+
options.workflowManifest.classes || {},
223+
workflowManifest.classes
224+
);
221225

222226
return {
223227
contents: transformedCode,

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ Directives must:
1717

1818
## JSON Manifest
1919

20-
All modes emit a JSON manifest comment at the top of the file containing metadata about discovered workflows and steps:
20+
All modes emit a JSON manifest comment at the top of the file containing metadata about discovered workflows, steps, and classes with custom serialization:
2121

2222
```javascript
23-
/**__internal_workflows{"workflows":{"path/file.ts":{"myWorkflow":{"workflowId":"workflow//path/file.ts//myWorkflow"}}},"steps":{"path/file.ts":{"myStep":{"stepId":"step//path/file.ts//myStep"}}}}*/
23+
/**__internal_workflows{"workflows":{"path/file.ts":{"myWorkflow":{"workflowId":"workflow//path/file.ts//myWorkflow"}}},"steps":{"path/file.ts":{"myStep":{"stepId":"step//path/file.ts//myStep"}}},"classes":{"path/file.ts":{"Point":{"classId":"class//path/file.ts//Point"}}}}*/
2424
```
2525

26-
This manifest is used by bundlers and the runtime to discover and register workflows and steps.
26+
The manifest includes:
27+
- **`workflows`**: Map of workflow function names to their `workflowId`
28+
- **`steps`**: Map of step function names to their `stepId`
29+
- **`classes`**: Map of class names with custom serialization to their `classId`
30+
31+
This manifest is used by bundlers and the runtime to discover and register workflows, steps, and serializable classes.
2732

2833
## ID Generation
2934

@@ -340,6 +345,7 @@ export class Point {
340345
Output (Client Mode):
341346
```javascript
342347
import { registerSerializationClass } from "workflow/internal/class-serialization";
348+
/**__internal_workflows{"classes":{"input.js":{"Point":{"classId":"class//input.js//Point"}}}}*/;
343349
export class Point {
344350
constructor(x, y) {
345351
this.x = x;
@@ -377,7 +383,7 @@ Output (Step Mode):
377383
```javascript
378384
import { registerStepFunction } from "workflow/internal/private";
379385
import { registerSerializationClass } from "workflow/internal/class-serialization";
380-
/**__internal_workflows{"steps":{"input.js":{"MyService.process":{"stepId":"step//input.js//MyService.process"}}}}*/;
386+
/**__internal_workflows{"steps":{"input.js":{"MyService.process":{"stepId":"step//input.js//MyService.process"}}},"classes":{"input.js":{"MyService":{"classId":"class//input.js//MyService"}}}}*/;
381387
export class MyService {
382388
static async process(data) {
383389
return data.value * 2;
@@ -390,7 +396,7 @@ registerSerializationClass("class//input.js//MyService", MyService);
390396
Output (Workflow Mode):
391397
```javascript
392398
import { registerSerializationClass } from "workflow/internal/class-serialization";
393-
/**__internal_workflows{"steps":{"input.js":{"MyService.process":{"stepId":"step//input.js//MyService.process"}}}}*/;
399+
/**__internal_workflows{"steps":{"input.js":{"MyService.process":{"stepId":"step//input.js//MyService.process"}}},"classes":{"input.js":{"MyService":{"classId":"class//input.js//MyService"}}}}*/;
394400
export class MyService {
395401
}
396402
MyService.process = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//MyService.process");
@@ -448,6 +454,7 @@ export class Point {
448454
Output:
449455
```javascript
450456
import { registerSerializationClass } from "workflow/internal/class-serialization";
457+
/**__internal_workflows{"classes":{"input.js":{"Point":{"classId":"class//input.js//Point"}}}}*/;
451458
export class Point {
452459
constructor(x, y) {
453460
this.x = x;

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ pub struct StepTransform {
230230
// Track identifiers that are known to be WORKFLOW_SERIALIZE symbols
231231
// (local name -> "workflow-serialize" or "workflow-deserialize")
232232
serialization_symbol_identifiers: HashMap<String, String>,
233+
// Track class names for the manifest (preserved copy before drain)
234+
classes_for_manifest: HashSet<String>,
233235
}
234236

235237
// Structure to track variable names and their access patterns
@@ -1094,6 +1096,7 @@ impl StepTransform {
10941096
static_step_methods_to_strip: Vec::new(),
10951097
classes_needing_serialization: HashSet::new(),
10961098
serialization_symbol_identifiers: HashMap::new(),
1099+
classes_for_manifest: HashSet::new(),
10971100
}
10981101
}
10991102

@@ -3006,6 +3009,22 @@ impl StepTransform {
30063009
metadata.insert("workflows", format!("{{{}}}", workflow_entries.join(",")));
30073010
}
30083011

3012+
// Build classes metadata
3013+
if !self.classes_for_manifest.is_empty() {
3014+
let mut sorted_classes: Vec<_> = self.classes_for_manifest.iter().collect();
3015+
sorted_classes.sort();
3016+
3017+
let class_entries: Vec<String> = sorted_classes
3018+
.into_iter()
3019+
.map(|class_name| {
3020+
let class_id = naming::format_name("class", &self.filename, class_name);
3021+
format!("\"{}\":{{\"classId\":\"{}\"}}", class_name, class_id)
3022+
})
3023+
.collect();
3024+
3025+
metadata.insert("classes", format!("{{{}}}", class_entries.join(",")));
3026+
}
3027+
30093028
// Build the final comment structure
30103029
let relative_filename = self.filename.replace('\\', "/"); // Normalize path separators
30113030
let mut parts = Vec::new();
@@ -3022,6 +3041,12 @@ impl StepTransform {
30223041
relative_filename, metadata["steps"]
30233042
));
30243043
}
3044+
if metadata.contains_key("classes") {
3045+
parts.push(format!(
3046+
"\"classes\":{{\"{}\":{}}}",
3047+
relative_filename, metadata["classes"]
3048+
));
3049+
}
30253050

30263051
if parts.is_empty() {
30273052
String::new()
@@ -3234,6 +3259,9 @@ impl VisitMut for StepTransform {
32343259
// First pass: collect step functions
32353260
program.visit_mut_children_with(self);
32363261

3262+
// Preserve class names for manifest before they get drained during registration
3263+
self.classes_for_manifest = self.classes_needing_serialization.clone();
3264+
32373265
// Add necessary imports and registrations
32383266
match program {
32393267
Program::Module(module) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { registerSerializationClass } from "workflow/internal/class-serialization";
2-
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}}}*/;
2+
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/;
33
export class TestClass {
44
// Error: instance methods can't have directives
55
async instanceMethod() {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { registerStepFunction } from "workflow/internal/private";
22
import { registerSerializationClass } from "workflow/internal/class-serialization";
3-
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}}}*/;
3+
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/;
44
export class TestClass {
55
// Error: instance methods can't have directives
66
async instanceMethod() {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { registerSerializationClass } from "workflow/internal/class-serialization";
2-
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}}}*/;
2+
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/;
33
export class TestClass {
44
// Error: instance methods can't have directives
55
async instanceMethod() {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { registerSerializationClass } from "workflow/internal/class-serialization";
22
// Test custom serialization with imported symbols from '@workflow/serde'
33
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
4+
/**__internal_workflows{"classes":{"input.js":{"Color":{"classId":"class//input.js//Color"},"Vector":{"classId":"class//input.js//Vector"}}}}*/;
45
// Class using imported symbols
56
export class Vector {
67
constructor(x, y, z){

0 commit comments

Comments
 (0)