Add CommonJS require() support for class serialization detection in SWC plugin#1144
Conversation
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
🦋 Changeset detectedLatest commit: 3648e46 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests▲ Vercel Production (1 failed)example (1 failed):
🌍 Community Worlds (45 failed)turso (45 failed):
Details by Category❌ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Pull request overview
This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.
Changes:
- Track CommonJS
require()bindings during identifier collection (namespaceconst x = require(...)and destructuredconst { ... } = require(...)) and recognizex.WORKFLOW_SERIALIZE/x.WORKFLOW_DESERIALIZEas serialization symbols. - Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
- Update the plugin spec and add a changeset entry documenting the new behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/swc-plugin-workflow/transform/src/lib.rs | Adds require-binding tracking and member-expression symbol recognition for serialization detection. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.js | New fixture input for namespace require() pattern. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.js | Expected workflow-mode output for namespace require() fixture. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.js | Expected step-mode output for namespace require() fixture. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.js | Expected client-mode output for namespace require() fixture. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.js | New fixture input for destructured require() pattern. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.js | Expected workflow-mode output for destructured require() fixture. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.js | Expected step-mode output for destructured require() fixture. |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.js | Expected client-mode output for destructured require() fixture. |
| packages/swc-plugin-workflow/spec.md | Documents supported CommonJS require() symbol patterns. |
| .changeset/social-forks-tease.md | Declares a patch release for the new detection behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Check if an expression is a `require(...)` call. | ||
| /// Returns true for any `require('...')` or `require("...")` call expression. | ||
| fn is_require_call(&self, expr: &Expr) -> bool { | ||
| if let Expr::Call(call) = expr { | ||
| if let Callee::Expr(callee) = &call.callee { | ||
| if let Expr::Ident(ident) = &**callee { | ||
| return ident.sym.as_str() == "require" && !call.args.is_empty(); | ||
| } |
There was a problem hiding this comment.
The is_require_call docstring says it returns true for require('...') / require("..."), but the implementation returns true for any require(...) call as long as it has at least one argument (including non-string / dynamic requires). Either tighten the implementation to check call.args.len() == 1 and the first arg is a string literal, or update the docstring to match the broader behavior (and consider whether dynamic requires should be tracked here).
| Decl::Var(var_decl) => { | ||
| for declarator in &var_decl.decls { | ||
| self.collect_idents_from_pat(&declarator.name); | ||
| // Track const declarations that assign Symbol.for('workflow-serialize') or Symbol.for('workflow-deserialize') | ||
| if let Pat::Ident(ident) = &declarator.name { | ||
| if let Some(init) = &declarator.init { | ||
| if let Some(symbol_name) = self.extract_symbol_for_name(init) { | ||
| if let Some(init) = &declarator.init { | ||
| // Track const declarations that assign Symbol.for('workflow-serialize') or Symbol.for('workflow-deserialize') | ||
| if let Pat::Ident(ident) = &declarator.name { | ||
| if let Some(symbol_name) = | ||
| self.extract_symbol_for_name(init) | ||
| { | ||
| if symbol_name == "workflow-serialize" | ||
| || symbol_name == "workflow-deserialize" | ||
| { | ||
| self.serialization_symbol_identifiers | ||
| .insert(ident.id.sym.to_string(), symbol_name); | ||
| } | ||
| } | ||
| // Track CommonJS namespace require: const serde_1 = require("...") | ||
| if self.is_require_call(init) { | ||
| self.require_namespace_identifiers | ||
| .insert(ident.id.sym.to_string()); | ||
| } | ||
| } | ||
| // Track CommonJS destructured require: | ||
| // const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("...") | ||
| if let Pat::Object(obj_pat) = &declarator.name { | ||
| if self.is_require_call(init) { | ||
| for prop in &obj_pat.props { | ||
| match prop { |
There was a problem hiding this comment.
The CommonJS require tracking logic (namespace + destructured patterns) is duplicated for Decl::Var and for ModuleDecl::ExportDecl(Decl::Var) in collect_declared_identifiers. Consider extracting a small helper that inspects a VarDeclarator (pattern + init) and updates serialization_symbol_identifiers / require_namespace_identifiers once, to reduce the chance these two branches drift over time.

The SWC compiler plugin now detects classes with custom serialization methods (
WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJSrequire()calls, in addition to the existing ESM import andSymbol.for()patterns.This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
into either:
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().