You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ae5ee5b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(swc-plugin): register class expressions via an IIFE instead of by name (vercel#3971)
* fix(swc-plugin): register class expressions via an IIFE and reject unnameable classes
Class expressions with "use step" methods or custom serialization were
registered by module-level statements referencing the class by name. When no
module-scope binding could be resolved the plugin fell back to a placeholder
`AnonymousClass` identifier, which is a guaranteed ReferenceError at module
evaluation (vercel#3929). Other shapes were silently wrong as well:
`var A = class {}, B = class {}` registered A's steps under B, `X = class {}`
assignments and classes nested inside functions emitted unresolvable
references.
Class expressions are now wrapped in a single IIFE that receives the class,
performs every registration recorded for it, and returns it, so the
registration no longer depends on a name being in scope. The class name is
still needed for step/class IDs and is derived from the assigned variable,
the class's own identifier, or the property key it is assigned to
(`exports.Foo = class {}`, `{ Foo: class {} }`). When none is available, or
the class is declared inside a function, the plugin emits a compile error
instead of broken code.
Class declarations keep their existing module-level output; the emitters
were factored so both paths share the same statement builders.
* fix(swc-plugin): generate names for anonymous class expressions instead of erroring
With registration happening inside the IIFE, an anonymous class expression
in a position that provides no name (`foo(class { ... })`, an array element,
a conditional branch) only needs a name for its step/class IDs. Generate a
deterministic `AnonymousClass<N>`, counting only anonymous classes that have
something to register, instead of rejecting them. Classes declared inside a
function remain an error.
Dead-code elimination now keeps module-level declarations whose initializer
contains a wrapped class expression: evaluating the initializer is what
registers the class, and the binding may be otherwise unreferenced.
Register class expressions through an IIFE that closes over the class instead of module-level code that references it by name, fixing the unresolvable `AnonymousClass` reference emitted for shapes such as `var Foo = class { ... }` in pre-bundled packages.
When a class expression is assigned to a variable, the plugin uses the variable name (binding name) for registration, not the internal class name. This is important because the internal class name is only accessible inside the class body.
719
+
Class *declarations* (`class Foo { ... }`, `export class Foo { ... }`) are registered by module-level statements appended to the module body that reference the class by name (see the examples above).
Class *expressions* are handled differently, because there is no guarantee that the class is reachable through a module-scope binding: bundlers routinely emit `var Foo = class { ... }` or `var Foo = class _Foo { ... }` (where `_Foo` is only in scope inside the class body), and a class expression can appear anywhere an expression can (`exports.Foo = class {}`, `{ Foo: class {} }`, `foo(class Named {})`, `var A = class {}, B = class {}`). Instead of emitting module-level code that refers to the class by name, the plugin wraps the class expression in an IIFE that receives the class as its argument, performs the registrations, and returns the class:
733
722
734
-
static [WORKFLOW_DESERIALIZE](data) {
735
-
returnnewBash(data.command);
736
-
}
723
+
Input (e.g., after tsdown/esbuild pre-bundling; this is the shape `@vercel/sandbox` ships):
For example, a class expression with step methods:
765
+
Note that:
766
+
- The IIFE closes over the class value itself (`__wf_cls`), so the registration does not depend on any name being in scope at module level. The same output shape is produced for every position a class expression can appear in.
767
+
- Everything recorded for the class (step methods, getters, custom serialization, static workflow methods) is emitted inside the single IIFE, in the same order as the module-level emission for class declarations. The registry lookups are hoisted once per registry rather than repeated per registration.
768
+
- A class expression with nothing to register is left untouched.
769
+
- Registration runs when the class expression is evaluated, which for a module-level class expression is module load, the same as for class declarations.
770
+
-`export default class { ... }` is a `ClassExpr` in the AST but not an expression position; it is handled by the rewrite described below rather than by the IIFE.
The IIFE removes the need to *reference* the class by name, but step and class IDs still need a name (`step//<module>//<ClassName>#<method>`). The name is resolved, in order of preference, from:
786
775
787
-
Output (step mode):
788
-
```javascript
789
-
(function(__wf_fn, __wf_id) {
790
-
var __wf_sym =Symbol.for("@workflow/core//registeredSteps"), __wf_reg =globalThis[__wf_sym] || (globalThis[__wf_sym] =newMap());
1. The variable the expression is assigned to: `var Foo = class _Foo {}` uses `Foo`, not `_Foo`. This also covers `let Foo; Foo = class {}`, parenthesized initializers (`var Foo = (class {})`), and chained assignments (`var Foo = exports.Foo = class {}`). With multiple declarators (`var A = class {}, B = class {}`) each class resolves to its own binding.
777
+
2. The class expression's own identifier: `foo(class Plugin {})` uses `Plugin`.
778
+
3. The property the expression is assigned to or defined under: `exports.Foo = class {}` and `{ Foo: class {} }` use `Foo` (string keys such as `'kebab-job'` are accepted as-is).
779
+
4. A generated `AnonymousClass<N>` when none of the above applies (`foo(class { ... })`, an array element, a conditional branch). `N` counts, in source order, only the anonymous class expressions that have something to register, so unrelated anonymous classes do not shift the numbering; if the module already declares `AnonymousClass<N>`, the name is suffixed (`AnonymousClass6$1`). Like the `_anonymousStep<N>` names used for anonymous step functions, these are positional: adding another such class earlier in the module renumbers the ones after it, and with them their step IDs. Name the class if its IDs need to be stable.
801
780
802
-
All references use `LanguageModel` (the binding name), not `_LanguageModel` (the internal class expression name). Only a single class registration IIFE is emitted. The step IDs also use the binding name.
781
+
Names from (1) and (2) are bindings that already refer to the class, so when the class expression is anonymous the binding name is inserted as the class's own identifier (`var Foo = class {}` becomes `(...)(class Foo {})`). Passing the class as a call argument would otherwise defeat the `.name` inference the original assignment provided. For typical usage this is behaviorally equivalent to `var Foo = class Foo {}`; an inner class-scoped `Foo` binding is introduced, which can differ in edge cases that assign to or shadow that name inside the class body. Names from (3) are *not* inserted as an identifier, since `exports.Foo = class { m() { return Foo; } }` may refer to an unrelated outer `Foo`; the IIFE instead sets `.name` at runtime with `Object.defineProperty(__wf_cls, "name", { value: "Foo", configurable: true })`. Generated names (4) leave `.name` untouched, since the original position inferred no name either.
803
782
804
-
### Anonymous class expression name re-insertion
783
+
Classes that already have an identifier (e.g. `class _Bash { ... }`) are never renamed.
805
784
806
-
When a serializable class expression has no internal name (anonymous) but has a binding name from a variable declaration, the plugin re-inserts the binding name as the class expression's identifier. This handles the common case where upstream bundlers like esbuild/tsup transform `class Foo { ... }` into `var Foo = class { ... }` (stripping the class name).
785
+
#### Dead-code elimination
807
786
808
-
Without this fix, the anonymous class would have an empty `.name` property, which can break downstream bundlers that rely on the class name for serialization registration.
787
+
Evaluating a wrapped class expression is what registers the class, so dead-code elimination keeps any module-level variable declaration whose initializer contains one, even when the declared binding is otherwise unreferenced (`const registry = new Map([["point", class { ...serde... }]])`).
A class (declaration or expression) that has `"use step"`/`"use workflow"` methods, `"use step"` getters, or custom serialization but is declared *inside a function* is a compile error:
Classes using "use step" methods must be declared at the top level of the module, not inside a function. Registration runs at module load and cannot reach a class declared in an inner scope
846
795
```
847
796
848
-
Note that:
849
-
- The class expression `class { ... }` becomes `class Shell { ... }` with the binding name inserted
850
-
- For typical usage, behavior is preserved while ensuring the `.name` property survives subsequent bundling (an inner class name binding is introduced, which can differ in edge cases that depend on assigning to or shadowing that name inside the class body)
851
-
- Classes that already have an internal name (e.g., `class _Bash { ... }`) are not modified
852
-
- Only classes with serialization methods (`WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`) are affected
797
+
Step registration must happen at module load for the step to be resolvable by ID; a class inside a function would only be registered when (and each time) that function runs. Earlier versions of the plugin emitted module-level code referencing the inner class's name (or a placeholder `AnonymousClass`), which threw a `ReferenceError` as soon as the module was evaluated. At most one error is reported per class, at the first offending member; nested classes without steps or serialization are unaffected. No errors are emitted in `detect` mode, which generates no code.
853
798
854
799
### Anonymous default class export rewriting
855
800
856
-
When an anonymous class with serialization methods or step methods is exported as the default export, the plugin rewrites it into a `const` declaration + re-export so that the class has a binding name accessible at module scope. Without this, the generated registration code would reference an undefined variable.
801
+
When an anonymous class with serialization methods or step methods is exported as the default export, the plugin rewrites it into a `const` declaration + re-export so that the class has a binding name accessible at module scope. `export default class { ... }` is not an expression position, so the registration IIFE used for class expressions does not apply; the class is instead registered by module-level statements that reference the generated `const`.
857
802
858
803
Input:
859
804
```javascript
@@ -958,6 +903,7 @@ The plugin emits errors for invalid usage:
958
903
| Invalid exports (`"use workflow"`) | Module-level `"use workflow"` files can only export async functions |
959
904
| Invalid exports (`"use step"`) | Module-level `"use step"` files can only export functions (sync or async) |
960
905
| Misspelled directive | Detects typos like `"use steps"` or `"use workflows"`|
906
+
| Nested class | A class with step/workflow methods, step getters, or custom serialization declared inside a function rather than at the module's top level |
0 commit comments