From 903d24687e92cd37cf469ae4b7735a686f0d8874 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 4 Sep 2026 11:16:34 -0700 Subject: [PATCH 1/2] 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/workflow#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. --- .../class-expression-registration-iife.md | 5 + packages/swc-plugin-workflow/spec.md | 176 +- .../swc-plugin-workflow/transform/src/lib.rs | 3184 +++++++++-------- .../anonymous-class-step-methods/input.js | 69 + .../output-step.js | 66 + .../output-step.stderr | 54 + .../output-workflow.js | 56 + .../output-workflow.stderr | 54 + .../errors/nested-class-step-methods/input.js | 59 + .../nested-class-step-methods/output-step.js | 75 + .../output-step.stderr | 34 + .../output-workflow.js | 64 + .../output-workflow.stderr | 34 + .../output-step.js | 57 +- .../output-workflow.js | 28 +- .../output-step.js | 48 +- .../output-workflow.js | 48 +- .../class-expression-binding-shapes/input.js | 93 + .../output-step.js | 264 ++ .../output-workflow.js | 168 + 20 files changed, 2873 insertions(+), 1763 deletions(-) create mode 100644 .changeset/class-expression-registration-iife.md create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js diff --git a/.changeset/class-expression-registration-iife.md b/.changeset/class-expression-registration-iife.md new file mode 100644 index 0000000000..5cb402229d --- /dev/null +++ b/.changeset/class-expression-registration-iife.md @@ -0,0 +1,5 @@ +--- +'@workflow/swc-plugin': patch +--- + +Register class expressions (`var Foo = class { ... }`, `exports.Foo = class { ... }`, `{ Foo: class { ... } }`, etc.) through an IIFE that closes over the class instead of module-level code that references the class by name, and fail the build with a clear error instead of emitting an unresolvable `AnonymousClass` reference when a class with `"use step"` methods or custom serialization has no derivable name or is declared inside a function. diff --git a/packages/swc-plugin-workflow/spec.md b/packages/swc-plugin-workflow/spec.md index 1449f97ac9..a9d2e77101 100644 --- a/packages/swc-plugin-workflow/spec.md +++ b/packages/swc-plugin-workflow/spec.md @@ -716,146 +716,92 @@ Destructured require also supports renaming (analogous to `import { WORKFLOW_SER const { WORKFLOW_SERIALIZE: WS, WORKFLOW_DESERIALIZE: WD } = require("@workflow/serde"); ``` -### Class expressions with binding names +### Class expressions -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. +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). -Input: -```javascript -import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; - -var Bash = class _Bash { - constructor(command) { - this.command = command; - } +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: - static [WORKFLOW_SERIALIZE](instance) { - return { command: instance.command }; - } - - static [WORKFLOW_DESERIALIZE](data) { - return new Bash(data.command); - } -}; -``` - -Output: +Input (e.g., after tsdown/esbuild pre-bundling; this is the shape `@vercel/sandbox` ships): ```javascript -import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; -/**__internal_workflows{"classes":{"input.js":{"Bash":{"classId":"class//./input//Bash"}}}}*/; -var Bash = class _Bash { - constructor(command) { - this.command = command; - } - static [WORKFLOW_SERIALIZE](instance) { - return { command: instance.command }; - } - static [WORKFLOW_DESERIALIZE](data) { - return new Bash(data.command); - } +var FileSystem = class { + constructor(sandbox) { this.sandbox = sandbox; } + async readFile(path) { "use step"; return this.sandbox.read(path); } }; -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false }); -})(Bash, "class//./input//Bash"); +export { FileSystem }; ``` -Note that: -- The registration uses `Bash` (the variable name), not `_Bash` (the internal class name) -- The `classId` in the manifest also uses `Bash` -- This ensures the registration call references a symbol that's actually in scope at module level - -This binding-name preference applies to **all** generated code that references the class at module scope, including: -- Class serialization registration IIFEs -- Step method registrations (inline IIFE calls) -- Workflow method stub assignments - -For example, a class expression with step methods: - -Input: +Output (step mode): ```javascript -import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; - -var LanguageModel = class _LanguageModel { - constructor(modelId) { this.modelId = modelId; } - static [WORKFLOW_SERIALIZE](inst) { return { modelId: inst.modelId }; } - static [WORKFLOW_DESERIALIZE](data) { return new _LanguageModel(data.modelId); } - async doStream(prompt) { "use step"; return { stream: prompt }; } - static async generate(input) { "use step"; return { result: input }; } -}; +/**__internal_workflows{"steps":{"input.js":{"FileSystem#readFile":{"stepId":"step//./input//FileSystem#readFile"}}},"classes":{"input.js":{"FileSystem":{"classId":"class//./input//FileSystem"}}}}*/; +var FileSystem = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["readFile"]; + __wf_reg.set("step//./input//FileSystem#readFile", __wf_fn); + __wf_fn.stepId = "step//./input//FileSystem#readFile"; + Object.defineProperty(__wf_fn, "name", { value: "readFile", configurable: true }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//FileSystem", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { value: "class//./input//FileSystem", writable: false, enumerable: false, configurable: false }); + return __wf_cls; +}(class FileSystem { + constructor(sandbox) { this.sandbox = sandbox; } + async readFile(path) { return this.sandbox.read(path); } +}); +export { FileSystem }; ``` -Output (step mode): +Output (workflow mode): ```javascript -(function(__wf_fn, __wf_id) { - var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_fn); - __wf_fn.stepId = __wf_id; -})(LanguageModel.generate, "step//./input//LanguageModel.generate"); -(function(__wf_fn, __wf_id) { - var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_fn); - __wf_fn.stepId = __wf_id; -})(LanguageModel.prototype["doStream"], "step//./input//LanguageModel#doStream"); -(function(__wf_cls, __wf_id) { /* ... */ })(LanguageModel, "class//./input//LanguageModel"); +var FileSystem = function(__wf_cls) { + __wf_cls.prototype["readFile"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//FileSystem#readFile"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//FileSystem", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { /* ... */ }); + return __wf_cls; +}(class FileSystem { + constructor(sandbox) { this.sandbox = sandbox; } +}); ``` -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. +Note that: +- 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. +- 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. +- A class expression with nothing to register is left untouched. +- Registration runs when the class expression is evaluated, which for a module-level class expression is module load, the same as for class declarations. +- `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. -### Anonymous class expression name re-insertion +#### Class names for IDs -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). +The IIFE removes the need to *reference* the class by name, but step and class IDs still need a name (`step////#`). The name is resolved, in order of preference, from: -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. +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. +2. The class expression's own identifier: `foo(class Plugin {})` uses `Plugin`. +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). -Input (e.g., after tsup pre-bundling): -```javascript -var Shell = class { - constructor(cmd) { - this.cmd = cmd; - } +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 })`. - static [Symbol.for('workflow-serialize')](instance) { - return { cmd: instance.cmd }; - } +Classes that already have an identifier (e.g. `class _Bash { ... }`) are never renamed. + +#### Unnameable and nested classes are errors + +If a class expression has `"use step"`/`"use workflow"` methods, `"use step"` getters, or custom serialization, and no name can be derived (e.g. `foo(class { ... })`, `[class { ... }]`, `cond ? class { ... } : null`), the plugin emits a compile error: - static [Symbol.for('workflow-deserialize')](data) { - return new Shell(data.cmd); - } -}; +``` +Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can reference it when registering it at module level ``` -Output: -```javascript -/**__internal_workflows{"classes":{"input.js":{"Shell":{"classId":"class//./input//Shell"}}}}*/; -var Shell = class Shell { - constructor(cmd) { - this.cmd = cmd; - } - static [Symbol.for('workflow-serialize')](instance) { - return { cmd: instance.cmd }; - } - static [Symbol.for('workflow-deserialize')](data) { - return new Shell(data.cmd); - } -}; -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false }); -})(Shell, "class//./input//Shell"); +Likewise, a class (declaration or expression) that uses those features but is declared *inside a function* is an error: + +``` +Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in an inner scope ``` -Note that: -- The class expression `class { ... }` becomes `class Shell { ... }` with the binding name inserted -- 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) -- Classes that already have an internal name (e.g., `class _Bash { ... }`) are not modified -- Only classes with serialization methods (`WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`) are affected +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 a placeholder `AnonymousClass` name (or the inner class's name) in these situations, which produced module-level code that threw a `ReferenceError` as soon as the module was evaluated. At most one error is reported per class, at the first offending member; classes without steps or serialization are unaffected. No errors are emitted in `detect` mode, which generates no code. ### Anonymous default class export rewriting -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. +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`. Input: ```javascript @@ -960,6 +906,8 @@ The plugin emits errors for invalid usage: | Invalid exports (`"use workflow"`) | Module-level `"use workflow"` files can only export async functions | | Invalid exports (`"use step"`) | Module-level `"use step"` files can only export functions (sync or async) | | Misspelled directive | Detects typos like `"use steps"` or `"use workflows"` | +| Unnameable class expression | An anonymous class expression with step/workflow methods, step getters, or custom serialization in a position that provides no name (e.g. `foo(class { ... })`) | +| 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 | --- diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index a6f7c2d337..623639c6a6 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -35,6 +35,15 @@ enum WorkflowErrorKind { span: swc_core::common::Span, directive: &'static str, }, + /// A class that needs generated module-level code (step method + /// registration or custom serialization registration) but that code has + /// no way to reference the class. Emitting a placeholder name would only + /// defer the failure to a `ReferenceError` when the module is evaluated. + UnreferenceableClass { + span: swc_core::common::Span, + class: UnreferenceableClass, + feature: &'static str, + }, } #[derive(Debug, Clone)] @@ -43,6 +52,33 @@ enum DirectiveLocation { FunctionBody, } +/// A module-level class expression that has been visited and may need to be +/// wrapped in a registration IIFE (see `wrap_class_expr_with_registrations`). +#[derive(Debug, Clone)] +struct PendingClassExpr { + /// Logical class name used for step/class IDs. + name: String, + /// When the class expression is anonymous and its name came from the + /// variable it is assigned to, the name is inserted as the class's own + /// identifier so that `.name` inference survives the IIFE wrapping. + /// `None` when the class already has an identifier, or when the name was + /// derived from a property key and must not be introduced as a binding. + ident_to_insert: Option, + /// Whether the class defines `WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE`. + has_custom_serialization: bool, +} + +/// Why generated module-level code cannot reference a class. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UnreferenceableClass { + /// An anonymous class expression that is not directly assigned to a + /// variable, e.g. `foo(class { ... })` or `exports.Foo = class { ... }`. + Anonymous, + /// A class declared inside a function body; its binding is not in scope + /// at module level where registrations are emitted. + Nested, +} + /// Sanitize a string for use as part of a JavaScript identifier. /// Replaces characters that are not valid in JS identifiers with `_`. fn sanitize_ident_part(name: &str) -> String { @@ -114,6 +150,23 @@ fn emit_error(error: WorkflowErrorKind) { ) }, ), + WorkflowErrorKind::UnreferenceableClass { + span, + class, + feature, + } => ( + span, + match class { + UnreferenceableClass::Anonymous => format!( + "Anonymous class expressions cannot use {}. Assign the class to a variable (e.g. `const MyClass = class {{ ... }}`) or give it a name (`class MyClass {{ ... }}`) so the compiler can reference it when registering it at module level", + feature + ), + UnreferenceableClass::Nested => format!( + "Classes using {} must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in an inner scope", + feature + ), + }, + ), }; HANDLER.with(|handler| handler.struct_span_err(span, &msg).emit()); @@ -403,6 +456,22 @@ pub struct StepTransform { // e.g., for `var Bash = class _Bash {}`, this would be "Bash" // This is needed because the internal class name (_Bash) is not in scope at module level current_class_binding_name: Option, + // True when `current_class_binding_name` was derived from a property key + // (`exports.Foo = class {}`, `{ Foo: class {} }`) rather than a variable + // binding. Such names are used for IDs only and are never inserted as the + // class's own identifier, since that could shadow an unrelated outer `Foo`. + current_class_binding_from_key: bool, + // Set by `visit_mut_class_expr` for a module-level class expression whose + // name resolved; consumed by `visit_mut_expr`, which owns the enclosing + // `Expr` node and can replace it with the registration IIFE. + pending_class_expr_registration: Option, + // Set while visiting a class whose generated registration code could not + // reference the class from module scope (see `UnreferenceableClass`). + // `current_class_name` is `None` for such classes so that step methods and + // custom serialization are reported as errors instead of emitting code + // that would throw a ReferenceError at runtime. Cleared after the first + // error so a class produces at most one diagnostic. + current_class_unreferenceable: Option, // Track static method steps that need registration after the class declaration // (class_name, method_name, step_id, span) static_method_step_registrations: Vec<(String, String, String, swc_core::common::Span)>, @@ -1563,7 +1632,9 @@ impl Visit for LexicalThisDetector { } } ClassMember::StaticBlock(_) => { /* `this` inside is class itself */ } - ClassMember::Empty(_) | ClassMember::TsIndexSignature(_) | ClassMember::AutoAccessor(_) => {} + ClassMember::Empty(_) + | ClassMember::TsIndexSignature(_) + | ClassMember::AutoAccessor(_) => {} } } } @@ -1845,6 +1916,9 @@ impl StepTransform { module_imports: HashSet::new(), current_class_name: None, current_class_binding_name: None, + current_class_binding_from_key: false, + pending_class_expr_registration: None, + current_class_unreferenceable: None, static_method_step_registrations: Vec::new(), static_method_workflow_registrations: Vec::new(), static_step_methods_to_strip: Vec::new(), @@ -1881,11 +1955,7 @@ impl StepTransform { /// (empty if there is no enclosing workflow). When `parent_workflow_name` /// is non-empty, the returned name is `parent/fn_name`; otherwise it is /// just `fn_name`. The mapping is recorded only when a prefix is added. - fn record_nested_step_name( - &mut self, - fn_name: &str, - parent_workflow_name: &str, - ) -> String { + fn record_nested_step_name(&mut self, fn_name: &str, parent_workflow_name: &str) -> String { if parent_workflow_name.is_empty() { fn_name.to_string() } else { @@ -3213,6 +3283,87 @@ impl StepTransform { has_serialize && has_deserialize } + /// Report that the class currently being visited needs module-level + /// registration code for `feature` but cannot be referenced from module + /// scope. Emits at most one error per class (the first offending member), + /// and never emits in `Detect` mode, which generates no code. + /// + /// Returns `true` if the class is unreferenceable (regardless of whether an + /// error was emitted), so callers can skip code generation. + fn report_unreferenceable_class( + &mut self, + span: swc_core::common::Span, + feature: &'static str, + ) -> bool { + let Some(class) = self.current_class_unreferenceable else { + return false; + }; + if !matches!(self.mode, TransformMode::Detect) { + emit_error(WorkflowErrorKind::UnreferenceableClass { + span, + class, + feature, + }); + } + // Only report once per class. + self.current_class_unreferenceable = None; + true + } + + /// Resolve the name that generated module-level code should use to + /// reference a class expression, or the reason it cannot be referenced. + /// + /// Prefers the binding the expression is assigned to (`Foo` in + /// `var Foo = class _Foo {}`) over the expression's own identifier + /// (`_Foo`), which is only in scope inside the class body. + fn resolve_class_expr_name( + &self, + class_expr: &ClassExpr, + binding_name: Option, + ) -> Result { + if !self.in_module_level { + return Err(UnreferenceableClass::Nested); + } + binding_name + .or_else(|| class_expr.ident.as_ref().map(|i| i.sym.to_string())) + .ok_or(UnreferenceableClass::Anonymous) + } + + /// The static name of a member access (`obj.name` or `obj["name"]`), if any. + fn member_prop_name(prop: &MemberProp) -> Option { + match prop { + MemberProp::Ident(ident) => Some(ident.sym.to_string()), + MemberProp::Computed(computed) => match &*computed.expr { + Expr::Lit(Lit::Str(s)) => Some(s.value.to_string_lossy().to_string()), + _ => None, + }, + MemberProp::PrivateName(_) => None, + } + } + + /// The static name of an object literal / class member key, if any. + fn prop_name_string(key: &PropName) -> Option { + match key { + PropName::Ident(ident) => Some(ident.sym.to_string()), + PropName::Str(s) => Some(s.value.to_string_lossy().to_string()), + _ => None, + } + } + + /// Returns the class expression a variable initializer ultimately + /// evaluates to, looking through parentheses and chained assignments + /// (`var A = (class {})`, `var A = exports.A = class {}`). + fn class_expr_of_initializer(init: &Expr) -> Option<&ClassExpr> { + match init { + Expr::Class(class_expr) => Some(class_expr), + Expr::Paren(paren) => Self::class_expr_of_initializer(&paren.expr), + Expr::Assign(assign) if assign.op == AssignOp::Assign => { + Self::class_expr_of_initializer(&assign.right) + } + _ => None, + } + } + /// Returns `true` if the class has any methods with `"use step"` or `"use workflow"` /// directives, or has custom serialization methods (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE). /// Used to determine whether an anonymous default class export needs a binding name rewrite. @@ -3324,217 +3475,214 @@ impl StepTransform { // __wf_reg.set(__wf_id, __wf_cls); // Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false }); // })(ClassName, "class//module_path//ClassName"); - fn create_class_serialization_registration(&self, class_name: &str) -> Stmt { - let class_id = naming::format_name("class", &self.get_module_path(), class_name); - - // Helper to create an identifier - let ident = - |name: &str| -> Ident { Ident::new(name.into(), DUMMY_SP, SyntaxContext::empty()) }; - - // Helper to create an identifier expression - let ident_expr = |name: &str| -> Box { Box::new(Expr::Ident(ident(name))) }; - - // var __wf_sym = Symbol.for("workflow-class-registry"); - let sym_decl = VarDeclarator { + /// Build the inline IIFE that registers a class for custom serialization. + /// `class_ref` is the identifier used to reference the class; `class_name` + /// is the logical class name used to derive the class ID. + /// Registry key (a `Symbol.for` name) and the variable names used for the + /// registry lookup emitted by `registry_lookup_stmt`. + fn registry_lookup_stmt(sym_var: &str, reg_var: &str, key: &str, extra_vars: &[&str]) -> Stmt { + // var = Symbol.for(""), + // = globalThis[] || (globalThis[] = new Map())[, ]; + let global_sym_access = || { + Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Self::ident_expr("globalThis"), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Self::ident_expr(sym_var), + }), + })) + }; + let declarator = |name: &str, init: Option| VarDeclarator { span: DUMMY_SP, name: Pat::Ident(BindingIdent { - id: ident("__wf_sym"), + id: Self::ident(name), type_ann: None, }), - init: Some(Box::new(Expr::Call(CallExpr { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: ident_expr("Symbol"), - prop: MemberProp::Ident(IdentName { - span: DUMMY_SP, - sym: "for".into(), - }), - }))), - args: vec![ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: "workflow-class-registry".into(), - raw: None, - }))), - }], - type_args: None, - }))), + init: init.map(Box::new), definite: false, }; - // var __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - let global_sym_access = Box::new(Expr::Member(MemberExpr { + let sym_init = Expr::Call(CallExpr { span: DUMMY_SP, - obj: ident_expr("globalThis"), - prop: MemberProp::Computed(ComputedPropName { - span: DUMMY_SP, - expr: ident_expr("__wf_sym"), - }), - })); + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Self::member(Self::ident_expr("Symbol"), "for")), + args: vec![ExprOrSpread { + spread: None, + expr: Self::str_lit(key), + }], + type_args: None, + }); - let reg_decl = VarDeclarator { + let Expr::Member(assign_target) = *global_sym_access() else { + unreachable!() + }; + let reg_init = Expr::Bin(BinExpr { span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: ident("__wf_reg"), - type_ann: None, - }), - init: Some(Box::new(Expr::Bin(BinExpr { + op: BinaryOp::LogicalOr, + left: global_sym_access(), + right: Box::new(Expr::Paren(ParenExpr { span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: global_sym_access.clone(), - right: Box::new(Expr::Paren(ParenExpr { + expr: Box::new(Expr::Assign(AssignExpr { span: DUMMY_SP, - expr: Box::new(Expr::Assign(AssignExpr { + op: AssignOp::Assign, + left: AssignTarget::Simple(SimpleAssignTarget::Member(assign_target)), + right: Box::new(Expr::New(NewExpr { span: DUMMY_SP, - op: AssignOp::Assign, - left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { - span: DUMMY_SP, - obj: ident_expr("globalThis"), - prop: MemberProp::Computed(ComputedPropName { - span: DUMMY_SP, - expr: ident_expr("__wf_sym"), - }), - })), - right: Box::new(Expr::New(NewExpr { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: ident_expr("Map"), - args: Some(vec![]), - type_args: None, - })), + ctxt: SyntaxContext::empty(), + callee: Self::ident_expr("Map"), + args: Some(vec![]), + type_args: None, })), })), - }))), - definite: false, - }; + })), + }); - // __wf_reg.set(__wf_id, __wf_cls); - let set_call = Stmt::Expr(ExprStmt { + let mut decls = vec![ + declarator(sym_var, Some(sym_init)), + declarator(reg_var, Some(reg_init)), + ]; + decls.extend(extra_vars.iter().map(|name| declarator(name, None))); + + Stmt::Decl(Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + kind: VarDeclKind::Var, + declare: false, + decls, + }))) + } + + /// `.set(key, value);` + fn registry_set_stmt(reg_var: &str, key: Box, value: Box) -> Stmt { + Stmt::Expr(ExprStmt { 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: ident_expr("__wf_reg"), - prop: MemberProp::Ident(IdentName { - span: DUMMY_SP, - sym: "set".into(), - }), - }))), + callee: Callee::Expr(Self::member(Self::ident_expr(reg_var), "set")), args: vec![ ExprOrSpread { spread: None, - expr: ident_expr("__wf_id"), + expr: key, }, ExprOrSpread { spread: None, - expr: ident_expr("__wf_cls"), + expr: value, }, ], type_args: None, })), - }); + }) + } - // Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false }); - let define_property_call = Stmt::Expr(ExprStmt { + /// `Object.defineProperty(target, "prop", { : , ... })` + fn define_property_stmt( + target: Box, + prop: &str, + descriptor: Vec<(&str, Box)>, + ) -> Stmt { + Stmt::Expr(ExprStmt { 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: ident_expr("Object"), - prop: MemberProp::Ident(IdentName { - span: DUMMY_SP, - sym: "defineProperty".into(), - }), - }))), + callee: Callee::Expr(Self::member(Self::ident_expr("Object"), "defineProperty")), args: vec![ ExprOrSpread { spread: None, - expr: ident_expr("__wf_cls"), + expr: target, }, ExprOrSpread { spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: "classId".into(), - raw: None, - }))), + expr: Self::str_lit(prop), }, ExprOrSpread { spread: None, expr: Box::new(Expr::Object(ObjectLit { span: DUMMY_SP, - props: vec![ - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName { - span: DUMMY_SP, - sym: "value".into(), - }), - value: ident_expr("__wf_id"), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName { - span: DUMMY_SP, - sym: "writable".into(), - }), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: false, - }))), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName { - span: DUMMY_SP, - sym: "enumerable".into(), - }), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: false, - }))), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName { - span: DUMMY_SP, - sym: "configurable".into(), - }), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: false, - }))), - }))), - ], + props: descriptor + .into_iter() + .map(|(key, value)| { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new(key.into(), DUMMY_SP)), + value, + }))) + }) + .collect(), })), }, ], type_args: None, })), - }); + }) + } - // The function body: var decls + set + defineProperty - let function_body = BlockStmt { + fn bool_lit(value: bool) -> Box { + Box::new(Expr::Lit(Lit::Bool(Bool { span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - stmts: vec![ - // var __wf_sym = ..., __wf_reg = ...; - Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Var, - declare: false, - decls: vec![sym_decl, reg_decl], - }))), - set_call, - define_property_call, - ], - }; + value, + }))) + } + + /// The statements that register a step function `fn_ref` under `step_id` + /// in the registry held in `reg_var`, and stamp the function: + /// + /// ```js + /// __wf_reg.set(, ); + /// .stepId = ; + /// Object.defineProperty(, "name", { value: "", configurable: true }); + /// ``` + /// + /// The `name` definition preserves the original function name in stack + /// traces even after bundler minification. + fn step_registration_stmts( + reg_var: &str, + fn_ref: &Expr, + step_id: &Expr, + fn_name: &str, + ) -> Vec { + let boxed = |expr: &Expr| Box::new(expr.clone()); + vec![ + Self::registry_set_stmt(reg_var, boxed(step_id), boxed(fn_ref)), + Self::assign_stmt(*Self::member(boxed(fn_ref), "stepId"), step_id.clone()), + Self::define_property_stmt( + boxed(fn_ref), + "name", + vec![ + ("value", Self::str_lit(fn_name)), + ("configurable", Self::bool_lit(true)), + ], + ), + ] + } - // The IIFE: (function(__wf_cls, __wf_id) { ... })(ClassName, /* generated class ID string */); + /// The statements that register a class `cls_ref` for custom + /// serialization under `class_id` in the registry held in `reg_var`: + /// + /// ```js + /// __wf_reg.set(, ); + /// Object.defineProperty(, "classId", { value: , writable: false, enumerable: false, configurable: false }); + /// ``` + fn class_registration_stmts(reg_var: &str, cls_ref: &Expr, class_id: &Expr) -> Vec { + let boxed = |expr: &Expr| Box::new(expr.clone()); + vec![ + Self::registry_set_stmt(reg_var, boxed(class_id), boxed(cls_ref)), + Self::define_property_stmt( + boxed(cls_ref), + "classId", + vec![ + ("value", boxed(class_id)), + ("writable", Self::bool_lit(false)), + ("enumerable", Self::bool_lit(false)), + ("configurable", Self::bool_lit(false)), + ], + ), + ] + } + + /// `(function() { })();` + fn iife_stmt(params: &[&str], body: Vec, args: Vec) -> Stmt { Stmt::Expr(ExprStmt { span: DUMMY_SP, expr: Box::new(Expr::Call(CallExpr { @@ -3545,28 +3693,25 @@ impl StepTransform { expr: Box::new(Expr::Fn(FnExpr { ident: None, function: Box::new(Function { - params: vec![ - Param { - span: DUMMY_SP, - decorators: vec![], - pat: Pat::Ident(BindingIdent { - id: ident("__wf_cls"), - type_ann: None, - }), - }, - Param { + params: params + .iter() + .map(|name| Param { span: DUMMY_SP, decorators: vec![], pat: Pat::Ident(BindingIdent { - id: ident("__wf_id"), + id: Self::ident(name), type_ann: None, }), - }, - ], + }) + .collect(), decorators: vec![], span: DUMMY_SP, ctxt: SyntaxContext::empty(), - body: Some(function_body), + body: Some(BlockStmt { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + stmts: body, + }), is_generator: false, is_async: false, type_params: None, @@ -3574,27 +3719,55 @@ impl StepTransform { }), })), }))), - args: vec![ - // First argument: ClassName - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Ident(ident(class_name))), - }, - // Second argument: class ID string - ExprOrSpread { + args: args + .into_iter() + .map(|expr| ExprOrSpread { spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: class_id.into(), - raw: None, - }))), - }, - ], + expr: Box::new(expr), + }) + .collect(), type_args: None, })), }) } + fn class_id_for(&self, class_name: &str) -> String { + naming::format_name("class", &self.get_module_path(), class_name) + } + + /// Build the inline IIFE that registers a class for custom serialization. + /// `class_ref` is the identifier used to reference the class; `class_name` + /// is the logical class name used to derive the class ID. + /// + /// Generates: + /// (function(__wf_cls, __wf_id) { + /// var __wf_sym = Symbol.for("workflow-class-registry"), + /// __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + /// __wf_reg.set(__wf_id, __wf_cls); + /// Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false }); + /// })(ClassName, "class//module_path//ClassName"); + fn create_class_serialization_registration(&self, class_ref: &str, class_name: &str) -> Stmt { + let mut body = vec![Self::registry_lookup_stmt( + "__wf_sym", + "__wf_reg", + CLASS_REGISTRY_KEY, + &[], + )]; + body.extend(Self::class_registration_stmts( + "__wf_reg", + &Self::ident_expr("__wf_cls"), + &Self::ident_expr("__wf_id"), + )); + Self::iife_stmt( + &["__wf_cls", "__wf_id"], + body, + vec![ + *Self::ident_expr(class_ref), + *Self::str_lit(&self.class_id_for(class_name)), + ], + ) + } + // Create an inline step function registration statement (step mode). // Instead of importing registerStepFunction from "workflow/internal/private", // we inline the registration logic as a self-contained IIFE that has zero module dependencies. @@ -3607,307 +3780,58 @@ impl StepTransform { // __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); // __wf_reg.set(__wf_id, __wf_fn); // __wf_fn.stepId = __wf_id; + // Object.defineProperty(__wf_fn, "name", { value: "fnName", configurable: true }); // })(fnRef, "step//module_path//fnName"); fn create_inline_step_registration(&self, step_id: &str, fn_ref: Expr, fn_name: &str) -> Stmt { - // Helper to create an identifier + let mut body = vec![Self::registry_lookup_stmt( + "__wf_sym", + "__wf_reg", + STEP_REGISTRY_KEY, + &[], + )]; + body.extend(Self::step_registration_stmts( + "__wf_reg", + &Self::ident_expr("__wf_fn"), + &Self::ident_expr("__wf_id"), + fn_name, + )); + Self::iife_stmt( + &["__wf_fn", "__wf_id"], + body, + vec![fn_ref, *Self::str_lit(step_id)], + ) + } + + // Create an inline closure variable access expression (step mode). + // Instead of importing __private_getClosureVars from "workflow/internal/private", + // we inline the access as a self-contained IIFE that reads from the global + // AsyncLocalStorage context. + // + // Generates: + // (function() { + // var __wf_ctx = globalThis[Symbol.for("WORKFLOW_STEP_CONTEXT_STORAGE")], + // __wf_store = __wf_ctx && __wf_ctx.getStore(); + // if (!__wf_store) throw new Error("Closure variables can only be accessed inside a step function"); + // return __wf_store.closureVars || {}; + // })() + fn create_inline_get_closure_vars(&self) -> Expr { let ident = |name: &str| -> Ident { Ident::new(name.into(), DUMMY_SP, SyntaxContext::empty()) }; - - // Helper to create an identifier expression let ident_expr = |name: &str| -> Box { Box::new(Expr::Ident(ident(name))) }; - // var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), - // __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - let sym_decl = VarDeclarator { + // var __wf_ctx = globalThis[Symbol.for("WORKFLOW_STEP_CONTEXT_STORAGE")] + let ctx_decl = VarDeclarator { span: DUMMY_SP, name: Pat::Ident(BindingIdent { - id: ident("__wf_sym"), + id: ident("__wf_ctx"), type_ann: None, }), - init: Some(Box::new(Expr::Call(CallExpr { + init: Some(Box::new(Expr::Member(MemberExpr { span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + obj: ident_expr("globalThis"), + prop: MemberProp::Computed(ComputedPropName { span: DUMMY_SP, - obj: ident_expr("Symbol"), - prop: MemberProp::Ident(IdentName { - span: DUMMY_SP, - sym: "for".into(), - }), - }))), - args: vec![ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: "@workflow/core//registeredSteps".into(), - raw: None, - }))), - }], - type_args: None, - }))), - definite: false, - }; - - let global_sym_access = Box::new(Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: ident_expr("globalThis"), - prop: MemberProp::Computed(ComputedPropName { - span: DUMMY_SP, - expr: ident_expr("__wf_sym"), - }), - })); - - let reg_decl = VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: ident("__wf_reg"), - type_ann: None, - }), - init: Some(Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: global_sym_access.clone(), - right: Box::new(Expr::Paren(ParenExpr { - span: DUMMY_SP, - expr: Box::new(Expr::Assign(AssignExpr { - span: DUMMY_SP, - op: AssignOp::Assign, - left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { - span: DUMMY_SP, - obj: ident_expr("globalThis"), - prop: MemberProp::Computed(ComputedPropName { - span: DUMMY_SP, - expr: ident_expr("__wf_sym"), - }), - })), - right: Box::new(Expr::New(NewExpr { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: ident_expr("Map"), - args: Some(vec![]), - type_args: None, - })), - })), - })), - }))), - definite: false, - }; - - // __wf_reg.set(__wf_id, __wf_fn); - let set_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::Member(MemberExpr { - span: DUMMY_SP, - obj: ident_expr("__wf_reg"), - prop: MemberProp::Ident(IdentName { - span: DUMMY_SP, - sym: "set".into(), - }), - }))), - args: vec![ - ExprOrSpread { - spread: None, - expr: ident_expr("__wf_id"), - }, - ExprOrSpread { - spread: None, - expr: ident_expr("__wf_fn"), - }, - ], - type_args: None, - })), - }); - - // __wf_fn.stepId = __wf_id; - let step_id_assignment = Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Assign(AssignExpr { - span: DUMMY_SP, - op: AssignOp::Assign, - left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { - span: DUMMY_SP, - obj: ident_expr("__wf_fn"), - prop: MemberProp::Ident(IdentName { - span: DUMMY_SP, - sym: "stepId".into(), - }), - })), - right: ident_expr("__wf_id"), - })), - }); - - // Object.defineProperty(__wf_fn, "name", { value: "originalName", configurable: true }) - // This preserves the original function name in stack traces even after bundler minification. - let define_name = Stmt::Expr(ExprStmt { - 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: ident_expr("Object"), - prop: MemberProp::Ident(IdentName { - span: DUMMY_SP, - sym: "defineProperty".into(), - }), - }))), - args: vec![ - ExprOrSpread { - spread: None, - expr: ident_expr("__wf_fn"), - }, - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: "name".into(), - raw: None, - }))), - }, - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Object(ObjectLit { - span: DUMMY_SP, - props: vec![ - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName { - span: DUMMY_SP, - sym: "value".into(), - }), - value: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: fn_name.into(), - raw: None, - }))), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName { - span: DUMMY_SP, - sym: "configurable".into(), - }), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: true, - }))), - }))), - ], - })), - }, - ], - type_args: None, - })), - }); - - // The function body: var decls + set + stepId assignment + name preservation - let function_body = BlockStmt { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - stmts: vec![ - Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Var, - declare: false, - decls: vec![sym_decl, reg_decl], - }))), - set_call, - step_id_assignment, - define_name, - ], - }; - - // The IIFE: (function(__wf_fn, __wf_id) { ... })(fnRef, "step_id"); - Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Call(CallExpr { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Paren(ParenExpr { - span: DUMMY_SP, - expr: Box::new(Expr::Fn(FnExpr { - ident: None, - function: Box::new(Function { - params: vec![ - Param { - span: DUMMY_SP, - decorators: vec![], - pat: Pat::Ident(BindingIdent { - id: ident("__wf_fn"), - type_ann: None, - }), - }, - Param { - span: DUMMY_SP, - decorators: vec![], - pat: Pat::Ident(BindingIdent { - id: ident("__wf_id"), - type_ann: None, - }), - }, - ], - decorators: vec![], - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - body: Some(function_body), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - }), - })), - }))), - args: vec![ - ExprOrSpread { - spread: None, - expr: Box::new(fn_ref), - }, - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: step_id.into(), - raw: None, - }))), - }, - ], - type_args: None, - })), - }) - } - - // Create an inline closure variable access expression (step mode). - // Instead of importing __private_getClosureVars from "workflow/internal/private", - // we inline the access as a self-contained IIFE that reads from the global - // AsyncLocalStorage context. - // - // Generates: - // (function() { - // var __wf_ctx = globalThis[Symbol.for("WORKFLOW_STEP_CONTEXT_STORAGE")], - // __wf_store = __wf_ctx && __wf_ctx.getStore(); - // if (!__wf_store) throw new Error("Closure variables can only be accessed inside a step function"); - // return __wf_store.closureVars || {}; - // })() - fn create_inline_get_closure_vars(&self) -> Expr { - let ident = - |name: &str| -> Ident { Ident::new(name.into(), DUMMY_SP, SyntaxContext::empty()) }; - let ident_expr = |name: &str| -> Box { Box::new(Expr::Ident(ident(name))) }; - - // var __wf_ctx = globalThis[Symbol.for("WORKFLOW_STEP_CONTEXT_STORAGE")] - let ctx_decl = VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: ident("__wf_ctx"), - type_ann: None, - }), - init: Some(Box::new(Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: ident_expr("globalThis"), - prop: MemberProp::Computed(ComputedPropName { - span: DUMMY_SP, - expr: Box::new(Expr::Call(CallExpr { + expr: Box::new(Expr::Call(CallExpr { span: DUMMY_SP, ctxt: SyntaxContext::empty(), callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { @@ -5109,836 +5033,1003 @@ impl<'a> ComprehensiveUsageCollector<'a> { } } -impl VisitMut for StepTransform { - fn visit_mut_program(&mut self, program: &mut Program) { - // First pass: collect step functions - program.visit_mut_children_with(self); +/// Identifier used inside a class-expression registration IIFE to refer to +/// the class being registered (the IIFE's parameter). +const CLASS_EXPR_IIFE_PARAM: &str = "__wf_cls"; - // Preserve class names for manifest before they get drained during registration - self.classes_for_manifest = self.classes_needing_serialization.clone(); +/// `Symbol.for` key of the global step registry (`Map`). +const STEP_REGISTRY_KEY: &str = "@workflow/core//registeredSteps"; +/// `Symbol.for` key of the global serialization class registry (`Map`). +const CLASS_REGISTRY_KEY: &str = "workflow-class-registry"; - // Add necessary imports and registrations - match program { - Program::Module(module) => { - // All registrations are now inlined (no imports needed). +/// Builders for the module-level code that registers a class's step methods, +/// getters, workflows and custom serialization. +/// +/// Every builder takes `class_ref`, the identifier through which the emitted +/// code refers to the class. For class declarations this is the class name and +/// the statements are appended to the module body. For class expressions the +/// class has no guaranteed module-scope binding (`exports.Foo = class {}`, +/// `foo(class {})`, `var A = class {}, B = class {}`, ...), so the statements +/// are placed inside an IIFE that receives the class as its argument and +/// `class_ref` is that parameter. See `wrap_class_expr_with_registrations`. +impl StepTransform { + fn ident(name: &str) -> Ident { + Ident::new(name.into(), DUMMY_SP, SyntaxContext::empty()) + } - // Add hoisted object property functions and registration calls at the end for step mode - if matches!(self.mode, TransformMode::Step) { - // Calculate insertion position once before any hoisting - let initial_insert_pos = module - .body - .iter() - .position(|item| { - !matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_))) - }) - .unwrap_or(0); - let mut current_insert_pos = initial_insert_pos; + fn ident_expr(name: &str) -> Box { + Box::new(Expr::Ident(Self::ident(name))) + } - // Process nested step functions FIRST (they typically appear earlier in source) - let nested_functions: Vec<_> = self.nested_step_functions.drain(..).collect(); + fn str_lit(value: &str) -> Box { + Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: value.into(), + raw: None, + }))) + } - for ( - fn_name, - mut fn_expr, - span, - closure_vars, - was_arrow, - parent_workflow_name, - references_lexical_this, - ) in nested_functions - { - // Generate hoisted name including parent workflow function name - let hoisted_name = if parent_workflow_name.is_empty() { - fn_name.clone() - } else { - format!("{}${}", parent_workflow_name, fn_name) - }; - // If there are closure variables, add destructuring as first statement - if !closure_vars.is_empty() { - if let Some(body) = &mut fn_expr.function.body { - // First, normalize the SyntaxContext of closure variable references in the body - // This ensures they match the identifiers we create in the destructuring pattern - ClosureVariableNormalizer::normalize_function_body( - &closure_vars, - body, - ); + /// `class_ref.prop` + fn member(obj: Box, prop: &str) -> Box { + Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj, + prop: MemberProp::Ident(IdentName::new(prop.into(), DUMMY_SP)), + })) + } - // Create destructuring statement using inline IIFE: - // const { var1, var2 } = (function() { ... })(); - let closure_destructure = - Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Const, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Object(ObjectPat { - span: DUMMY_SP, - props: closure_vars - .iter() - .map(|var_name| { - ObjectPatProp::Assign(AssignPatProp { - span: DUMMY_SP, - key: BindingIdent { - id: Ident::new( - var_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - ), - type_ann: None, - }, - value: None, - }) - }) - .collect(), - optional: false, - type_ann: None, - }), - init: Some(Box::new( - self.create_inline_get_closure_vars(), - )), - definite: false, - }], - declare: false, - }))); + /// `obj["name"]` + fn computed_member(obj: Box, name: &str) -> Box { + Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj, + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Self::str_lit(name), + }), + })) + } - // Prepend to function body - body.stmts.insert(0, closure_destructure); - } - } + /// `Object.getOwnPropertyDescriptor(target, "name").get` + fn getter_ref(target: Box, name: &str) -> Expr { + Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Self::member( + Self::ident_expr("Object"), + "getOwnPropertyDescriptor", + )), + args: vec![ + ExprOrSpread { + spread: None, + expr: target, + }, + ExprOrSpread { + spread: None, + expr: Self::str_lit(name), + }, + ], + type_args: None, + })), + prop: MemberProp::Ident(IdentName::new("get".into(), DUMMY_SP)), + }) + } - // Create the appropriate hoisted declaration based on original function type. - // - // If the original arrow body referenced lexical `this`, we - // hoist as a regular `function` (not an arrow) so that the - // workflow runtime's `stepFn.apply(thisVal, args)` can - // rebind `this` to the value captured at call time. - let hoisted_decl = if was_arrow && !references_lexical_this { - // Convert back to arrow function: var name = async () => { ... }; - let arrow_expr = self.convert_fn_expr_to_arrow(&fn_expr); - ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Var, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: Ident::new( - hoisted_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - ), - type_ann: None, - }), - init: Some(Box::new(Expr::Arrow(arrow_expr))), - definite: false, - }], - declare: false, - })))) - } else { - // Keep as function declaration: async function name() { ... } - ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { - ident: Ident::new( - hoisted_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - ), - function: fn_expr.function, - declare: false, - }))) - }; + /// `target = value;` where `target` is a member expression. + fn assign_stmt(target: Expr, value: Expr) -> Stmt { + let Expr::Member(target) = target else { + unreachable!("assignment targets built here are always member expressions"); + }; + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + left: AssignTarget::Simple(SimpleAssignTarget::Member(target)), + op: AssignOp::Assign, + right: Box::new(value), + })), + }) + } - // Insert at current position and increment for next iteration - module.body.insert(current_insert_pos, hoisted_decl); - current_insert_pos += 1; + /// `Object.defineProperty(target, "name", { value: "name", configurable: true })` + fn define_name_stmt(target: &str, name: &str) -> Stmt { + Self::define_property_stmt( + Self::ident_expr(target), + "name", + vec![ + ("value", Self::str_lit(name)), + ("configurable", Self::bool_lit(true)), + ], + ) + } - // Create a registration call or stepId assignment with parent workflow name in the step ID - let step_fn_name = - self.record_nested_step_name(&fn_name, &parent_workflow_name); - let step_id = self.create_id(Some(&step_fn_name), span, false); + fn var_stmt(name: &str, init: Expr) -> Stmt { + Stmt::Decl(Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + kind: VarDeclKind::Var, + declare: false, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: Self::ident(name), + type_ann: None, + }), + init: Some(Box::new(init)), + definite: false, + }], + }))) + } - // Insert inline IIFE registration right after the hoisted declaration - let registration_stmt = { - let fn_ref = Expr::Ident(Ident::new( - hoisted_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - )); - self.create_inline_step_registration(&step_id, fn_ref, &hoisted_name) - }; - module - .body - .insert(current_insert_pos, ModuleItem::Stmt(registration_stmt)); - current_insert_pos += 1; - } + /// Step mode: register `class_ref.method` as a step. + fn build_static_step_registration( + &self, + class_ref: &str, + method_name: &str, + step_id: &str, + ) -> Stmt { + let fn_ref = *Self::member(Self::ident_expr(class_ref), method_name); + self.create_inline_step_registration(step_id, fn_ref, method_name) + } - // Then process object property step functions (they typically appear later) - // Collect hoisting information before the loop - let hoisting_info: Vec<_> = self - .object_property_step_functions - .iter() - .map( - |(parent_var, prop_name, fn_expr, _span, workflow_name, _was_arrow)| { - // Replace slashes with $ in parent_var to create valid JS identifier - let safe_parent_var = parent_var.replace('/', "$"); - let hoist_var_name = if !workflow_name.is_empty() { - format!("{}${}${}", workflow_name, safe_parent_var, prop_name) - } else { - format!("{}${}", safe_parent_var, prop_name) - }; - let wf_name = if workflow_name.is_empty() { - None - } else { - Some(workflow_name.as_str()) - }; - let step_id = self.create_object_property_id( - parent_var, prop_name, false, wf_name, - ); - (hoist_var_name, fn_expr.clone(), step_id, parent_var.clone()) - }, - ) - .collect(); + /// Step mode: register `class_ref.prototype["method"]` as a step. + fn build_instance_step_registration( + &self, + class_ref: &str, + method_name: &str, + step_id: &str, + ) -> Stmt { + let fn_ref = *Self::computed_member( + Self::member(Self::ident_expr(class_ref), "prototype"), + method_name, + ); + self.create_inline_step_registration(step_id, fn_ref, method_name) + } - // Now drain and process - self.object_property_step_functions.drain(..); + /// Step mode: register the getter function of `class_ref.prototype` (or of + /// `class_ref` itself for static getters) as a step. + fn build_getter_step_registration( + &self, + class_ref: &str, + getter_name: &str, + step_id: &str, + is_static: bool, + ) -> Stmt { + let target = if is_static { + Self::ident_expr(class_ref) + } else { + Self::member(Self::ident_expr(class_ref), "prototype") + }; + self.create_inline_step_registration( + step_id, + Self::getter_ref(target, getter_name), + getter_name, + ) + } - for (hoist_var_name, fn_expr, step_id, _parent_var) in hoisting_info { - // Create a var declaration for the hoisted function - // Using function expression (not arrow) to preserve `this` binding - let hoisted_decl = - ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Var, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: Ident::new( - hoist_var_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - ), - type_ann: None, - }), - init: Some(Box::new(Expr::Fn(fn_expr))), - definite: false, - }], - declare: false, - })))); + /// Workflow mode: `class_ref.method = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id")` + /// (or on `class_ref.prototype["method"]` for instance methods), replacing + /// the method that was stripped from the class body. + fn build_step_proxy_assignment( + &self, + class_ref: &str, + method_name: &str, + step_id: &str, + is_static: bool, + ) -> Stmt { + let target = if is_static { + Self::member(Self::ident_expr(class_ref), method_name) + } else { + Self::computed_member( + Self::member(Self::ident_expr(class_ref), "prototype"), + method_name, + ) + }; + Self::assign_stmt(*target, self.create_step_initializer(step_id)) + } - // Insert at current position and increment for next iteration - module.body.insert(current_insert_pos, hoisted_decl); - current_insert_pos += 1; + /// Workflow mode: redefine a stripped getter step as a property whose + /// getter invokes the step proxy: + /// + /// ```js + /// var __step_Class$getter = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id"); + /// Object.defineProperty(Class.prototype, "getter", { + /// get() { return __step_Class$getter.call(this); }, + /// configurable: true, + /// enumerable: false + /// }); + /// ``` + /// + /// Static getters target `Class` and call the proxy without `this`. + fn build_getter_step_definition( + &self, + class_ref: &str, + class_name: &str, + getter_name: &str, + step_id: &str, + is_static: bool, + ) -> Vec { + let var_name = format!( + "__step_{}${}", + sanitize_ident_part(class_name), + sanitize_ident_part(getter_name) + ); - // Insert inline IIFE registration right after the hoisted declaration - let registration_stmt = { - let fn_ref = Expr::Ident(Ident::new( - hoist_var_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - )); - self.create_inline_step_registration(&step_id, fn_ref, &hoist_var_name) - }; - module - .body - .insert(current_insert_pos, ModuleItem::Stmt(registration_stmt)); - current_insert_pos += 1; - } + let var_decl = Self::var_stmt(&var_name, self.create_step_initializer(step_id)); - // Add static method step registrations (inline IIFE) - let static_step_regs: Vec<_> = - self.static_method_step_registrations.drain(..).collect(); - for (class_name, method_name, step_id, _span) in static_step_regs { - let fn_ref = 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( - method_name.clone().into(), - DUMMY_SP, - )), - }); - let registration_call = - self.create_inline_step_registration(&step_id, fn_ref, &method_name); - module.body.push(ModuleItem::Stmt(registration_call)); - } + let proxy_call = if is_static { + // __step_var() + Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Self::ident_expr(&var_name)), + args: vec![], + type_args: None, + }) + } else { + // __step_var.call(this) + Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Self::member(Self::ident_expr(&var_name), "call")), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::This(ThisExpr { span: DUMMY_SP })), + }], + type_args: None, + }) + }; - // Add instance method step registrations (inline IIFE) - // For instance methods, we register ClassName.prototype["methodName"] - let instance_step_regs: Vec<_> = - self.instance_method_step_registrations.drain(..).collect(); - for (class_name, method_name, step_id, _span) in instance_step_regs { - let fn_ref = 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::Computed(ComputedPropName { - span: DUMMY_SP, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: method_name.clone().into(), - raw: None, - }))), - }), - }); - let registration_call = - self.create_inline_step_registration(&step_id, fn_ref, &method_name); - module.body.push(ModuleItem::Stmt(registration_call)); - } + let bool_prop = |key: &str, value: bool| { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName::new(key.into(), DUMMY_SP)), + value: Box::new(Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value, + }))), + }))) + }; - // Add instance getter step registrations - // For getters, we register Object.getOwnPropertyDescriptor(ClassName.prototype, "getterName").get - // using an inline IIFE (same pattern as other step registrations) - for (class_name, getter_name, step_id, _span) in { - let regs: Vec<_> = - self.instance_getter_step_registrations.drain(..).collect(); - regs - } - .into_iter() - { - // Build: Object.getOwnPropertyDescriptor(ClassName.prototype, "getterName").get - let getter_ref = Expr::Member(MemberExpr { + let descriptor = Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![ + PropOrSpread::Prop(Box::new(Prop::Method(MethodProp { + key: PropName::Ident(IdentName::new("get".into(), DUMMY_SP)), + function: Box::new(Function { + params: vec![], + decorators: vec![], + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + body: Some(BlockStmt { span: DUMMY_SP, - obj: Box::new(Expr::Call(CallExpr { + ctxt: SyntaxContext::empty(), + stmts: vec![Stmt::Return(ReturnStmt { span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Ident(Ident::new( - "Object".into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - prop: MemberProp::Ident(IdentName::new( - "getOwnPropertyDescriptor".into(), - DUMMY_SP, - )), - }))), - args: vec![ - // First arg: ClassName.prototype - ExprOrSpread { - spread: None, - expr: 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, - )), - })), - }, - // Second arg: "getterName" - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: getter_name.clone().into(), - raw: None, - }))), - }, - ], - type_args: None, - })), - prop: MemberProp::Ident(IdentName::new("get".into(), DUMMY_SP)), - }); + arg: Some(Box::new(proxy_call)), + })], + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + }), + }))), + bool_prop("configurable", true), + bool_prop("enumerable", false), + ], + }); - let registration_call = self.create_inline_step_registration( - &step_id, - getter_ref, - &getter_name, - ); - module.body.push(ModuleItem::Stmt(registration_call)); - } + let target = if is_static { + Self::ident_expr(class_ref) + } else { + Self::member(Self::ident_expr(class_ref), "prototype") + }; - // Add static getter step registrations - // For static getters, we register Object.getOwnPropertyDescriptor(ClassName, "getterName").get - for (class_name, getter_name, step_id, _span) in { - let regs: Vec<_> = - self.static_getter_step_registrations.drain(..).collect(); - regs - } - .into_iter() - { - // Build: Object.getOwnPropertyDescriptor(ClassName, "getterName").get - let getter_ref = Expr::Member(MemberExpr { + let define_property_call = Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Self::member(Self::ident_expr("Object"), "defineProperty")), + args: vec![ + ExprOrSpread { + spread: None, + expr: target, + }, + ExprOrSpread { + spread: None, + expr: Self::str_lit(getter_name), + }, + ExprOrSpread { + spread: None, + expr: Box::new(descriptor), + }, + ], + type_args: None, + })), + }); + + vec![var_decl, define_property_call] + } + + /// `class_ref.method.workflowId = "workflow_id"` + fn build_static_workflow_id_assignment( + &self, + class_ref: &str, + method_name: &str, + workflow_id: &str, + ) -> Stmt { + Self::assign_stmt( + *Self::member( + Self::member(Self::ident_expr(class_ref), method_name), + "workflowId", + ), + *Self::str_lit(workflow_id), + ) + } + + /// Workflow mode: `globalThis.__private_workflows.set("workflow_id", class_ref.method)` + fn build_static_workflow_registration( + &self, + class_ref: &str, + method_name: &str, + workflow_id: &str, + ) -> Stmt { + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Self::member( + Self::member(Self::ident_expr("globalThis"), "__private_workflows"), + "set", + )), + args: vec![ + ExprOrSpread { + spread: None, + expr: Self::str_lit(workflow_id), + }, + ExprOrSpread { + spread: None, + expr: Self::member(Self::ident_expr(class_ref), method_name), + }, + ], + type_args: None, + })), + }) + } + + /// Remove and return the entries of `entries` whose class (selected by + /// `class_of`) is `class_name`, preserving the order of the rest. + fn drain_class_entries( + entries: &mut Vec, + class_name: &str, + class_of: impl Fn(&T) -> &str, + ) -> Vec { + let (taken, kept): (Vec, Vec) = std::mem::take(entries) + .into_iter() + .partition(|entry| class_of(entry) == class_name); + *entries = kept; + taken + } + + /// Build the statements that register everything recorded for + /// `class_name` while visiting its body, referring to the class as + /// `class_ref`. The recorded entries are consumed so the module-level + /// emission in `visit_mut_program` does not register them a second time. + /// + /// The statements are meant to run inside a scope of their own (the class + /// expression's registration IIFE), so the registry lookups are hoisted + /// once per registry rather than repeated per registration as the + /// module-level IIFEs do: + /// + /// ```js + /// var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), + /// __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + /// __wf_fn = __wf_cls.prototype["run"]; + /// __wf_reg.set("step//...//Foo#run", __wf_fn); + /// __wf_fn.stepId = "step//...//Foo#run"; + /// Object.defineProperty(__wf_fn, "name", { value: "run", configurable: true }); + /// var __wf_cls_sym = Symbol.for("workflow-class-registry"), + /// __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + /// __wf_cls_reg.set("class//...//Foo", __wf_cls); + /// Object.defineProperty(__wf_cls, "classId", { ... }); + /// ``` + /// + /// The statement order mirrors the module-level emission: step + /// registrations (or, in workflow mode, proxy assignments for the stripped + /// methods), getters, custom serialization, then workflow methods. + fn build_class_registration_stmts(&mut self, class_name: &str, class_ref: &str) -> Vec { + let mut stmts = Vec::new(); + + let static_steps = Self::drain_class_entries( + &mut self.static_method_step_registrations, + class_name, + |(cn, ..)| cn, + ); + let instance_steps = Self::drain_class_entries( + &mut self.instance_method_step_registrations, + class_name, + |(cn, ..)| cn, + ); + let instance_getters = Self::drain_class_entries( + &mut self.instance_getter_step_registrations, + class_name, + |(cn, ..)| cn, + ); + let static_getters = Self::drain_class_entries( + &mut self.static_getter_step_registrations, + class_name, + |(cn, ..)| cn, + ); + let static_strips = Self::drain_class_entries( + &mut self.static_step_methods_to_strip, + class_name, + |(cn, ..)| cn, + ); + let instance_strips = Self::drain_class_entries( + &mut self.instance_step_methods_to_strip, + class_name, + |(cn, ..)| cn, + ); + let instance_getter_strips = Self::drain_class_entries( + &mut self.instance_getter_steps_to_strip, + class_name, + |(cn, ..)| cn, + ); + let static_getter_strips = Self::drain_class_entries( + &mut self.static_getter_steps_to_strip, + class_name, + |(cn, ..)| cn, + ); + let workflows = Self::drain_class_entries( + &mut self.static_method_workflow_registrations, + class_name, + |(cn, ..)| cn, + ); + let needs_serialization = self.classes_needing_serialization.remove(class_name); + if needs_serialization { + // The module-level pass snapshots `classes_needing_serialization` + // for the manifest after traversal; this class is consumed before + // then, so record it directly. + self.classes_for_manifest.insert(class_name.to_string()); + } + + let cls = Self::ident_expr(class_ref); + let proto = || Self::member(Self::ident_expr(class_ref), "prototype"); + + match self.mode { + TransformMode::Step => { + // (fn reference, step id, name to stamp on the function) + let mut steps: Vec<(Box, &str, &str)> = Vec::new(); + for (_, method, step_id, _) in &static_steps { + steps.push((Self::member(cls.clone(), method), step_id, method)); + } + for (_, method, step_id, _) in &instance_steps { + steps.push((Self::computed_member(proto(), method), step_id, method)); + } + for (_, getter, step_id, _) in &instance_getters { + steps.push((Box::new(Self::getter_ref(proto(), getter)), step_id, getter)); + } + for (_, getter, step_id, _) in &static_getters { + steps.push(( + Box::new(Self::getter_ref(cls.clone(), getter)), + step_id, + getter, + )); + } + + if !steps.is_empty() { + stmts.push(Self::registry_lookup_stmt( + "__wf_sym", + "__wf_reg", + STEP_REGISTRY_KEY, + &["__wf_fn"], + )); + let fn_var = Self::ident_expr("__wf_fn"); + for (fn_ref, step_id, fn_name) in steps { + // __wf_fn = ; + stmts.push(Stmt::Expr(ExprStmt { span: DUMMY_SP, - obj: Box::new(Expr::Call(CallExpr { + expr: Box::new(Expr::Assign(AssignExpr { span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Ident(Ident::new( - "Object".into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - prop: MemberProp::Ident(IdentName::new( - "getOwnPropertyDescriptor".into(), - DUMMY_SP, - )), - }))), - args: vec![ - // First arg: ClassName (not .prototype for static) - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Ident(Ident::new( - class_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - }, - // Second arg: "getterName" - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: getter_name.clone().into(), - raw: None, - }))), + op: AssignOp::Assign, + left: AssignTarget::Simple(SimpleAssignTarget::Ident( + BindingIdent { + id: Self::ident("__wf_fn"), + type_ann: None, }, - ], - type_args: None, + )), + right: fn_ref, })), - prop: MemberProp::Ident(IdentName::new("get".into(), DUMMY_SP)), - }); - - let registration_call = self.create_inline_step_registration( - &step_id, - getter_ref, - &getter_name, - ); - module.body.push(ModuleItem::Stmt(registration_call)); + })); + stmts.extend(Self::step_registration_stmts( + "__wf_reg", + &fn_var, + &Self::str_lit(step_id), + fn_name, + )); } + } + } + TransformMode::Workflow => { + for (_, method, step_id) in &static_strips { + stmts.push(self.build_step_proxy_assignment(class_ref, method, step_id, true)); + } + for (_, method, step_id) in &instance_strips { + stmts.push(self.build_step_proxy_assignment(class_ref, method, step_id, false)); + } + for (_, getter, step_id) in &instance_getter_strips { + stmts.extend(self.build_getter_step_definition( + class_ref, class_name, getter, step_id, false, + )); + } + for (_, getter, step_id) in &static_getter_strips { + stmts.extend(self.build_getter_step_definition( + class_ref, class_name, getter, step_id, true, + )); + } + } + TransformMode::Detect => return stmts, + } - // Add class serialization registrations for step mode - // Uses inlined IIFE registration (no import needed) - // Sort for deterministic output ordering - let mut sorted_classes: Vec<_> = - self.classes_needing_serialization.drain().collect(); - sorted_classes.sort(); - for class_name in sorted_classes { - let registration_call = - self.create_class_serialization_registration(&class_name); - module.body.push(ModuleItem::Stmt(registration_call)); - } + if needs_serialization { + stmts.push(Self::registry_lookup_stmt( + "__wf_cls_sym", + "__wf_cls_reg", + CLASS_REGISTRY_KEY, + &[], + )); + stmts.extend(Self::class_registration_stmts( + "__wf_cls_reg", + &cls, + &Self::str_lit(&self.class_id_for(class_name)), + )); + } + + for (_, method, workflow_id, _) in &workflows { + stmts.push(self.build_static_workflow_id_assignment(class_ref, method, workflow_id)); + if matches!(self.mode, TransformMode::Workflow) { + stmts.push(self.build_static_workflow_registration(class_ref, method, workflow_id)); + } + } + + stmts + } + + /// Wrap a class expression that needs registration code in an IIFE that + /// receives the class, runs the registrations, and returns it: + /// + /// ```js + /// var Foo = class { async run() { "use step"; } }; + /// // becomes + /// var Foo = (function(__wf_cls) { + /// (function(__wf_fn, __wf_id) { ... })(__wf_cls.prototype["run"], "step//...//Foo#run"); + /// return __wf_cls; + /// })(class { async run() { ... } }); + /// ``` + /// + /// The IIFE closes over the class value itself, so the registration does + /// not depend on the class being reachable through a module-scope binding. + /// This makes every position a class expression can appear in work the + /// same way: `exports.Foo = class {}`, `{ Foo: class {} }`, + /// `var A = class {}, B = class {}`, `foo(class Named {})`, etc. + /// + /// Leaves the expression untouched when nothing was recorded for the class. + fn wrap_class_expr_with_registrations(&mut self, expr: &mut Expr, pending: PendingClassExpr) { + let PendingClassExpr { + name: class_name, + ident_to_insert, + .. + } = pending; + + let stmts = self.build_class_registration_stmts(&class_name, CLASS_EXPR_IIFE_PARAM); + if stmts.is_empty() { + return; + } + + let Expr::Class(mut class_expr) = + std::mem::replace(expr, Expr::Invalid(Invalid { span: DUMMY_SP })) + else { + unreachable!("wrap_class_expr_with_registrations is only called on class expressions"); + }; + + let mut body = Vec::with_capacity(stmts.len() + 2); + + if class_expr.ident.is_none() { + match ident_to_insert { + // `var Foo = class {}` -> `var Foo = (...)(class Foo {})`. + // Passing the class as a call argument defeats the `.name` + // inference the original assignment provided, so make the + // name explicit. This mirrors `var Foo = class Foo {}`, which + // is behaviorally equivalent for typical class usage. + Some(name) => { + class_expr.ident = Some(Self::ident(&name)); + } + // The name came from a property key (`exports.Foo = class {}`, + // `{ Foo: class {} }`). Introducing `Foo` as the class's own + // binding could shadow an unrelated outer `Foo` referenced + // from the class body, so set `.name` at runtime instead. + None => { + body.push(Self::define_name_stmt(CLASS_EXPR_IIFE_PARAM, &class_name)); } + } + } - // Hoist getter workflow proxy vars for object literal getters (workflow mode) - // These must be inserted before the code that references them - if matches!(self.mode, TransformMode::Workflow) - && !self.getter_workflow_proxy_hoists.is_empty() - { - let insert_pos = module + body.extend(stmts); + body.push(Stmt::Return(ReturnStmt { + span: DUMMY_SP, + arg: Some(Self::ident_expr(CLASS_EXPR_IIFE_PARAM)), + })); + + let iife = Expr::Fn(FnExpr { + ident: None, + function: Box::new(Function { + params: vec![Param { + span: DUMMY_SP, + decorators: vec![], + pat: Pat::Ident(BindingIdent { + id: Self::ident(CLASS_EXPR_IIFE_PARAM), + type_ann: None, + }), + }], + decorators: vec![], + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + body: Some(BlockStmt { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + stmts: body, + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + }), + }); + + *expr = Expr::Call(CallExpr { + span: class_expr.class.span, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Paren(ParenExpr { + span: DUMMY_SP, + expr: Box::new(iife), + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Class(class_expr)), + }], + type_args: None, + }); + } +} + +impl VisitMut for StepTransform { + fn visit_mut_program(&mut self, program: &mut Program) { + // First pass: collect step functions + program.visit_mut_children_with(self); + + // Preserve class names for manifest before they get drained during + // registration. Class expressions were already drained (and recorded) + // while wrapping them in their registration IIFE, so extend rather + // than replace. + self.classes_for_manifest + .extend(self.classes_needing_serialization.iter().cloned()); + + // Add necessary imports and registrations + match program { + Program::Module(module) => { + // All registrations are now inlined (no imports needed). + + // Add hoisted object property functions and registration calls at the end for step mode + if matches!(self.mode, TransformMode::Step) { + // Calculate insertion position once before any hoisting + let initial_insert_pos = module .body .iter() .position(|item| { !matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_))) }) .unwrap_or(0); + let mut current_insert_pos = initial_insert_pos; - let mut offset = 0; - let proxy_hoists: Vec<_> = - self.getter_workflow_proxy_hoists.drain(..).collect(); - for (var_name, step_id) in proxy_hoists { - let step_proxy = self.create_step_initializer(&step_id); - let var_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Var, - declare: false, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: Ident::new( - var_name.into(), - DUMMY_SP, - SyntaxContext::empty(), - ), - type_ann: None, - }), - init: Some(Box::new(step_proxy)), - definite: false, - }], - })))); - module.body.insert(insert_pos + offset, var_decl); - offset += 1; - } - } + // Process nested step functions FIRST (they typically appear earlier in source) + let nested_functions: Vec<_> = self.nested_step_functions.drain(..).collect(); - // Add static step method property assignments (workflow mode) - // These methods were stripped from the class and need to be assigned as properties - if matches!(self.mode, TransformMode::Workflow) { - for (class_name, method_name, step_id) in - self.static_step_methods_to_strip.drain(..) + for ( + fn_name, + mut fn_expr, + span, + closure_vars, + was_arrow, + parent_workflow_name, + references_lexical_this, + ) in nested_functions { - // Create: ClassName.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 { + // Generate hoisted name including parent workflow function name + let hoisted_name = if parent_workflow_name.is_empty() { + fn_name.clone() + } else { + format!("{}${}", parent_workflow_name, fn_name) + }; + // If there are closure variables, add destructuring as first statement + if !closure_vars.is_empty() { + if let Some(body) = &mut fn_expr.function.body { + // First, normalize the SyntaxContext of closure variable references in the body + // This ensures they match the identifiers we create in the destructuring pattern + ClosureVariableNormalizer::normalize_function_body( + &closure_vars, + body, + ); + + // Create destructuring statement using inline IIFE: + // const { var1, var2 } = (function() { ... })(); + let closure_destructure = + Stmt::Decl(Decl::Var(Box::new(VarDecl { span: DUMMY_SP, ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + kind: VarDeclKind::Const, + decls: vec![VarDeclarator { 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 { + name: Pat::Object(ObjectPat { span: DUMMY_SP, - value: "WORKFLOW_USE_STEP".into(), - raw: None, - }))), + props: closure_vars + .iter() + .map(|var_name| { + ObjectPatProp::Assign(AssignPatProp { + span: DUMMY_SP, + key: BindingIdent { + id: Ident::new( + var_name.clone().into(), + DUMMY_SP, + SyntaxContext::empty(), + ), + type_ann: None, + }, + value: None, + }) + }) + .collect(), + optional: false, + type_ann: None, + }), + init: Some(Box::new( + self.create_inline_get_closure_vars(), + )), + definite: false, }], - 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, - }); + declare: false, + }))); - let assignment = Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Assign(AssignExpr { + // Prepend to function body + body.stmts.insert(0, closure_destructure); + } + } + + // Create the appropriate hoisted declaration based on original function type. + // + // If the original arrow body referenced lexical `this`, we + // hoist as a regular `function` (not an arrow) so that the + // workflow runtime's `stepFn.apply(thisVal, args)` can + // rebind `this` to the value captured at call time. + let hoisted_decl = if was_arrow && !references_lexical_this { + // Convert back to arrow function: var name = async () => { ... }; + let arrow_expr = self.convert_fn_expr_to_arrow(&fn_expr); + ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { span: DUMMY_SP, - left: AssignTarget::Simple(SimpleAssignTarget::Member( - MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Ident(Ident::new( - class_name.into(), + ctxt: SyntaxContext::empty(), + kind: VarDeclKind::Var, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: Ident::new( + hoisted_name.clone().into(), DUMMY_SP, SyntaxContext::empty(), - ))), - 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 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(), + ), + type_ann: None, + }), + init: Some(Box::new(Expr::Arrow(arrow_expr))), + definite: false, + }], + declare: false, + })))) + } else { + // Keep as function declaration: async function name() { ... } + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { + ident: Ident::new( + hoisted_name.clone().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, - }); + ), + function: fn_expr.function, + declare: false, + }))) + }; - // 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::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, - right: Box::new(proxy_expr), - })), - }); - module.body.push(ModuleItem::Stmt(assignment)); + // Insert at current position and increment for next iteration + module.body.insert(current_insert_pos, hoisted_decl); + current_insert_pos += 1; + + // Create a registration call or stepId assignment with parent workflow name in the step ID + let step_fn_name = + self.record_nested_step_name(&fn_name, &parent_workflow_name); + let step_id = self.create_id(Some(&step_fn_name), span, false); + + // Insert inline IIFE registration right after the hoisted declaration + let registration_stmt = { + let fn_ref = Expr::Ident(Ident::new( + hoisted_name.clone().into(), + DUMMY_SP, + SyntaxContext::empty(), + )); + self.create_inline_step_registration(&step_id, fn_ref, &hoisted_name) + }; + module + .body + .insert(current_insert_pos, ModuleItem::Stmt(registration_stmt)); + current_insert_pos += 1; } - // Add instance getter step definitions (workflow mode) - // These getters were stripped from the class and need to be redefined via Object.defineProperty - let getter_strips: Vec<_> = - self.instance_getter_steps_to_strip.drain(..).collect(); - for (class_name, getter_name, step_id) in getter_strips { - // Sanitize names for use in JS identifier - let safe_getter = sanitize_ident_part(&getter_name); - let var_name = format!( - "__step_{}${}", - sanitize_ident_part(&class_name), - safe_getter - ); + // Then process object property step functions (they typically appear later) + // Collect hoisting information before the loop + let hoisting_info: Vec<_> = self + .object_property_step_functions + .iter() + .map( + |(parent_var, prop_name, fn_expr, _span, workflow_name, _was_arrow)| { + // Replace slashes with $ in parent_var to create valid JS identifier + let safe_parent_var = parent_var.replace('/', "$"); + let hoist_var_name = if !workflow_name.is_empty() { + format!("{}${}${}", workflow_name, safe_parent_var, prop_name) + } else { + format!("{}${}", safe_parent_var, prop_name) + }; + let wf_name = if workflow_name.is_empty() { + None + } else { + Some(workflow_name.as_str()) + }; + let step_id = self.create_object_property_id( + parent_var, prop_name, false, wf_name, + ); + (hoist_var_name, fn_expr.clone(), step_id, parent_var.clone()) + }, + ) + .collect(); - // Create: var __step_ClassName$getterName = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id") - let step_proxy = self.create_step_initializer(&step_id); - let var_decl = Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Var, - declare: false, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: Ident::new( - var_name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - ), - type_ann: None, - }), - init: Some(Box::new(step_proxy)), - definite: false, - }], - }))); - module.body.push(ModuleItem::Stmt(var_decl)); - - // Create: Object.defineProperty(ClassName.prototype, "getterName", { - // get() { return __step_ClassName$getterName.call(this); }, - // configurable: true, - // enumerable: false - // }) - let getter_body = BlockStmt { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - stmts: vec![Stmt::Return(ReturnStmt { + // Now drain and process + self.object_property_step_functions.drain(..); + + for (hoist_var_name, fn_expr, step_id, _parent_var) in hoisting_info { + // Create a var declaration for the hoisted function + // Using function expression (not arrow) to preserve `this` binding + let hoisted_decl = + ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { span: DUMMY_SP, - arg: Some(Box::new(Expr::Call(CallExpr { + ctxt: SyntaxContext::empty(), + kind: VarDeclKind::Var, + decls: vec![VarDeclarator { span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Ident(Ident::new( - var_name.into(), + name: Pat::Ident(BindingIdent { + id: Ident::new( + hoist_var_name.clone().into(), DUMMY_SP, SyntaxContext::empty(), - ))), - prop: MemberProp::Ident(IdentName::new( - "call".into(), - DUMMY_SP, - )), - }))), - args: vec![ExprOrSpread { - spread: None, - expr: Box::new(Expr::This(ThisExpr { span: DUMMY_SP })), - }], - type_args: None, - }))), - })], + ), + type_ann: None, + }), + init: Some(Box::new(Expr::Fn(fn_expr))), + definite: false, + }], + declare: false, + })))); + + // Insert at current position and increment for next iteration + module.body.insert(current_insert_pos, hoisted_decl); + current_insert_pos += 1; + + // Insert inline IIFE registration right after the hoisted declaration + let registration_stmt = { + let fn_ref = Expr::Ident(Ident::new( + hoist_var_name.clone().into(), + DUMMY_SP, + SyntaxContext::empty(), + )); + self.create_inline_step_registration(&step_id, fn_ref, &hoist_var_name) }; + module + .body + .insert(current_insert_pos, ModuleItem::Stmt(registration_stmt)); + current_insert_pos += 1; + } - let descriptor = Expr::Object(ObjectLit { - span: DUMMY_SP, - props: vec![ - // get() { return __step_var.call(this); } - PropOrSpread::Prop(Box::new(Prop::Method(MethodProp { - key: PropName::Ident(IdentName::new("get".into(), DUMMY_SP)), - function: Box::new(Function { - params: vec![], - decorators: vec![], - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - body: Some(getter_body), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - }), - }))), - // configurable: true - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName::new( - "configurable".into(), - DUMMY_SP, - )), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: true, - }))), - }))), - // enumerable: false - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName::new( - "enumerable".into(), - DUMMY_SP, - )), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: false, - }))), - }))), - ], - }); + // Add class registrations for class declarations. Class *expressions* + // register themselves inline (see `wrap_class_expr_with_registrations`) + // and have already been drained from these lists. - let define_property_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::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Ident(Ident::new( - "Object".into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - prop: MemberProp::Ident(IdentName::new( - "defineProperty".into(), - DUMMY_SP, - )), - }))), - args: vec![ - // ClassName.prototype - ExprOrSpread { - spread: None, - expr: 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, - )), - })), - }, - // "getterName" - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: getter_name.into(), - raw: None, - }))), - }, - // { get() { ... }, configurable: true, enumerable: false } - ExprOrSpread { - spread: None, - expr: Box::new(descriptor), - }, - ], - type_args: None, - })), - }); - module.body.push(ModuleItem::Stmt(define_property_call)); + // Static method step registrations (inline IIFE) + let static_step_regs: Vec<_> = + self.static_method_step_registrations.drain(..).collect(); + for (class_name, method_name, step_id, _span) in static_step_regs { + let stmt = self.build_static_step_registration( + &class_name, + &method_name, + &step_id, + ); + module.body.push(ModuleItem::Stmt(stmt)); } - // Add static getter step definitions (workflow mode) - // Same as instance getters but targets ClassName instead of ClassName.prototype - let static_getter_strips: Vec<_> = - self.static_getter_steps_to_strip.drain(..).collect(); - for (class_name, getter_name, step_id) in static_getter_strips { - let safe_getter = sanitize_ident_part(&getter_name); - let var_name = format!( - "__step_{}${}", - sanitize_ident_part(&class_name), - safe_getter + // Instance method step registrations: ClassName.prototype["methodName"] + let instance_step_regs: Vec<_> = + self.instance_method_step_registrations.drain(..).collect(); + for (class_name, method_name, step_id, _span) in instance_step_regs { + let stmt = self.build_instance_step_registration( + &class_name, + &method_name, + &step_id, + ); + module.body.push(ModuleItem::Stmt(stmt)); + } + + // Getter step registrations: + // Object.getOwnPropertyDescriptor(ClassName.prototype | ClassName, "getterName").get + let instance_getter_regs: Vec<_> = + self.instance_getter_step_registrations.drain(..).collect(); + for (class_name, getter_name, step_id, _span) in instance_getter_regs { + let stmt = self.build_getter_step_registration( + &class_name, + &getter_name, + &step_id, + false, + ); + module.body.push(ModuleItem::Stmt(stmt)); + } + let static_getter_regs: Vec<_> = + self.static_getter_step_registrations.drain(..).collect(); + for (class_name, getter_name, step_id, _span) in static_getter_regs { + let stmt = self.build_getter_step_registration( + &class_name, + &getter_name, + &step_id, + true, ); + module.body.push(ModuleItem::Stmt(stmt)); + } + + // Class serialization registrations (inline IIFE, no import needed). + // Sort for deterministic output ordering. + let mut sorted_classes: Vec<_> = + self.classes_needing_serialization.drain().collect(); + sorted_classes.sort(); + for class_name in sorted_classes { + let stmt = + self.create_class_serialization_registration(&class_name, &class_name); + module.body.push(ModuleItem::Stmt(stmt)); + } + } + + // Hoist getter workflow proxy vars for object literal getters (workflow mode) + // These must be inserted before the code that references them + if matches!(self.mode, TransformMode::Workflow) + && !self.getter_workflow_proxy_hoists.is_empty() + { + let insert_pos = module + .body + .iter() + .position(|item| { + !matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_))) + }) + .unwrap_or(0); + let mut offset = 0; + let proxy_hoists: Vec<_> = + self.getter_workflow_proxy_hoists.drain(..).collect(); + for (var_name, step_id) in proxy_hoists { let step_proxy = self.create_step_initializer(&step_id); - let var_decl = Stmt::Decl(Decl::Var(Box::new(VarDecl { + let var_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { span: DUMMY_SP, ctxt: SyntaxContext::empty(), kind: VarDeclKind::Var, @@ -5947,7 +6038,7 @@ impl VisitMut for StepTransform { span: DUMMY_SP, name: Pat::Ident(BindingIdent { id: Ident::new( - var_name.clone().into(), + var_name.into(), DUMMY_SP, SyntaxContext::empty(), ), @@ -5956,262 +6047,102 @@ impl VisitMut for StepTransform { init: Some(Box::new(step_proxy)), definite: false, }], - }))); - module.body.push(ModuleItem::Stmt(var_decl)); + })))); + module.body.insert(insert_pos + offset, var_decl); + offset += 1; + } + } - // Object.defineProperty(ClassName, "getterName", { get() { return __step_var(); }, ... }) - // Note: static getters don't need .call(this), just invoke directly - let getter_body = BlockStmt { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - stmts: vec![Stmt::Return(ReturnStmt { - span: DUMMY_SP, - arg: Some(Box::new(Expr::Call(CallExpr { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Ident(Ident::new( - var_name.into(), - DUMMY_SP, - SyntaxContext::empty(), - )))), - args: vec![], - type_args: None, - }))), - })], - }; + // Workflow mode: the "use step" methods of class declarations were + // stripped from the class body; reattach them as step proxies. Class + // expressions did this inline (see `wrap_class_expr_with_registrations`). + if matches!(self.mode, TransformMode::Workflow) { + let static_strips: Vec<_> = + self.static_step_methods_to_strip.drain(..).collect(); + for (class_name, method_name, step_id) in static_strips { + // ClassName.methodName = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id") + let stmt = self.build_step_proxy_assignment( + &class_name, + &method_name, + &step_id, + true, + ); + module.body.push(ModuleItem::Stmt(stmt)); + } - let descriptor = Expr::Object(ObjectLit { - span: DUMMY_SP, - props: vec![ - PropOrSpread::Prop(Box::new(Prop::Method(MethodProp { - key: PropName::Ident(IdentName::new("get".into(), DUMMY_SP)), - function: Box::new(Function { - params: vec![], - decorators: vec![], - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - body: Some(getter_body), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - }), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName::new( - "configurable".into(), - DUMMY_SP, - )), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: true, - }))), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(IdentName::new( - "enumerable".into(), - DUMMY_SP, - )), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: DUMMY_SP, - value: false, - }))), - }))), - ], - }); + let instance_strips: Vec<_> = + self.instance_step_methods_to_strip.drain(..).collect(); + for (class_name, method_name, step_id) in instance_strips { + // ClassName.prototype["methodName"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id") + let stmt = self.build_step_proxy_assignment( + &class_name, + &method_name, + &step_id, + false, + ); + module.body.push(ModuleItem::Stmt(stmt)); + } - let define_property_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::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Ident(Ident::new( - "Object".into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - prop: MemberProp::Ident(IdentName::new( - "defineProperty".into(), - DUMMY_SP, - )), - }))), - args: vec![ - // ClassName (not .prototype for static) - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Ident(Ident::new( - class_name.into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - }, - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: getter_name.into(), - raw: None, - }))), - }, - ExprOrSpread { - spread: None, - expr: Box::new(descriptor), - }, - ], - type_args: None, - })), - }); - module.body.push(ModuleItem::Stmt(define_property_call)); + // Stripped getter steps are redefined via Object.defineProperty + let getter_strips: Vec<_> = + self.instance_getter_steps_to_strip.drain(..).collect(); + for (class_name, getter_name, step_id) in getter_strips { + let stmts = self.build_getter_step_definition( + &class_name, + &class_name, + &getter_name, + &step_id, + false, + ); + module.body.extend(stmts.into_iter().map(ModuleItem::Stmt)); + } + let static_getter_strips: Vec<_> = + self.static_getter_steps_to_strip.drain(..).collect(); + for (class_name, getter_name, step_id) in static_getter_strips { + let stmts = self.build_getter_step_definition( + &class_name, + &class_name, + &getter_name, + &step_id, + true, + ); + module.body.extend(stmts.into_iter().map(ModuleItem::Stmt)); } - // 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 - // Sort for deterministic output ordering + // Class serialization registrations (same as step mode). + // Sort for deterministic output ordering. let mut sorted_classes: Vec<_> = self.classes_needing_serialization.drain().collect(); sorted_classes.sort(); for class_name in sorted_classes { - let registration_call = - self.create_class_serialization_registration(&class_name); - module.body.push(ModuleItem::Stmt(registration_call)); + let stmt = + self.create_class_serialization_registration(&class_name, &class_name); + module.body.push(ModuleItem::Stmt(stmt)); } } - // Add static method workflow registrations (workflowId and __private_workflows.set) - if matches!(self.mode, TransformMode::Workflow) { - for (class_name, method_name, workflow_id, _span) in - self.static_method_workflow_registrations.drain(..) - { - // Add ClassName.methodName.workflowId = "workflow_id" - let workflow_id_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.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - prop: MemberProp::Ident(IdentName::new( - method_name.clone().into(), - DUMMY_SP, - )), - })), - prop: MemberProp::Ident(IdentName::new( - "workflowId".into(), - DUMMY_SP, - )), - }, - )), - op: AssignOp::Assign, - right: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: workflow_id.clone().into(), - raw: None, - }))), - })), - }); - module.body.push(ModuleItem::Stmt(workflow_id_assignment)); - - // Add globalThis.__private_workflows.set("workflow_id", ClassName.methodName) - let workflows_set_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::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(Expr::Ident(Ident::new( - "globalThis".into(), - DUMMY_SP, - SyntaxContext::empty(), - ))), - prop: MemberProp::Ident(IdentName::new( - "__private_workflows".into(), - DUMMY_SP, - )), - })), - prop: MemberProp::Ident(IdentName::new("set".into(), DUMMY_SP)), - }))), - args: vec![ - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: workflow_id.into(), - raw: None, - }))), - }, - ExprOrSpread { - spread: None, - expr: 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( - method_name.into(), - DUMMY_SP, - )), - })), - }, - ], - type_args: None, - })), - }); - module.body.push(ModuleItem::Stmt(workflows_set_call)); - } - } else if matches!(self.mode, TransformMode::Step) { - // For step mode, just add the workflowId assignment - for (class_name, method_name, workflow_id, _span) in - self.static_method_workflow_registrations.drain(..) - { - let workflow_id_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( - method_name.into(), - DUMMY_SP, - )), - })), - prop: MemberProp::Ident(IdentName::new( - "workflowId".into(), - DUMMY_SP, - )), - }, - )), - op: AssignOp::Assign, - right: Box::new(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: workflow_id.into(), - raw: None, - }))), - })), - }); - module.body.push(ModuleItem::Stmt(workflow_id_assignment)); + // Static method workflow registrations: `ClassName.method.workflowId = "..."` + // in both modes, plus `globalThis.__private_workflows.set(...)` in workflow mode. + if !matches!(self.mode, TransformMode::Detect) { + let workflow_regs: Vec<_> = self + .static_method_workflow_registrations + .drain(..) + .collect(); + for (class_name, method_name, workflow_id, _span) in workflow_regs { + let stmt = self.build_static_workflow_id_assignment( + &class_name, + &method_name, + &workflow_id, + ); + module.body.push(ModuleItem::Stmt(stmt)); + if matches!(self.mode, TransformMode::Workflow) { + let stmt = self.build_static_workflow_registration( + &class_name, + &method_name, + &workflow_id, + ); + module.body.push(ModuleItem::Stmt(stmt)); + } } } @@ -6270,8 +6201,8 @@ impl VisitMut for StepTransform { self.classes_needing_serialization.drain().collect(); sorted_classes.sort(); for class_name in sorted_classes { - let registration_call = - self.create_class_serialization_registration(&class_name); + let registration_call = self + .create_class_serialization_registration(&class_name, &class_name); module_items.push(ModuleItem::Stmt(registration_call)); } } @@ -7889,8 +7820,7 @@ impl VisitMut for StepTransform { let old_in_workflow = self.in_workflow_function; let old_workflow_name = self.current_workflow_function_name.clone(); - let old_parent_name = - self.current_parent_function_name.clone(); + let old_parent_name = self.current_parent_function_name.clone(); let old_in_module = self.in_module_level; self.in_workflow_function = true; self.current_workflow_function_name = Some(name.clone()); @@ -8158,8 +8088,7 @@ impl VisitMut for StepTransform { let old_in_workflow = self.in_workflow_function; let old_workflow_name = self.current_workflow_function_name.clone(); - let old_parent_name = - self.current_parent_function_name.clone(); + let old_parent_name = self.current_parent_function_name.clone(); let old_in_module = self.in_module_level; self.in_workflow_function = true; self.current_workflow_function_name = Some(name.clone()); @@ -8247,22 +8176,32 @@ impl VisitMut for StepTransform { } } } - Expr::Class(_) => { - // Track the binding name for class expressions like: - // var Bash = class _Bash {} - // The binding name (Bash) is what's accessible at module scope, - // not the internal class name (_Bash) - // We set the binding name here; it will be used when visit_mut_class_expr - // is called during visit_mut_children_with below - self.current_class_binding_name = Some(name.clone()); - } _ => {} } } } } - var_decl.visit_mut_children_with(self); + // Visit each declarator individually so that class expressions pick up + // the binding they are assigned to, e.g. `Bash` in + // `var Bash = class _Bash {}`. The binding name is what is accessible + // at module scope (the internal `_Bash` is only in scope inside the + // class body), so it is what generated registration code must use. + // The name is set per declarator: with `var A = class {}, B = class {}` + // each class must resolve to its own binding. + for decl in var_decl.decls.iter_mut() { + let class_binding = match (&decl.name, decl.init.as_deref()) { + (Pat::Ident(binding), Some(init)) + if Self::class_expr_of_initializer(init).is_some() => + { + Some(binding.id.sym.to_string()) + } + _ => None, + }; + self.current_class_binding_name = class_binding; + decl.visit_mut_with(self); + self.current_class_binding_name = None; + } } // Handle JSX attributes with function values @@ -8295,6 +8234,19 @@ impl VisitMut for StepTransform { }); } } + // A class expression as a property value (`{ Job: class {} }`) + // takes the property key as its name. The name is only used + // for IDs; see `current_class_binding_from_key`. + Prop::KeyValue(kv) if Self::class_expr_of_initializer(&kv.value).is_some() => { + if let Some(name) = Self::prop_name_string(&kv.key) { + self.current_class_binding_name = Some(name); + self.current_class_binding_from_key = true; + prop.visit_mut_children_with(self); + self.current_class_binding_name = None; + self.current_class_binding_from_key = false; + return; + } + } // Note: Prop::Getter validation is handled in process_object_properties_for_step_functions // to avoid emitting duplicate errors when the visitor recurses into the same node. _ => {} @@ -8310,12 +8262,28 @@ impl VisitMut for StepTransform { fn visit_mut_class_decl(&mut self, class_decl: &mut ClassDecl) { let class_name = class_decl.ident.sym.to_string(); let old_class_name = self.current_class_name.take(); - self.current_class_name = Some(class_name.clone()); + let old_unreferenceable = self.current_class_unreferenceable.take(); - // Check if class has custom serialization methods (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) - if self.has_custom_serialization_methods(&class_decl.class) { - self.classes_needing_serialization - .insert(class_name.clone()); + if self.in_module_level { + self.current_class_name = Some(class_name.clone()); + + // Check if class has custom serialization methods (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) + if self.has_custom_serialization_methods(&class_decl.class) { + self.classes_needing_serialization + .insert(class_name.clone()); + } + } else { + // A class declared inside a function is not in scope at module + // level, where registrations are emitted. Leave `current_class_name` + // unset so step methods / serialization inside it are reported + // instead of generating code that throws a ReferenceError. + self.current_class_unreferenceable = Some(UnreferenceableClass::Nested); + if self.has_custom_serialization_methods(&class_decl.class) { + self.report_unreferenceable_class( + class_decl.class.span, + "custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE)", + ); + } } // Visit the class body (this populates static_step_methods_to_strip) @@ -8396,91 +8364,107 @@ impl VisitMut for StepTransform { // Restore previous class name self.current_class_name = old_class_name; + self.current_class_unreferenceable = old_unreferenceable; } // Handle class expressions to track class name for static methods fn visit_mut_class_expr(&mut self, class_expr: &mut ClassExpr) { - // Get the binding name set by visit_mut_var_decl (e.g., "Foo" from `var Foo = class { ... }`) + // Get the binding name set by visit_mut_var_decl / visit_mut_assign_expr / + // visit_mut_prop_or_spread (e.g., "Foo" from `var Foo = class { ... }`) let binding_name = self.current_class_binding_name.take(); - - // Get the internal class expression name (e.g. `_Foo` from `class _Foo { ... }`) - let expr_ident_name = class_expr - .ident - .as_ref() - .map(|i| i.sym.to_string()) - .unwrap_or_else(|| "AnonymousClass".to_string()); + let binding_from_key = std::mem::replace(&mut self.current_class_binding_from_key, false); // Compute the tracked class name: prefer the binding name (e.g. `Foo` // from `var Foo = class _Foo {}`) over the internal class expression - // name (`_Foo`). The internal name is only scoped inside the class body - // and is not accessible at module level, so all generated code emitted - // outside the class — method step registrations, class serialization - // IIFEs, and method-stripping filters — must use the binding name. - // Without this, generated code like - // `registerStepFunction("...", _Foo.prototype["method"])` would - // produce a ReferenceError at runtime. - let tracked_class_name = binding_name - .clone() - .unwrap_or_else(|| expr_ident_name.clone()); - + // name (`_Foo`). The name is used to derive step and class IDs, and to + // match the registrations recorded while visiting the body. + // + // Generated registration code never references a class expression by + // name: `visit_mut_expr` wraps the expression in an IIFE that receives + // the class as an argument (see `wrap_class_expr_with_registrations`), + // so the class does not need a module-scope binding at all. What it + // does need is a *name* for its IDs. When none can be derived (an + // anonymous class expression in a position with no key or binding, + // e.g. `foo(class { ... })`), or when the class is nested inside a + // function (its registration would only run when that function runs, + // not at module load), record the problem so that any step method or + // custom serialization found in the body is reported as a compile + // error instead of silently producing a broken registration. let old_class_name = self.current_class_name.take(); - self.current_class_name = Some(tracked_class_name.clone()); + let old_unreferenceable = self.current_class_unreferenceable.take(); + + let tracked_class_name = + match self.resolve_class_expr_name(class_expr, binding_name.clone()) { + Ok(name) => { + self.current_class_name = Some(name.clone()); + Some(name) + } + Err(reason) => { + self.current_class_unreferenceable = Some(reason); + None + } + }; // Check if class has custom serialization methods (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) let has_serde = self.has_custom_serialization_methods(&class_expr.class); if has_serde { - self.classes_needing_serialization - .insert(tracked_class_name.clone()); - } - - // esbuild emits anonymous class expressions for classes that don't - // self-reference (e.g. `var Foo = class { ... }`). Downstream bundlers - // (like Nitro's Rollup bundler) rely on the class expression name for - // serialization class registration. Without a name, the class `.name` - // property is empty and lookups can fail at runtime. Re-insert the - // binding name so the output becomes `var Foo = class Foo { ... }` — - // behaviorally equivalent for typical class usage and preserves the - // identifier through subsequent bundling passes. - if has_serde && class_expr.ident.is_none() { - if let Some(ref name) = binding_name { - class_expr.ident = Some(Ident::new( - name.clone().into(), - DUMMY_SP, - SyntaxContext::empty(), - )); + match &tracked_class_name { + Some(name) => { + self.classes_needing_serialization.insert(name.clone()); + } + None => { + self.report_unreferenceable_class( + class_expr.class.span, + "custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE)", + ); + } } } + // When the class expression is anonymous and its name comes from the + // variable it is assigned to, the name is inserted as the class's own + // identifier if the expression ends up wrapped in a registration IIFE + // (`var Foo = class {}` -> `var Foo = (...)(class Foo {})`), so that + // `.name` survives the wrapping. Names derived from property keys are + // not inserted: `exports.Foo = class { m() { Foo } }` may refer to an + // unrelated outer `Foo`, which a class-scoped `Foo` would shadow. + let ident_to_insert = match (&binding_name, class_expr.ident.is_none(), binding_from_key) { + (Some(name), true, false) => Some(name.clone()), + _ => None, + }; + // Visit the class body (this populates static_step_methods_to_strip) class_expr.class.visit_mut_with(self); // In workflow mode, remove static and instance step methods from the class body - if matches!(self.mode, TransformMode::Workflow) { + if let (TransformMode::Workflow, Some(tracked_class_name)) = + (&self.mode, &tracked_class_name) + { let static_methods_to_strip: Vec<_> = self .static_step_methods_to_strip .iter() - .filter(|(cn, _, _)| cn == &tracked_class_name) + .filter(|(cn, _, _)| cn == tracked_class_name) .map(|(_, mn, _)| mn.clone()) .collect(); let instance_methods_to_strip: Vec<_> = self .instance_step_methods_to_strip .iter() - .filter(|(cn, _, _)| cn == &tracked_class_name) + .filter(|(cn, _, _)| cn == tracked_class_name) .map(|(_, mn, _)| mn.clone()) .collect(); let instance_getters_to_strip: Vec<_> = self .instance_getter_steps_to_strip .iter() - .filter(|(cn, _, _)| cn == &tracked_class_name) + .filter(|(cn, _, _)| cn == tracked_class_name) .map(|(_, gn, _)| gn.clone()) .collect(); let static_getters_to_strip: Vec<_> = self .static_getter_steps_to_strip .iter() - .filter(|(cn, _, _)| cn == &tracked_class_name) + .filter(|(cn, _, _)| cn == tracked_class_name) .map(|(_, gn, _)| gn.clone()) .collect(); @@ -8517,8 +8501,17 @@ impl VisitMut for StepTransform { } } + // Hand off to `visit_mut_expr`, which owns the enclosing `Expr` and can + // replace it with the registration IIFE. + self.pending_class_expr_registration = tracked_class_name.map(|name| PendingClassExpr { + name, + ident_to_insert, + has_custom_serialization: has_serde, + }); + // Restore previous class name self.current_class_name = old_class_name; + self.current_class_unreferenceable = old_unreferenceable; } // Handle class methods @@ -8556,6 +8549,10 @@ impl VisitMut for StepTransform { let class_name = match &self.current_class_name { Some(name) => name.clone(), None => { + // The enclosing class cannot be referenced from module + // level (anonymous or nested), so the getter cannot be + // registered. Report it and leave the getter untouched. + self.report_unreferenceable_class(method.span, "\"use step\" getters"); method.visit_mut_children_with(self); return; } @@ -8660,7 +8657,10 @@ impl VisitMut for StepTransform { let class_name = match &self.current_class_name { Some(name) => name.clone(), None => { - // No class context - shouldn't happen, but fall back + // The enclosing class cannot be referenced from module + // level (anonymous or nested), so the method cannot be + // registered. Report it and leave the method untouched. + self.report_unreferenceable_class(method.span, "\"use step\" methods"); method.visit_mut_children_with(self); return; } @@ -8754,7 +8754,17 @@ impl VisitMut for StepTransform { let class_name = match &self.current_class_name { Some(name) => name.clone(), None => { - // No class context - shouldn't happen, but fall back + // The enclosing class cannot be referenced from module + // level (anonymous or nested), so the method cannot be + // registered. Report it and leave the method untouched. + self.report_unreferenceable_class( + method.span, + if has_workflow { + "\"use workflow\" methods" + } else { + "\"use step\" methods" + }, + ); method.visit_mut_children_with(self); return; } @@ -8890,8 +8900,37 @@ impl VisitMut for StepTransform { // Handle assignment expressions fn visit_mut_assign_expr(&mut self, assign: &mut AssignExpr) { - // Track function names from assignments like `foo = async () => {}` + // A class expression assigned to a plain identifier + // (`Foo = class { ... }`) is referenceable through that identifier, so + // use it as the class name unless an enclosing variable declarator + // already provided one (`var Foo = Bar = class {}` keeps `Foo`). + // + // A class assigned to a property (`exports.Foo = class {}`, + // `module.exports.Foo = class {}`) takes the property name. That name + // is only used for IDs; see `current_class_binding_from_key`. + let had_pending_binding = self.current_class_binding_name.is_some(); + if !had_pending_binding + && assign.op == AssignOp::Assign + && Self::class_expr_of_initializer(&assign.right).is_some() + { + match &assign.left { + AssignTarget::Simple(SimpleAssignTarget::Ident(binding)) => { + self.current_class_binding_name = Some(binding.id.sym.to_string()); + } + AssignTarget::Simple(SimpleAssignTarget::Member(member)) => { + if let Some(name) = Self::member_prop_name(&member.prop) { + self.current_class_binding_name = Some(name); + self.current_class_binding_from_key = true; + } + } + _ => {} + } + } assign.visit_mut_children_with(self); + if !had_pending_binding { + self.current_class_binding_name = None; + self.current_class_binding_from_key = false; + } } // Override visit_mut_expr to track closure variables and handle step functions @@ -8910,6 +8949,19 @@ impl VisitMut for StepTransform { // Handle step functions that appear in expressions (e.g., return statements) // but are not in var declarators (those are handled in visit_mut_var_decl) match expr { + Expr::Class(_) => { + // Visit the class (runs `visit_mut_class_expr`, which records + // the class's registrations and hands back its resolved name), + // then wrap the expression in the registration IIFE. Clearing + // first guards against a stale hand-off from a class that was + // visited through a path that does not go through here. + self.pending_class_expr_registration = None; + expr.visit_mut_children_with(self); + if let Some(pending) = self.pending_class_expr_registration.take() { + self.wrap_class_expr_with_registrations(expr, pending); + } + return; + } Expr::Fn(fn_expr) => { if self.has_step_directive(&fn_expr.function, false) { if !self.in_module_level { @@ -9299,6 +9351,24 @@ impl VisitMut for StepTransform { // Visit the class body so serde/step transforms run decl.visit_mut_children_with(self); + // `DefaultDecl::Class` holds a `ClassExpr` directly rather than + // an `Expr`, so the registration IIFE wrapping done by + // `visit_mut_expr` does not apply here. The class is instead + // rewritten to a `const` (below) and registered at module + // level by name, so consume the hand-off. Only the identifier + // insertion carries over, and only for classes with custom + // serialization: downstream bundlers (e.g. Nitro's Rollup) rely + // on the class expression name for serialization lookups. + if let Some(pending) = self.pending_class_expr_registration.take() { + if let (DefaultDecl::Class(class_expr), Some(name)) = + (&mut decl.decl, pending.ident_to_insert) + { + if class_expr.ident.is_none() && pending.has_custom_serialization { + class_expr.ident = Some(Self::ident(&name)); + } + } + } + // After visiting, defer the rewrite for anonymous classes if let Some(const_name) = saved_const_name { if let DefaultDecl::Class(class_expr) = &decl.decl { diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js new file mode 100644 index 0000000000..89da57fa9b --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js @@ -0,0 +1,69 @@ +// Anonymous class expressions in positions that provide no name (not assigned +// to a variable or property) have nothing to derive a step/class ID from. The +// compiler used to emit `AnonymousClass.prototype[...]`, which is a guaranteed +// ReferenceError at module evaluation (vercel/workflow#3929). It must instead +// fail at compile time. +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; + +// Error: class passed directly as an argument +registerPlugin(class { + async run() { + 'use step'; + return 'plugin'; + } +}); + +// Error: class as an array element +export const handlers = [ + class { + static async execute() { + 'use step'; + return 'job'; + } + }, +]; + +// Error: class chosen by a conditional +export const Worker = process.env.FAST + ? class { + get status() { + 'use step'; + return 'ok'; + } + } + : null; + +// Error: custom serialization without a derivable name +const registry = new Map([ + ['point', class { + static [WORKFLOW_SERIALIZE](inst) { + return { x: inst.x }; + } + static [WORKFLOW_DESERIALIZE](data) { + return { x: data.x }; + } + }], +]); + +// Error: static "use workflow" method +useModel(class { + static async orchestrate() { + 'use workflow'; + return 'done'; + } +}); + +// OK: anonymous class expression without steps or serialization +export const plain = class { + greet() { + return 'hi'; + } +}; + +// OK: naming the class is all that is needed +registerPlugin(class NamedPlugin { + async run() { + 'use step'; + return 'named'; + } +}); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js new file mode 100644 index 0000000000..0a5b458d5c --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js @@ -0,0 +1,66 @@ +// Anonymous class expressions in positions that provide no name (not assigned +// to a variable or property) have nothing to derive a step/class ID from. The +// compiler used to emit `AnonymousClass.prototype[...]`, which is a guaranteed +// ReferenceError at module evaluation (vercel/workflow#3929). It must instead +// fail at compile time. +/**__internal_workflows{"steps":{"input.js":{"NamedPlugin#run":{"stepId":"step//./input//NamedPlugin#run"}}},"classes":{"input.js":{"NamedPlugin":{"classId":"class//./input//NamedPlugin"}}}}*/; +// Error: class passed directly as an argument +registerPlugin(class { + async run() { + 'use step'; + return 'plugin'; + } +}); +// Error: class as an array element +export const handlers = [ + class { + static async execute() { + 'use step'; + return 'job'; + } + } +]; +// Error: class chosen by a conditional +export const Worker = process.env.FAST ? class { + get status() { + 'use step'; + return 'ok'; + } +} : null; +// Error: static "use workflow" method +useModel(class { + static async orchestrate() { + 'use workflow'; + return 'done'; + } +}); +// OK: anonymous class expression without steps or serialization +export const plain = class { + greet() { + return 'hi'; + } +}; +// OK: naming the class is all that is needed +registerPlugin(function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//NamedPlugin#run", __wf_fn); + __wf_fn.stepId = "step//./input//NamedPlugin#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//NamedPlugin", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class NamedPlugin { + async run() { + return 'named'; + } +})); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr new file mode 100644 index 0000000000..80fb8c40f4 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr @@ -0,0 +1,54 @@ + x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can + | reference it when registering it at module level + ,-[input.js:10:1] + 9 | registerPlugin(class { + 10 | ,-> async run() { + 11 | | 'use step'; + 12 | | return 'plugin'; + 13 | `-> } + 14 | }); + `---- + x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can + | reference it when registering it at module level + ,-[input.js:19:1] + 18 | class { + 19 | ,-> static async execute() { + 20 | | 'use step'; + 21 | | return 'job'; + 22 | `-> } + 23 | }, + `---- + x Anonymous class expressions cannot use "use step" getters. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can + | reference it when registering it at module level + ,-[input.js:29:1] + 28 | ? class { + 29 | ,-> get status() { + 30 | | 'use step'; + 31 | | return 'ok'; + 32 | `-> } + 33 | } + `---- + x Anonymous class expressions cannot use custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE). Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name + | (`class MyClass { ... }`) so the compiler can reference it when registering it at module level + ,-[input.js:38:1] + 37 | const registry = new Map([ + 38 | ,-> ['point', class { + 39 | | static [WORKFLOW_SERIALIZE](inst) { + 40 | | return { x: inst.x }; + 41 | | } + 42 | | static [WORKFLOW_DESERIALIZE](data) { + 43 | | return { x: data.x }; + 44 | | } + 45 | `-> }], + 46 | ]); + `---- + x Anonymous class expressions cannot use "use workflow" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler + | can reference it when registering it at module level + ,-[input.js:50:1] + 49 | useModel(class { + 50 | ,-> static async orchestrate() { + 51 | | 'use workflow'; + 52 | | return 'done'; + 53 | `-> } + 54 | }); + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js new file mode 100644 index 0000000000..72666f7233 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js @@ -0,0 +1,56 @@ +// Anonymous class expressions in positions that provide no name (not assigned +// to a variable or property) have nothing to derive a step/class ID from. The +// compiler used to emit `AnonymousClass.prototype[...]`, which is a guaranteed +// ReferenceError at module evaluation (vercel/workflow#3929). It must instead +// fail at compile time. +/**__internal_workflows{"steps":{"input.js":{"NamedPlugin#run":{"stepId":"step//./input//NamedPlugin#run"}}},"classes":{"input.js":{"NamedPlugin":{"classId":"class//./input//NamedPlugin"}}}}*/; +// Error: class passed directly as an argument +registerPlugin(class { + async run() { + 'use step'; + return 'plugin'; + } +}); +// Error: class as an array element +export const handlers = [ + class { + static async execute() { + 'use step'; + return 'job'; + } + } +]; +// Error: class chosen by a conditional +export const Worker = process.env.FAST ? class { + get status() { + 'use step'; + return 'ok'; + } +} : null; +// Error: static "use workflow" method +useModel(class { + static async orchestrate() { + 'use workflow'; + return 'done'; + } +}); +// OK: anonymous class expression without steps or serialization +export const plain = class { + greet() { + return 'hi'; + } +}; +// OK: naming the class is all that is needed +registerPlugin(function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//NamedPlugin#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//NamedPlugin", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class NamedPlugin { +})); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr new file mode 100644 index 0000000000..80fb8c40f4 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr @@ -0,0 +1,54 @@ + x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can + | reference it when registering it at module level + ,-[input.js:10:1] + 9 | registerPlugin(class { + 10 | ,-> async run() { + 11 | | 'use step'; + 12 | | return 'plugin'; + 13 | `-> } + 14 | }); + `---- + x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can + | reference it when registering it at module level + ,-[input.js:19:1] + 18 | class { + 19 | ,-> static async execute() { + 20 | | 'use step'; + 21 | | return 'job'; + 22 | `-> } + 23 | }, + `---- + x Anonymous class expressions cannot use "use step" getters. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can + | reference it when registering it at module level + ,-[input.js:29:1] + 28 | ? class { + 29 | ,-> get status() { + 30 | | 'use step'; + 31 | | return 'ok'; + 32 | `-> } + 33 | } + `---- + x Anonymous class expressions cannot use custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE). Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name + | (`class MyClass { ... }`) so the compiler can reference it when registering it at module level + ,-[input.js:38:1] + 37 | const registry = new Map([ + 38 | ,-> ['point', class { + 39 | | static [WORKFLOW_SERIALIZE](inst) { + 40 | | return { x: inst.x }; + 41 | | } + 42 | | static [WORKFLOW_DESERIALIZE](data) { + 43 | | return { x: data.x }; + 44 | | } + 45 | `-> }], + 46 | ]); + `---- + x Anonymous class expressions cannot use "use workflow" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler + | can reference it when registering it at module level + ,-[input.js:50:1] + 49 | useModel(class { + 50 | ,-> static async orchestrate() { + 51 | | 'use workflow'; + 52 | | return 'done'; + 53 | `-> } + 54 | }); + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/input.js b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/input.js new file mode 100644 index 0000000000..12ce23dfed --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/input.js @@ -0,0 +1,59 @@ +// Step method and serialization registrations are emitted at module level, so +// a class declared inside a function cannot be referenced by them. The +// compiler used to emit `Inner.prototype[...]` at module scope, which throws a +// ReferenceError at module evaluation. It must instead fail at compile time. +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; + +// Error: class declaration inside a function +export function makeService() { + class Service { + async fetch() { + 'use step'; + return 'data'; + } + } + return new Service(); +} + +// Error: class expression assigned inside a function (this is also the shape +// esbuild produces when it wraps a module in a lazy `__esm` initializer) +var Lazy; +export function init() { + Lazy = class { + static async load() { + 'use step'; + return 'lazy'; + } + }; +} + +// Error: custom serialization on a nested class +export const factory = () => { + const Point = class { + static [WORKFLOW_SERIALIZE](inst) { + return { x: inst.x }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Point(data.x); + } + }; + return Point; +}; + +// OK: a nested class without steps or serialization +export function helper() { + class Local { + value() { + return 1; + } + } + return new Local(); +} + +// OK: module-level class declaration +export class Top { + async run() { + 'use step'; + return 'top'; + } +} diff --git a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.js new file mode 100644 index 0000000000..211e62be85 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.js @@ -0,0 +1,75 @@ +// Step method and serialization registrations are emitted at module level, so +// a class declared inside a function cannot be referenced by them. The +// compiler used to emit `Inner.prototype[...]` at module scope, which throws a +// ReferenceError at module evaluation. It must instead fail at compile time. +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; +/**__internal_workflows{"steps":{"input.js":{"Top#run":{"stepId":"step//./input//Top#run"}}},"classes":{"input.js":{"Top":{"classId":"class//./input//Top"}}}}*/; +// Error: class declaration inside a function +export function makeService() { + class Service { + async fetch() { + 'use step'; + return 'data'; + } + } + return new Service(); +} +// Error: class expression assigned inside a function (this is also the shape +// esbuild produces when it wraps a module in a lazy `__esm` initializer) +var Lazy; +export function init() { + Lazy = class { + static async load() { + 'use step'; + return 'lazy'; + } + }; +} +// Error: custom serialization on a nested class +export const factory = ()=>{ + const Point = class { + static [WORKFLOW_SERIALIZE](inst) { + return { + x: inst.x + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Point(data.x); + } + }; + return Point; +}; +// OK: a nested class without steps or serialization +export function helper() { + class Local { + value() { + return 1; + } + } + return new Local(); +} +// OK: module-level class declaration +export class Top { + async run() { + return 'top'; + } +} +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); +})(Top.prototype["run"], "step//./input//Top#run"); +(function(__wf_cls, __wf_id) { + var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: __wf_id, + writable: false, + enumerable: false, + configurable: false + }); +})(Top, "class//./input//Top"); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr new file mode 100644 index 0000000000..c09dd5d195 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr @@ -0,0 +1,34 @@ + x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in + | an inner scope + ,-[input.js:10:1] + 9 | class Service { + 10 | ,-> async fetch() { + 11 | | 'use step'; + 12 | | return 'data'; + 13 | `-> } + 14 | } + `---- + x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in + | an inner scope + ,-[input.js:23:1] + 22 | Lazy = class { + 23 | ,-> static async load() { + 24 | | 'use step'; + 25 | | return 'lazy'; + 26 | `-> } + 27 | }; + `---- + x Classes using custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) must be declared at the top level of the module, not inside a function. The compiler registers the class at module + | level and cannot reference a class declared in an inner scope + ,-[input.js:32:1] + 31 | export const factory = () => { + 32 | ,-> const Point = class { + 33 | | static [WORKFLOW_SERIALIZE](inst) { + 34 | | return { x: inst.x }; + 35 | | } + 36 | | static [WORKFLOW_DESERIALIZE](data) { + 37 | | return new Point(data.x); + 38 | | } + 39 | `-> }; + 40 | return Point; + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.js new file mode 100644 index 0000000000..724e5879bc --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.js @@ -0,0 +1,64 @@ +// Step method and serialization registrations are emitted at module level, so +// a class declared inside a function cannot be referenced by them. The +// compiler used to emit `Inner.prototype[...]` at module scope, which throws a +// ReferenceError at module evaluation. It must instead fail at compile time. +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; +/**__internal_workflows{"steps":{"input.js":{"Top#run":{"stepId":"step//./input//Top#run"}}},"classes":{"input.js":{"Top":{"classId":"class//./input//Top"}}}}*/; +// Error: class declaration inside a function +export function makeService() { + class Service { + async fetch() { + 'use step'; + return 'data'; + } + } + return new Service(); +} +// Error: class expression assigned inside a function (this is also the shape +// esbuild produces when it wraps a module in a lazy `__esm` initializer) +var Lazy; +export function init() { + Lazy = class { + static async load() { + 'use step'; + return 'lazy'; + } + }; +} +// Error: custom serialization on a nested class +export const factory = ()=>{ + const Point = class { + static [WORKFLOW_SERIALIZE](inst) { + return { + x: inst.x + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Point(data.x); + } + }; + return Point; +}; +// OK: a nested class without steps or serialization +export function helper() { + class Local { + value() { + return 1; + } + } + return new Local(); +} +// OK: module-level class declaration +export class Top { +} +Top.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Top#run"); +(function(__wf_cls, __wf_id) { + var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: __wf_id, + writable: false, + enumerable: false, + configurable: false + }); +})(Top, "class//./input//Top"); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr new file mode 100644 index 0000000000..c09dd5d195 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr @@ -0,0 +1,34 @@ + x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in + | an inner scope + ,-[input.js:10:1] + 9 | class Service { + 10 | ,-> async fetch() { + 11 | | 'use step'; + 12 | | return 'data'; + 13 | `-> } + 14 | } + `---- + x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in + | an inner scope + ,-[input.js:23:1] + 22 | Lazy = class { + 23 | ,-> static async load() { + 24 | | 'use step'; + 25 | | return 'lazy'; + 26 | `-> } + 27 | }; + `---- + x Classes using custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) must be declared at the top level of the module, not inside a function. The compiler registers the class at module + | level and cannot reference a class declared in an inner scope + ,-[input.js:32:1] + 31 | export const factory = () => { + 32 | ,-> const Point = class { + 33 | | static [WORKFLOW_SERIALIZE](inst) { + 34 | | return { x: inst.x }; + 35 | | } + 36 | | static [WORKFLOW_DESERIALIZE](data) { + 37 | | return new Point(data.x); + 38 | | } + 39 | `-> }; + 40 | return Point; + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-step.js index 1b9e814ef0..cb36c2b429 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-step.js @@ -4,7 +4,32 @@ // not the internal name (_LanguageModel) which is only scoped inside the class body. import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; /**__internal_workflows{"steps":{"input.js":{"LanguageModel#doStream":{"stepId":"step//./input//LanguageModel#doStream"},"LanguageModel.generate":{"stepId":"step//./input//LanguageModel.generate"}}},"classes":{"input.js":{"LanguageModel":{"classId":"class//./input//LanguageModel"}}}}*/; -var LanguageModel = class _LanguageModel { +var LanguageModel = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.generate; + __wf_reg.set("step//./input//LanguageModel.generate", __wf_fn); + __wf_fn.stepId = "step//./input//LanguageModel.generate"; + Object.defineProperty(__wf_fn, "name", { + value: "generate", + configurable: true + }); + __wf_fn = __wf_cls.prototype["doStream"]; + __wf_reg.set("step//./input//LanguageModel#doStream", __wf_fn); + __wf_fn.stepId = "step//./input//LanguageModel#doStream"; + Object.defineProperty(__wf_fn, "name", { + value: "doStream", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//LanguageModel", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//LanguageModel", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class _LanguageModel { constructor(modelId, config){ this.modelId = modelId; this.config = config; @@ -28,33 +53,5 @@ var LanguageModel = class _LanguageModel { result: input }; } -}; +}); export { LanguageModel }; -(function(__wf_fn, __wf_id) { - var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_fn); - __wf_fn.stepId = __wf_id; - Object.defineProperty(__wf_fn, "name", { - value: "generate", - configurable: true - }); -})(LanguageModel.generate, "step//./input//LanguageModel.generate"); -(function(__wf_fn, __wf_id) { - var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_fn); - __wf_fn.stepId = __wf_id; - Object.defineProperty(__wf_fn, "name", { - value: "doStream", - configurable: true - }); -})(LanguageModel.prototype["doStream"], "step//./input//LanguageModel#doStream"); -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: __wf_id, - writable: false, - enumerable: false, - configurable: false - }); -})(LanguageModel, "class//./input//LanguageModel"); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-workflow.js index d65622af05..a90d3eba5e 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-workflow.js @@ -4,7 +4,19 @@ // not the internal name (_LanguageModel) which is only scoped inside the class body. import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; /**__internal_workflows{"steps":{"input.js":{"LanguageModel#doStream":{"stepId":"step//./input//LanguageModel#doStream"},"LanguageModel.generate":{"stepId":"step//./input//LanguageModel.generate"}}},"classes":{"input.js":{"LanguageModel":{"classId":"class//./input//LanguageModel"}}}}*/; -var LanguageModel = class _LanguageModel { +var LanguageModel = function(__wf_cls) { + __wf_cls.generate = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//LanguageModel.generate"); + __wf_cls.prototype["doStream"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//LanguageModel#doStream"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//LanguageModel", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//LanguageModel", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class _LanguageModel { constructor(modelId, config){ this.modelId = modelId; this.config = config; @@ -18,17 +30,5 @@ var LanguageModel = class _LanguageModel { static [WORKFLOW_DESERIALIZE](data) { return new _LanguageModel(data.modelId, data.config); } -}; +}); export { LanguageModel }; -LanguageModel.generate = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//LanguageModel.generate"); -LanguageModel.prototype["doStream"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//LanguageModel#doStream"); -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: __wf_id, - writable: false, - enumerable: false, - configurable: false - }); -})(LanguageModel, "class//./input//LanguageModel"); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-step.js index c4a6f08279..e6b441ca30 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-step.js @@ -3,7 +3,17 @@ import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; /**__internal_workflows{"classes":{"input.js":{"Bash":{"classId":"class//./input//Bash"},"Shell":{"classId":"class//./input//Shell"}}}}*/; // Class expression with different binding name -var Bash = class _Bash { +var Bash = function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Bash", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Bash", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class _Bash { constructor(command){ this.command = command; } @@ -15,9 +25,19 @@ var Bash = class _Bash { static [WORKFLOW_DESERIALIZE](data) { return new Bash(data.command); } -}; +}); // Also test anonymous class expression (no internal name) -var Shell = class Shell { +var Shell = function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Shell", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Shell", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Shell { constructor(cmd){ this.cmd = cmd; } @@ -29,25 +49,5 @@ var Shell = class Shell { static [WORKFLOW_DESERIALIZE](data) { return new Shell(data.cmd); } -}; +}); export { Bash, Shell }; -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: __wf_id, - writable: false, - enumerable: false, - configurable: false - }); -})(Bash, "class//./input//Bash"); -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: __wf_id, - writable: false, - enumerable: false, - configurable: false - }); -})(Shell, "class//./input//Shell"); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-workflow.js index c4a6f08279..e6b441ca30 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-workflow.js @@ -3,7 +3,17 @@ import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; /**__internal_workflows{"classes":{"input.js":{"Bash":{"classId":"class//./input//Bash"},"Shell":{"classId":"class//./input//Shell"}}}}*/; // Class expression with different binding name -var Bash = class _Bash { +var Bash = function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Bash", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Bash", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class _Bash { constructor(command){ this.command = command; } @@ -15,9 +25,19 @@ var Bash = class _Bash { static [WORKFLOW_DESERIALIZE](data) { return new Bash(data.command); } -}; +}); // Also test anonymous class expression (no internal name) -var Shell = class Shell { +var Shell = function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Shell", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Shell", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Shell { constructor(cmd){ this.cmd = cmd; } @@ -29,25 +49,5 @@ var Shell = class Shell { static [WORKFLOW_DESERIALIZE](data) { return new Shell(data.cmd); } -}; +}); export { Bash, Shell }; -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: __wf_id, - writable: false, - enumerable: false, - configurable: false - }); -})(Bash, "class//./input//Bash"); -(function(__wf_cls, __wf_id) { - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); - __wf_reg.set(__wf_id, __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: __wf_id, - writable: false, - enumerable: false, - configurable: false - }); -})(Shell, "class//./input//Shell"); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js new file mode 100644 index 0000000000..02126c0251 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js @@ -0,0 +1,93 @@ +// Class expressions with "use step" methods must be registered through the +// binding that is in scope at module level. Bundlers emit several shapes for +// `class Foo {}` and all of them must resolve to the assigned binding rather +// than falling back to a placeholder name that does not exist at runtime. + +// tsdown/rolldown and esbuild emit this for classes that do not self-reference +// (this is the shape shipped by @vercel/sandbox, see vercel/workflow#3929). +var FileSystem = class { + constructor(sandbox) { + this.sandbox = sandbox; + } + async readFile(path) { + 'use step'; + return this.sandbox.read(path); + } +}; + +// Multiple declarators in one statement: each class must get its own binding. +var Alpha = class { + async run() { + 'use step'; + return 'alpha'; + } + }, + Beta = class { + async run() { + 'use step'; + return 'beta'; + } + }; + +// Deferred assignment to a module-level binding. +let Gamma; +Gamma = class { + async run() { + 'use step'; + return 'gamma'; + } +}; + +// Parenthesized initializer. +var Delta = (class { + async run() { + 'use step'; + return 'delta'; + } +}); + +// Assignment chain (Babel CJS interop emits `var X = exports.X = class {}`). +var Epsilon = (exports.Epsilon = class { + static async make() { + 'use step'; + return new Epsilon(); + } +}); + +// Assigned to a property: the property name is used for IDs but is not +// introduced as a binding (the class body's `Zeta` refers to the outer one). +const Zeta = 'outer'; +exports.Zeta = class { + async run() { + 'use step'; + return Zeta; + } +}; + +// Object literal property value: the key is the name, with `.name` preserved. +export const handlers = { + Job: class { + static async execute() { + 'use step'; + return 'job'; + } + }, + 'kebab-job': class { + get status() { + 'use step'; + return 'ok'; + } + }, +}; + +// Named class expression in an arbitrary position: its own name is used. +registerPlugin( + class Plugin { + async run() { + 'use step'; + return 'plugin'; + } + } +); + +export { FileSystem, Alpha, Beta, Gamma, Delta, Epsilon }; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js new file mode 100644 index 0000000000..8605c755e0 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js @@ -0,0 +1,264 @@ +/**__internal_workflows{"steps":{"input.js":{"Alpha#run":{"stepId":"step//./input//Alpha#run"},"Beta#run":{"stepId":"step//./input//Beta#run"},"Delta#run":{"stepId":"step//./input//Delta#run"},"Epsilon.make":{"stepId":"step//./input//Epsilon.make"},"FileSystem#readFile":{"stepId":"step//./input//FileSystem#readFile"},"Gamma#run":{"stepId":"step//./input//Gamma#run"},"Job.execute":{"stepId":"step//./input//Job.execute"},"Plugin#run":{"stepId":"step//./input//Plugin#run"},"Zeta#run":{"stepId":"step//./input//Zeta#run"},"kebab-job#status":{"stepId":"step//./input//kebab-job#status"}}},"classes":{"input.js":{"Alpha":{"classId":"class//./input//Alpha"},"Beta":{"classId":"class//./input//Beta"},"Delta":{"classId":"class//./input//Delta"},"Epsilon":{"classId":"class//./input//Epsilon"},"FileSystem":{"classId":"class//./input//FileSystem"},"Gamma":{"classId":"class//./input//Gamma"},"Job":{"classId":"class//./input//Job"},"Plugin":{"classId":"class//./input//Plugin"},"Zeta":{"classId":"class//./input//Zeta"},"kebab-job":{"classId":"class//./input//kebab-job"}}}}*/; +// Class expressions with "use step" methods must be registered through the +// binding that is in scope at module level. Bundlers emit several shapes for +// `class Foo {}` and all of them must resolve to the assigned binding rather +// than falling back to a placeholder name that does not exist at runtime. +// tsdown/rolldown and esbuild emit this for classes that do not self-reference +// (this is the shape shipped by @vercel/sandbox, see vercel/workflow#3929). +var FileSystem = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["readFile"]; + __wf_reg.set("step//./input//FileSystem#readFile", __wf_fn); + __wf_fn.stepId = "step//./input//FileSystem#readFile"; + Object.defineProperty(__wf_fn, "name", { + value: "readFile", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//FileSystem", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//FileSystem", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class FileSystem { + constructor(sandbox){ + this.sandbox = sandbox; + } + async readFile(path) { + return this.sandbox.read(path); + } +}); +// Multiple declarators in one statement: each class must get its own binding. +var Alpha = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//Alpha#run", __wf_fn); + __wf_fn.stepId = "step//./input//Alpha#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Alpha", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Alpha", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Alpha { + async run() { + return 'alpha'; + } +}), Beta = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//Beta#run", __wf_fn); + __wf_fn.stepId = "step//./input//Beta#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Beta", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Beta", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Beta { + async run() { + return 'beta'; + } +}); +// Deferred assignment to a module-level binding. +let Gamma; +Gamma = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//Gamma#run", __wf_fn); + __wf_fn.stepId = "step//./input//Gamma#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Gamma", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Gamma", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Gamma { + async run() { + return 'gamma'; + } +}); +// Parenthesized initializer. +var Delta = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//Delta#run", __wf_fn); + __wf_fn.stepId = "step//./input//Delta#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Delta", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Delta", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Delta { + async run() { + return 'delta'; + } +}); +// Assignment chain (Babel CJS interop emits `var X = exports.X = class {}`). +var Epsilon = exports.Epsilon = function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.make; + __wf_reg.set("step//./input//Epsilon.make", __wf_fn); + __wf_fn.stepId = "step//./input//Epsilon.make"; + Object.defineProperty(__wf_fn, "name", { + value: "make", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Epsilon", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Epsilon", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Epsilon { + static async make() { + return new Epsilon(); + } +}); +// Assigned to a property: the property name is used for IDs but is not +// introduced as a binding (the class body's `Zeta` refers to the outer one). +const Zeta = 'outer'; +exports.Zeta = function(__wf_cls) { + Object.defineProperty(__wf_cls, "name", { + value: "Zeta", + configurable: true + }); + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//Zeta#run", __wf_fn); + __wf_fn.stepId = "step//./input//Zeta#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Zeta", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Zeta", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { + async run() { + return Zeta; + } +}); +// Object literal property value: the key is the name, with `.name` preserved. +export const handlers = { + Job: function(__wf_cls) { + Object.defineProperty(__wf_cls, "name", { + value: "Job", + configurable: true + }); + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.execute; + __wf_reg.set("step//./input//Job.execute", __wf_fn); + __wf_fn.stepId = "step//./input//Job.execute"; + Object.defineProperty(__wf_fn, "name", { + value: "execute", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Job", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Job", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + static async execute() { + return 'job'; + } + }), + 'kebab-job': function(__wf_cls) { + Object.defineProperty(__wf_cls, "name", { + value: "kebab-job", + configurable: true + }); + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = Object.getOwnPropertyDescriptor(__wf_cls.prototype, "status").get; + __wf_reg.set("step//./input//kebab-job#status", __wf_fn); + __wf_fn.stepId = "step//./input//kebab-job#status"; + Object.defineProperty(__wf_fn, "name", { + value: "status", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//kebab-job", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//kebab-job", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + get status() { + return 'ok'; + } + }) +}; +// Named class expression in an arbitrary position: its own name is used. +registerPlugin(function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//Plugin#run", __wf_fn); + __wf_fn.stepId = "step//./input//Plugin#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Plugin", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Plugin", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Plugin { + async run() { + return 'plugin'; + } +})); +export { FileSystem, Alpha, Beta, Gamma, Delta, Epsilon }; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js new file mode 100644 index 0000000000..3e871ed5b8 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js @@ -0,0 +1,168 @@ +/**__internal_workflows{"steps":{"input.js":{"Alpha#run":{"stepId":"step//./input//Alpha#run"},"Beta#run":{"stepId":"step//./input//Beta#run"},"Delta#run":{"stepId":"step//./input//Delta#run"},"Epsilon.make":{"stepId":"step//./input//Epsilon.make"},"FileSystem#readFile":{"stepId":"step//./input//FileSystem#readFile"},"Gamma#run":{"stepId":"step//./input//Gamma#run"},"Job.execute":{"stepId":"step//./input//Job.execute"},"Plugin#run":{"stepId":"step//./input//Plugin#run"},"Zeta#run":{"stepId":"step//./input//Zeta#run"},"kebab-job#status":{"stepId":"step//./input//kebab-job#status"}}},"classes":{"input.js":{"Alpha":{"classId":"class//./input//Alpha"},"Beta":{"classId":"class//./input//Beta"},"Delta":{"classId":"class//./input//Delta"},"Epsilon":{"classId":"class//./input//Epsilon"},"FileSystem":{"classId":"class//./input//FileSystem"},"Gamma":{"classId":"class//./input//Gamma"},"Job":{"classId":"class//./input//Job"},"Plugin":{"classId":"class//./input//Plugin"},"Zeta":{"classId":"class//./input//Zeta"},"kebab-job":{"classId":"class//./input//kebab-job"}}}}*/; +// Class expressions with "use step" methods must be registered through the +// binding that is in scope at module level. Bundlers emit several shapes for +// `class Foo {}` and all of them must resolve to the assigned binding rather +// than falling back to a placeholder name that does not exist at runtime. +// tsdown/rolldown and esbuild emit this for classes that do not self-reference +// (this is the shape shipped by @vercel/sandbox, see vercel/workflow#3929). +var FileSystem = function(__wf_cls) { + __wf_cls.prototype["readFile"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//FileSystem#readFile"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//FileSystem", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//FileSystem", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class FileSystem { + constructor(sandbox){ + this.sandbox = sandbox; + } +}); +// Multiple declarators in one statement: each class must get its own binding. +var Alpha = function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Alpha#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Alpha", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Alpha", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Alpha { +}), Beta = function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Beta#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Beta", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Beta", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Beta { +}); +// Deferred assignment to a module-level binding. +let Gamma; +Gamma = function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Gamma#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Gamma", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Gamma", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Gamma { +}); +// Parenthesized initializer. +var Delta = function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Delta#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Delta", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Delta", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Delta { +}); +// Assignment chain (Babel CJS interop emits `var X = exports.X = class {}`). +var Epsilon = exports.Epsilon = function(__wf_cls) { + __wf_cls.make = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Epsilon.make"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Epsilon", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Epsilon", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Epsilon { +}); +exports.Zeta = function(__wf_cls) { + Object.defineProperty(__wf_cls, "name", { + value: "Zeta", + configurable: true + }); + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Zeta#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Zeta", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Zeta", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { +}); +// Object literal property value: the key is the name, with `.name` preserved. +export const handlers = { + Job: function(__wf_cls) { + Object.defineProperty(__wf_cls, "name", { + value: "Job", + configurable: true + }); + __wf_cls.execute = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Job.execute"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Job", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Job", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + }), + 'kebab-job': function(__wf_cls) { + Object.defineProperty(__wf_cls, "name", { + value: "kebab-job", + configurable: true + }); + var __step_kebab_job$status = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//kebab-job#status"); + Object.defineProperty(__wf_cls.prototype, "status", { + get () { + return __step_kebab_job$status.call(this); + }, + configurable: true, + enumerable: false + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//kebab-job", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//kebab-job", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + }) +}; +// Named class expression in an arbitrary position: its own name is used. +registerPlugin(function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Plugin#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Plugin", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Plugin", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Plugin { +})); +export { FileSystem, Alpha, Beta, Gamma, Delta, Epsilon }; From 8f107f909c19fc1ef44958b6f3e9fb547fd47e59 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 4 Sep 2026 11:39:53 -0700 Subject: [PATCH 2/2] 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`, 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. --- .../class-expression-registration-iife.md | 2 +- packages/swc-plugin-workflow/spec.md | 18 +- .../swc-plugin-workflow/transform/src/lib.rs | 296 ++++++++++-------- .../output-step.js | 66 ---- .../output-step.stderr | 54 ---- .../output-workflow.js | 56 ---- .../output-workflow.stderr | 54 ---- .../output-step.stderr | 10 +- .../output-workflow.stderr | 10 +- .../input.js | 40 ++- .../output-step.js | 172 ++++++++++ .../output-workflow.js | 130 ++++++++ .../class-expression-binding-shapes/input.js | 12 + .../output-step.js | 27 +- .../output-workflow.js | 27 +- 15 files changed, 573 insertions(+), 401 deletions(-) delete mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js delete mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr delete mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js delete mode 100644 packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr rename packages/swc-plugin-workflow/transform/tests/{errors/anonymous-class-step-methods => fixture/class-expression-anonymous-generated-names}/input.js (50%) create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-workflow.js diff --git a/.changeset/class-expression-registration-iife.md b/.changeset/class-expression-registration-iife.md index 5cb402229d..0425d1a67d 100644 --- a/.changeset/class-expression-registration-iife.md +++ b/.changeset/class-expression-registration-iife.md @@ -2,4 +2,4 @@ '@workflow/swc-plugin': patch --- -Register class expressions (`var Foo = class { ... }`, `exports.Foo = class { ... }`, `{ Foo: class { ... } }`, etc.) through an IIFE that closes over the class instead of module-level code that references the class by name, and fail the build with a clear error instead of emitting an unresolvable `AnonymousClass` reference when a class with `"use step"` methods or custom serialization has no derivable name or is declared inside a function. +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. diff --git a/packages/swc-plugin-workflow/spec.md b/packages/swc-plugin-workflow/spec.md index a9d2e77101..b5404f819b 100644 --- a/packages/swc-plugin-workflow/spec.md +++ b/packages/swc-plugin-workflow/spec.md @@ -778,26 +778,25 @@ The IIFE removes the need to *reference* the class by name, but step and class I 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. 2. The class expression's own identifier: `foo(class Plugin {})` uses `Plugin`. 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). +4. A generated `AnonymousClass` 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`, the name is suffixed (`AnonymousClass6$1`). Like the `_anonymousStep` 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. -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 })`. +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. Classes that already have an identifier (e.g. `class _Bash { ... }`) are never renamed. -#### Unnameable and nested classes are errors +#### Dead-code elimination -If a class expression has `"use step"`/`"use workflow"` methods, `"use step"` getters, or custom serialization, and no name can be derived (e.g. `foo(class { ... })`, `[class { ... }]`, `cond ? class { ... } : null`), the plugin emits a compile error: +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... }]])`). -``` -Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can reference it when registering it at module level -``` +#### Nested classes are errors -Likewise, a class (declaration or expression) that uses those features but is declared *inside a function* is an error: +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. The compiler registers the class at module level and cannot reference a class declared in an inner scope +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 ``` -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 a placeholder `AnonymousClass` name (or the inner class's name) in these situations, which produced module-level code that threw a `ReferenceError` as soon as the module was evaluated. At most one error is reported per class, at the first offending member; classes without steps or serialization are unaffected. No errors are emitted in `detect` mode, which generates no code. +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. ### Anonymous default class export rewriting @@ -906,7 +905,6 @@ The plugin emits errors for invalid usage: | Invalid exports (`"use workflow"`) | Module-level `"use workflow"` files can only export async functions | | Invalid exports (`"use step"`) | Module-level `"use step"` files can only export functions (sync or async) | | Misspelled directive | Detects typos like `"use steps"` or `"use workflows"` | -| Unnameable class expression | An anonymous class expression with step/workflow methods, step getters, or custom serialization in a position that provides no name (e.g. `foo(class { ... })`) | | 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 | --- diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index 623639c6a6..ad4dd0270f 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -35,13 +35,12 @@ enum WorkflowErrorKind { span: swc_core::common::Span, directive: &'static str, }, - /// A class that needs generated module-level code (step method - /// registration or custom serialization registration) but that code has - /// no way to reference the class. Emitting a placeholder name would only - /// defer the failure to a `ReferenceError` when the module is evaluated. - UnreferenceableClass { + /// A class declared inside a function that uses step methods or custom + /// serialization. Its registration would only run when (and each time) + /// the enclosing function runs, not at module load, so its steps could + /// not be resolved by ID. + NestedClass { span: swc_core::common::Span, - class: UnreferenceableClass, feature: &'static str, }, } @@ -64,21 +63,16 @@ struct PendingClassExpr { /// `None` when the class already has an identifier, or when the name was /// derived from a property key and must not be introduced as a binding. ident_to_insert: Option, + /// When the class expression is anonymous and its name came from a + /// property key (`exports.Foo = class {}`, `{ Foo: class {} }`), `.name` + /// is set at runtime inside the registration IIFE instead, preserving the + /// name the original position inferred without introducing a binding. + /// `None` for generated names, which leave `.name` as it was (`""`). + name_to_define: Option, /// Whether the class defines `WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE`. has_custom_serialization: bool, } -/// Why generated module-level code cannot reference a class. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum UnreferenceableClass { - /// An anonymous class expression that is not directly assigned to a - /// variable, e.g. `foo(class { ... })` or `exports.Foo = class { ... }`. - Anonymous, - /// A class declared inside a function body; its binding is not in scope - /// at module level where registrations are emitted. - Nested, -} - /// Sanitize a string for use as part of a JavaScript identifier. /// Replaces characters that are not valid in JS identifiers with `_`. fn sanitize_ident_part(name: &str) -> String { @@ -150,22 +144,12 @@ fn emit_error(error: WorkflowErrorKind) { ) }, ), - WorkflowErrorKind::UnreferenceableClass { - span, - class, - feature, - } => ( + WorkflowErrorKind::NestedClass { span, feature } => ( span, - match class { - UnreferenceableClass::Anonymous => format!( - "Anonymous class expressions cannot use {}. Assign the class to a variable (e.g. `const MyClass = class {{ ... }}`) or give it a name (`class MyClass {{ ... }}`) so the compiler can reference it when registering it at module level", - feature - ), - UnreferenceableClass::Nested => format!( - "Classes using {} must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in an inner scope", - feature - ), - }, + format!( + "Classes using {} 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", + feature + ), ), }; @@ -465,13 +449,16 @@ pub struct StepTransform { // name resolved; consumed by `visit_mut_expr`, which owns the enclosing // `Expr` node and can replace it with the registration IIFE. pending_class_expr_registration: Option, - // Set while visiting a class whose generated registration code could not - // reference the class from module scope (see `UnreferenceableClass`). - // `current_class_name` is `None` for such classes so that step methods and - // custom serialization are reported as errors instead of emitting code - // that would throw a ReferenceError at runtime. Cleared after the first - // error so a class produces at most one diagnostic. - current_class_unreferenceable: Option, + // Set while visiting a class declared inside a function (see + // `WorkflowErrorKind::NestedClass`). `current_class_name` is `None` for + // such classes so that step methods and custom serialization are reported + // as errors instead of registering a class that cannot be resolved at + // module load. Cleared after the first error so a class produces at most + // one diagnostic. + current_class_is_nested: bool, + // Counter for naming anonymous class expressions that have nothing to + // derive a name from (`foo(class { ... })`): `AnonymousClass1`, ... + anonymous_class_counter: usize, // Track static method steps that need registration after the class declaration // (class_name, method_name, step_id, span) static_method_step_registrations: Vec<(String, String, String, swc_core::common::Span)>, @@ -513,6 +500,12 @@ pub struct StepTransform { require_namespace_identifiers: HashSet, // Track class names for the manifest (preserved copy before drain) classes_for_manifest: HashSet, + // Spans of class expressions that were wrapped in a registration IIFE. + // Dead-code elimination must keep any declaration whose initializer + // contains one of these: the registration is a side effect of evaluating + // the initializer, and the declared binding may be otherwise unused + // (`const registry = new Map([["point", class { ...serde... }]])`). + registered_class_expr_spans: HashSet, } // Structure to track variable names and their access patterns @@ -1918,7 +1911,8 @@ impl StepTransform { current_class_binding_name: None, current_class_binding_from_key: false, pending_class_expr_registration: None, - current_class_unreferenceable: None, + current_class_is_nested: false, + anonymous_class_counter: 0, static_method_step_registrations: Vec::new(), static_method_workflow_registrations: Vec::new(), static_step_methods_to_strip: Vec::new(), @@ -1933,6 +1927,7 @@ impl StepTransform { serialization_symbol_identifiers: HashMap::new(), require_namespace_identifiers: HashSet::new(), classes_for_manifest: HashSet::new(), + registered_class_expr_spans: HashSet::new(), } } @@ -3283,50 +3278,55 @@ impl StepTransform { has_serialize && has_deserialize } - /// Report that the class currently being visited needs module-level - /// registration code for `feature` but cannot be referenced from module - /// scope. Emits at most one error per class (the first offending member), - /// and never emits in `Detect` mode, which generates no code. + /// Report that the class currently being visited uses `feature` but is + /// declared inside a function. Emits at most one error per class (at the + /// first offending member), and never in `Detect` mode, which generates + /// no code. /// - /// Returns `true` if the class is unreferenceable (regardless of whether an - /// error was emitted), so callers can skip code generation. - fn report_unreferenceable_class( - &mut self, - span: swc_core::common::Span, - feature: &'static str, - ) -> bool { - let Some(class) = self.current_class_unreferenceable else { + /// Returns `true` if the class is nested (regardless of whether an error + /// was emitted), so callers can skip code generation. + fn report_nested_class(&mut self, span: swc_core::common::Span, feature: &'static str) -> bool { + if !self.current_class_is_nested { return false; - }; + } if !matches!(self.mode, TransformMode::Detect) { - emit_error(WorkflowErrorKind::UnreferenceableClass { - span, - class, - feature, - }); + emit_error(WorkflowErrorKind::NestedClass { span, feature }); } // Only report once per class. - self.current_class_unreferenceable = None; + self.current_class_is_nested = false; true } - /// Resolve the name that generated module-level code should use to - /// reference a class expression, or the reason it cannot be referenced. + /// Resolve the name used for a module-level class expression's step and + /// class IDs. /// /// Prefers the binding the expression is assigned to (`Foo` in /// `var Foo = class _Foo {}`) over the expression's own identifier - /// (`_Foo`), which is only in scope inside the class body. + /// (`_Foo`), which is only in scope inside the class body. When neither + /// exists (`foo(class { ... })`) a deterministic `AnonymousClass` name + /// is generated, counting only anonymous classes that have something to + /// register so unrelated anonymous classes do not shift the numbering. + /// Returns `None` for an anonymous class with nothing to register. + /// + /// Generated names are positional: adding another such class earlier in + /// the module renumbers the ones after it, and with them their step IDs. fn resolve_class_expr_name( - &self, + &mut self, class_expr: &ClassExpr, binding_name: Option, - ) -> Result { - if !self.in_module_level { - return Err(UnreferenceableClass::Nested); - } + ) -> Option { binding_name .or_else(|| class_expr.ident.as_ref().map(|i| i.sym.to_string())) - .ok_or(UnreferenceableClass::Anonymous) + .or_else(|| { + if !self.class_needs_binding_rewrite(&class_expr.class) { + return None; + } + self.anonymous_class_counter += 1; + Some(self.generate_unique_name(&format!( + "AnonymousClass{}", + self.anonymous_class_counter + ))) + }) } /// The static name of a member access (`obj.name` or `obj["name"]`), if any. @@ -4374,6 +4374,34 @@ impl StepTransform { } // Remove dead code (unused functions, variables, statements, and imports) recursively + /// Whether any initializer in `var_decl` contains a class expression that + /// was wrapped in a registration IIFE. + fn contains_registered_class_expr(&self, var_decl: &VarDecl) -> bool { + if self.registered_class_expr_spans.is_empty() { + return false; + } + struct Finder<'a> { + spans: &'a HashSet, + found: bool, + } + impl Visit for Finder<'_> { + noop_visit_type!(); + fn visit_class(&mut self, class: &Class) { + if self.spans.contains(&class.span) { + self.found = true; + } else { + class.visit_children_with(self); + } + } + } + let mut finder = Finder { + spans: &self.registered_class_expr_spans, + found: false, + }; + var_decl.visit_with(&mut finder); + finder.found + } + fn remove_dead_code(&self, items: &mut Vec) { // Only runs in workflow and step mode if !matches!(self.mode, TransformMode::Workflow | TransformMode::Step) { @@ -4406,8 +4434,13 @@ impl StepTransform { && !self.step_function_names.contains(&fn_name) && !self.workflow_function_names.contains(&fn_name) } - // Remove unused variable declarations + // Remove unused variable declarations, unless evaluating an + // initializer registers a class (see + // `registered_class_expr_spans`). ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => { + if self.contains_registered_class_expr(var_decl) { + continue; + } // Check if all variables in this declaration are unused var_decl.decls.iter().all(|declarator| { match &declarator.name { @@ -5622,6 +5655,7 @@ impl StepTransform { let PendingClassExpr { name: class_name, ident_to_insert, + name_to_define, .. } = pending; @@ -5635,26 +5669,26 @@ impl StepTransform { else { unreachable!("wrap_class_expr_with_registrations is only called on class expressions"); }; + self.registered_class_expr_spans + .insert(class_expr.class.span); let mut body = Vec::with_capacity(stmts.len() + 2); if class_expr.ident.is_none() { - match ident_to_insert { - // `var Foo = class {}` -> `var Foo = (...)(class Foo {})`. - // Passing the class as a call argument defeats the `.name` - // inference the original assignment provided, so make the - // name explicit. This mirrors `var Foo = class Foo {}`, which - // is behaviorally equivalent for typical class usage. - Some(name) => { - class_expr.ident = Some(Self::ident(&name)); - } - // The name came from a property key (`exports.Foo = class {}`, - // `{ Foo: class {} }`). Introducing `Foo` as the class's own - // binding could shadow an unrelated outer `Foo` referenced - // from the class body, so set `.name` at runtime instead. - None => { - body.push(Self::define_name_stmt(CLASS_EXPR_IIFE_PARAM, &class_name)); - } + // `var Foo = class {}` -> `var Foo = (...)(class Foo {})`. + // Passing the class as a call argument defeats the `.name` + // inference the original assignment provided, so make the name + // explicit. This mirrors `var Foo = class Foo {}`, which is + // behaviorally equivalent for typical class usage. + if let Some(name) = ident_to_insert { + class_expr.ident = Some(Self::ident(&name)); + } + // The name came from a property key (`exports.Foo = class {}`, + // `{ Foo: class {} }`). Introducing `Foo` as the class's own + // binding could shadow an unrelated outer `Foo` referenced from + // the class body, so set `.name` at runtime instead. + if let Some(name) = name_to_define { + body.push(Self::define_name_stmt(CLASS_EXPR_IIFE_PARAM, &name)); } } @@ -8262,7 +8296,7 @@ impl VisitMut for StepTransform { fn visit_mut_class_decl(&mut self, class_decl: &mut ClassDecl) { let class_name = class_decl.ident.sym.to_string(); let old_class_name = self.current_class_name.take(); - let old_unreferenceable = self.current_class_unreferenceable.take(); + let old_is_nested = std::mem::replace(&mut self.current_class_is_nested, false); if self.in_module_level { self.current_class_name = Some(class_name.clone()); @@ -8273,13 +8307,13 @@ impl VisitMut for StepTransform { .insert(class_name.clone()); } } else { - // A class declared inside a function is not in scope at module - // level, where registrations are emitted. Leave `current_class_name` + // A class declared inside a function would only be registered when + // that function runs, not at module load. Leave `current_class_name` // unset so step methods / serialization inside it are reported - // instead of generating code that throws a ReferenceError. - self.current_class_unreferenceable = Some(UnreferenceableClass::Nested); + // instead of generating a registration that cannot be resolved. + self.current_class_is_nested = true; if self.has_custom_serialization_methods(&class_decl.class) { - self.report_unreferenceable_class( + self.report_nested_class( class_decl.class.span, "custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE)", ); @@ -8364,7 +8398,7 @@ impl VisitMut for StepTransform { // Restore previous class name self.current_class_name = old_class_name; - self.current_class_unreferenceable = old_unreferenceable; + self.current_class_is_nested = old_is_nested; } // Handle class expressions to track class name for static methods @@ -8376,34 +8410,29 @@ impl VisitMut for StepTransform { // Compute the tracked class name: prefer the binding name (e.g. `Foo` // from `var Foo = class _Foo {}`) over the internal class expression - // name (`_Foo`). The name is used to derive step and class IDs, and to - // match the registrations recorded while visiting the body. + // name (`_Foo`), falling back to a generated `AnonymousClass`. The + // name is used to derive step and class IDs, and to match the + // registrations recorded while visiting the body. // // Generated registration code never references a class expression by // name: `visit_mut_expr` wraps the expression in an IIFE that receives // the class as an argument (see `wrap_class_expr_with_registrations`), - // so the class does not need a module-scope binding at all. What it - // does need is a *name* for its IDs. When none can be derived (an - // anonymous class expression in a position with no key or binding, - // e.g. `foo(class { ... })`), or when the class is nested inside a - // function (its registration would only run when that function runs, - // not at module load), record the problem so that any step method or - // custom serialization found in the body is reported as a compile - // error instead of silently producing a broken registration. + // so the class does not need a module-scope binding at all, only a + // name for its IDs. A class nested inside a function is the exception: + // its registration would only run when that function runs, not at + // module load, so any step method or custom serialization found in + // its body is reported as a compile error instead. let old_class_name = self.current_class_name.take(); - let old_unreferenceable = self.current_class_unreferenceable.take(); + let old_is_nested = std::mem::replace(&mut self.current_class_is_nested, false); - let tracked_class_name = - match self.resolve_class_expr_name(class_expr, binding_name.clone()) { - Ok(name) => { - self.current_class_name = Some(name.clone()); - Some(name) - } - Err(reason) => { - self.current_class_unreferenceable = Some(reason); - None - } - }; + let tracked_class_name = if self.in_module_level { + let name = self.resolve_class_expr_name(class_expr, binding_name.clone()); + self.current_class_name = name.clone(); + name + } else { + self.current_class_is_nested = true; + None + }; // Check if class has custom serialization methods (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) let has_serde = self.has_custom_serialization_methods(&class_expr.class); @@ -8413,7 +8442,7 @@ impl VisitMut for StepTransform { self.classes_needing_serialization.insert(name.clone()); } None => { - self.report_unreferenceable_class( + self.report_nested_class( class_expr.class.span, "custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE)", ); @@ -8428,10 +8457,14 @@ impl VisitMut for StepTransform { // `.name` survives the wrapping. Names derived from property keys are // not inserted: `exports.Foo = class { m() { Foo } }` may refer to an // unrelated outer `Foo`, which a class-scoped `Foo` would shadow. - let ident_to_insert = match (&binding_name, class_expr.ident.is_none(), binding_from_key) { - (Some(name), true, false) => Some(name.clone()), - _ => None, - }; + // Generated names leave `.name` untouched: the original position + // inferred no name either. + let (ident_to_insert, name_to_define) = + match (&binding_name, class_expr.ident.is_none(), binding_from_key) { + (Some(name), true, false) => (Some(name.clone()), None), + (Some(name), true, true) => (None, Some(name.clone())), + _ => (None, None), + }; // Visit the class body (this populates static_step_methods_to_strip) class_expr.class.visit_mut_with(self); @@ -8506,12 +8539,13 @@ impl VisitMut for StepTransform { self.pending_class_expr_registration = tracked_class_name.map(|name| PendingClassExpr { name, ident_to_insert, + name_to_define, has_custom_serialization: has_serde, }); // Restore previous class name self.current_class_name = old_class_name; - self.current_class_unreferenceable = old_unreferenceable; + self.current_class_is_nested = old_is_nested; } // Handle class methods @@ -8549,10 +8583,10 @@ impl VisitMut for StepTransform { let class_name = match &self.current_class_name { Some(name) => name.clone(), None => { - // The enclosing class cannot be referenced from module - // level (anonymous or nested), so the getter cannot be + // The enclosing class is declared inside a function (or + // has nothing to register), so the getter cannot be // registered. Report it and leave the getter untouched. - self.report_unreferenceable_class(method.span, "\"use step\" getters"); + self.report_nested_class(method.span, "\"use step\" getters"); method.visit_mut_children_with(self); return; } @@ -8657,10 +8691,10 @@ impl VisitMut for StepTransform { let class_name = match &self.current_class_name { Some(name) => name.clone(), None => { - // The enclosing class cannot be referenced from module - // level (anonymous or nested), so the method cannot be - // registered. Report it and leave the method untouched. - self.report_unreferenceable_class(method.span, "\"use step\" methods"); + // The enclosing class is declared inside a function, so + // the method cannot be registered at module load. + // Report it and leave the method untouched. + self.report_nested_class(method.span, "\"use step\" methods"); method.visit_mut_children_with(self); return; } @@ -8754,10 +8788,10 @@ impl VisitMut for StepTransform { let class_name = match &self.current_class_name { Some(name) => name.clone(), None => { - // The enclosing class cannot be referenced from module - // level (anonymous or nested), so the method cannot be - // registered. Report it and leave the method untouched. - self.report_unreferenceable_class( + // The enclosing class is declared inside a function, so + // the method cannot be registered at module load. + // Report it and leave the method untouched. + self.report_nested_class( method.span, if has_workflow { "\"use workflow\" methods" diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js deleted file mode 100644 index 0a5b458d5c..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js +++ /dev/null @@ -1,66 +0,0 @@ -// Anonymous class expressions in positions that provide no name (not assigned -// to a variable or property) have nothing to derive a step/class ID from. The -// compiler used to emit `AnonymousClass.prototype[...]`, which is a guaranteed -// ReferenceError at module evaluation (vercel/workflow#3929). It must instead -// fail at compile time. -/**__internal_workflows{"steps":{"input.js":{"NamedPlugin#run":{"stepId":"step//./input//NamedPlugin#run"}}},"classes":{"input.js":{"NamedPlugin":{"classId":"class//./input//NamedPlugin"}}}}*/; -// Error: class passed directly as an argument -registerPlugin(class { - async run() { - 'use step'; - return 'plugin'; - } -}); -// Error: class as an array element -export const handlers = [ - class { - static async execute() { - 'use step'; - return 'job'; - } - } -]; -// Error: class chosen by a conditional -export const Worker = process.env.FAST ? class { - get status() { - 'use step'; - return 'ok'; - } -} : null; -// Error: static "use workflow" method -useModel(class { - static async orchestrate() { - 'use workflow'; - return 'done'; - } -}); -// OK: anonymous class expression without steps or serialization -export const plain = class { - greet() { - return 'hi'; - } -}; -// OK: naming the class is all that is needed -registerPlugin(function(__wf_cls) { - var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; - __wf_fn = __wf_cls.prototype["run"]; - __wf_reg.set("step//./input//NamedPlugin#run", __wf_fn); - __wf_fn.stepId = "step//./input//NamedPlugin#run"; - Object.defineProperty(__wf_fn, "name", { - value: "run", - configurable: true - }); - var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); - __wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: "class//./input//NamedPlugin", - writable: false, - enumerable: false, - configurable: false - }); - return __wf_cls; -}(class NamedPlugin { - async run() { - return 'named'; - } -})); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr deleted file mode 100644 index 80fb8c40f4..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr +++ /dev/null @@ -1,54 +0,0 @@ - x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can - | reference it when registering it at module level - ,-[input.js:10:1] - 9 | registerPlugin(class { - 10 | ,-> async run() { - 11 | | 'use step'; - 12 | | return 'plugin'; - 13 | `-> } - 14 | }); - `---- - x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can - | reference it when registering it at module level - ,-[input.js:19:1] - 18 | class { - 19 | ,-> static async execute() { - 20 | | 'use step'; - 21 | | return 'job'; - 22 | `-> } - 23 | }, - `---- - x Anonymous class expressions cannot use "use step" getters. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can - | reference it when registering it at module level - ,-[input.js:29:1] - 28 | ? class { - 29 | ,-> get status() { - 30 | | 'use step'; - 31 | | return 'ok'; - 32 | `-> } - 33 | } - `---- - x Anonymous class expressions cannot use custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE). Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name - | (`class MyClass { ... }`) so the compiler can reference it when registering it at module level - ,-[input.js:38:1] - 37 | const registry = new Map([ - 38 | ,-> ['point', class { - 39 | | static [WORKFLOW_SERIALIZE](inst) { - 40 | | return { x: inst.x }; - 41 | | } - 42 | | static [WORKFLOW_DESERIALIZE](data) { - 43 | | return { x: data.x }; - 44 | | } - 45 | `-> }], - 46 | ]); - `---- - x Anonymous class expressions cannot use "use workflow" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler - | can reference it when registering it at module level - ,-[input.js:50:1] - 49 | useModel(class { - 50 | ,-> static async orchestrate() { - 51 | | 'use workflow'; - 52 | | return 'done'; - 53 | `-> } - 54 | }); - `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js deleted file mode 100644 index 72666f7233..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js +++ /dev/null @@ -1,56 +0,0 @@ -// Anonymous class expressions in positions that provide no name (not assigned -// to a variable or property) have nothing to derive a step/class ID from. The -// compiler used to emit `AnonymousClass.prototype[...]`, which is a guaranteed -// ReferenceError at module evaluation (vercel/workflow#3929). It must instead -// fail at compile time. -/**__internal_workflows{"steps":{"input.js":{"NamedPlugin#run":{"stepId":"step//./input//NamedPlugin#run"}}},"classes":{"input.js":{"NamedPlugin":{"classId":"class//./input//NamedPlugin"}}}}*/; -// Error: class passed directly as an argument -registerPlugin(class { - async run() { - 'use step'; - return 'plugin'; - } -}); -// Error: class as an array element -export const handlers = [ - class { - static async execute() { - 'use step'; - return 'job'; - } - } -]; -// Error: class chosen by a conditional -export const Worker = process.env.FAST ? class { - get status() { - 'use step'; - return 'ok'; - } -} : null; -// Error: static "use workflow" method -useModel(class { - static async orchestrate() { - 'use workflow'; - return 'done'; - } -}); -// OK: anonymous class expression without steps or serialization -export const plain = class { - greet() { - return 'hi'; - } -}; -// OK: naming the class is all that is needed -registerPlugin(function(__wf_cls) { - __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//NamedPlugin#run"); - var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); - __wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls); - Object.defineProperty(__wf_cls, "classId", { - value: "class//./input//NamedPlugin", - writable: false, - enumerable: false, - configurable: false - }); - return __wf_cls; -}(class NamedPlugin { -})); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr deleted file mode 100644 index 80fb8c40f4..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr +++ /dev/null @@ -1,54 +0,0 @@ - x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can - | reference it when registering it at module level - ,-[input.js:10:1] - 9 | registerPlugin(class { - 10 | ,-> async run() { - 11 | | 'use step'; - 12 | | return 'plugin'; - 13 | `-> } - 14 | }); - `---- - x Anonymous class expressions cannot use "use step" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can - | reference it when registering it at module level - ,-[input.js:19:1] - 18 | class { - 19 | ,-> static async execute() { - 20 | | 'use step'; - 21 | | return 'job'; - 22 | `-> } - 23 | }, - `---- - x Anonymous class expressions cannot use "use step" getters. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler can - | reference it when registering it at module level - ,-[input.js:29:1] - 28 | ? class { - 29 | ,-> get status() { - 30 | | 'use step'; - 31 | | return 'ok'; - 32 | `-> } - 33 | } - `---- - x Anonymous class expressions cannot use custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE). Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name - | (`class MyClass { ... }`) so the compiler can reference it when registering it at module level - ,-[input.js:38:1] - 37 | const registry = new Map([ - 38 | ,-> ['point', class { - 39 | | static [WORKFLOW_SERIALIZE](inst) { - 40 | | return { x: inst.x }; - 41 | | } - 42 | | static [WORKFLOW_DESERIALIZE](data) { - 43 | | return { x: data.x }; - 44 | | } - 45 | `-> }], - 46 | ]); - `---- - x Anonymous class expressions cannot use "use workflow" methods. Assign the class to a variable (e.g. `const MyClass = class { ... }`) or give it a name (`class MyClass { ... }`) so the compiler - | can reference it when registering it at module level - ,-[input.js:50:1] - 49 | useModel(class { - 50 | ,-> static async orchestrate() { - 51 | | 'use workflow'; - 52 | | return 'done'; - 53 | `-> } - 54 | }); - `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr index c09dd5d195..716d8a80b6 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr @@ -1,5 +1,4 @@ - x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in - | an inner scope + x 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 ,-[input.js:10:1] 9 | class Service { 10 | ,-> async fetch() { @@ -8,8 +7,7 @@ 13 | `-> } 14 | } `---- - x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in - | an inner scope + x 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 ,-[input.js:23:1] 22 | Lazy = class { 23 | ,-> static async load() { @@ -18,8 +16,8 @@ 26 | `-> } 27 | }; `---- - x Classes using custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) must be declared at the top level of the module, not inside a function. The compiler registers the class at module - | level and cannot reference a class declared in an inner scope + x Classes using custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) 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 ,-[input.js:32:1] 31 | export const factory = () => { 32 | ,-> const Point = class { diff --git a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr index c09dd5d195..716d8a80b6 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr @@ -1,5 +1,4 @@ - x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in - | an inner scope + x 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 ,-[input.js:10:1] 9 | class Service { 10 | ,-> async fetch() { @@ -8,8 +7,7 @@ 13 | `-> } 14 | } `---- - x Classes using "use step" methods must be declared at the top level of the module, not inside a function. The compiler registers the class at module level and cannot reference a class declared in - | an inner scope + x 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 ,-[input.js:23:1] 22 | Lazy = class { 23 | ,-> static async load() { @@ -18,8 +16,8 @@ 26 | `-> } 27 | }; `---- - x Classes using custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) must be declared at the top level of the module, not inside a function. The compiler registers the class at module - | level and cannot reference a class declared in an inner scope + x Classes using custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE) 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 ,-[input.js:32:1] 31 | export const factory = () => { 32 | ,-> const Point = class { diff --git a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/input.js similarity index 50% rename from packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js rename to packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/input.js index 89da57fa9b..a1813e9e69 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/input.js @@ -1,11 +1,19 @@ // Anonymous class expressions in positions that provide no name (not assigned -// to a variable or property) have nothing to derive a step/class ID from. The -// compiler used to emit `AnonymousClass.prototype[...]`, which is a guaranteed -// ReferenceError at module evaluation (vercel/workflow#3929). It must instead -// fail at compile time. +// to a variable or property) still register through the IIFE; only their IDs +// need a name, so a deterministic `AnonymousClass` is generated. The plugin +// used to emit a placeholder `AnonymousClass.prototype[...]` reference, which +// is a guaranteed ReferenceError at module evaluation (vercel/workflow#3929). import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; -// Error: class passed directly as an argument +// Not counted: an anonymous class with nothing to register does not shift +// the numbering of the ones that follow. +export const plain = class { + greet() { + return 'hi'; + } +}; + +// AnonymousClass1: class passed directly as an argument registerPlugin(class { async run() { 'use step'; @@ -13,7 +21,7 @@ registerPlugin(class { } }); -// Error: class as an array element +// AnonymousClass2: class as an array element (static step) export const handlers = [ class { static async execute() { @@ -23,7 +31,7 @@ export const handlers = [ }, ]; -// Error: class chosen by a conditional +// AnonymousClass3: class chosen by a conditional (step getter) export const Worker = process.env.FAST ? class { get status() { @@ -33,7 +41,7 @@ export const Worker = process.env.FAST } : null; -// Error: custom serialization without a derivable name +// AnonymousClass4: custom serialization only const registry = new Map([ ['point', class { static [WORKFLOW_SERIALIZE](inst) { @@ -45,7 +53,7 @@ const registry = new Map([ }], ]); -// Error: static "use workflow" method +// AnonymousClass5: static "use workflow" method useModel(class { static async orchestrate() { 'use workflow'; @@ -53,14 +61,16 @@ useModel(class { } }); -// OK: anonymous class expression without steps or serialization -export const plain = class { - greet() { - return 'hi'; +// Generated names avoid identifiers already declared in the module. +const AnonymousClass6 = 'taken'; +useModel(class { + async run() { + 'use step'; + return 'six'; } -}; +}); -// OK: naming the class is all that is needed +// A named class expression in the same position keeps its own name. registerPlugin(class NamedPlugin { async run() { 'use step'; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-step.js new file mode 100644 index 0000000000..12d9a481bc --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-step.js @@ -0,0 +1,172 @@ +// Anonymous class expressions in positions that provide no name (not assigned +// to a variable or property) still register through the IIFE; only their IDs +// need a name, so a deterministic `AnonymousClass` is generated. The plugin +// used to emit a placeholder `AnonymousClass.prototype[...]` reference, which +// is a guaranteed ReferenceError at module evaluation (vercel/workflow#3929). +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; +/**__internal_workflows{"workflows":{"input.js":{"AnonymousClass5.orchestrate":{"workflowId":"workflow//./input//AnonymousClass5.orchestrate"}}},"steps":{"input.js":{"AnonymousClass1#run":{"stepId":"step//./input//AnonymousClass1#run"},"AnonymousClass2.execute":{"stepId":"step//./input//AnonymousClass2.execute"},"AnonymousClass3#status":{"stepId":"step//./input//AnonymousClass3#status"},"AnonymousClass6$1#run":{"stepId":"step//./input//AnonymousClass6$1#run"},"NamedPlugin#run":{"stepId":"step//./input//NamedPlugin#run"}}},"classes":{"input.js":{"AnonymousClass1":{"classId":"class//./input//AnonymousClass1"},"AnonymousClass2":{"classId":"class//./input//AnonymousClass2"},"AnonymousClass3":{"classId":"class//./input//AnonymousClass3"},"AnonymousClass4":{"classId":"class//./input//AnonymousClass4"},"AnonymousClass6$1":{"classId":"class//./input//AnonymousClass6$1"},"NamedPlugin":{"classId":"class//./input//NamedPlugin"}}}}*/; +// Not counted: an anonymous class with nothing to register does not shift +// the numbering of the ones that follow. +export const plain = class { + greet() { + return 'hi'; + } +}; +// AnonymousClass1: class passed directly as an argument +registerPlugin(function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//AnonymousClass1#run", __wf_fn); + __wf_fn.stepId = "step//./input//AnonymousClass1#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass1", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass1", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { + async run() { + return 'plugin'; + } +})); +// AnonymousClass2: class as an array element (static step) +export const handlers = [ + function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.execute; + __wf_reg.set("step//./input//AnonymousClass2.execute", __wf_fn); + __wf_fn.stepId = "step//./input//AnonymousClass2.execute"; + Object.defineProperty(__wf_fn, "name", { + value: "execute", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass2", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass2", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + static async execute() { + return 'job'; + } + }) +]; +// AnonymousClass3: class chosen by a conditional (step getter) +export const Worker = process.env.FAST ? function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = Object.getOwnPropertyDescriptor(__wf_cls.prototype, "status").get; + __wf_reg.set("step//./input//AnonymousClass3#status", __wf_fn); + __wf_fn.stepId = "step//./input//AnonymousClass3#status"; + Object.defineProperty(__wf_fn, "name", { + value: "status", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass3", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass3", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { + get status() { + return 'ok'; + } +}) : null; +// AnonymousClass4: custom serialization only +const registry = new Map([ + [ + 'point', + function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass4", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass4", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + static [WORKFLOW_SERIALIZE](inst) { + return { + x: inst.x + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return { + x: data.x + }; + } + }) + ] +]); +// AnonymousClass5: static "use workflow" method +useModel(function(__wf_cls) { + __wf_cls.orchestrate.workflowId = "workflow//./input//AnonymousClass5.orchestrate"; + return __wf_cls; +}(class { + static async orchestrate() { + throw new Error("You attempted to execute workflow AnonymousClass5.orchestrate function directly. To start a workflow, use start(workflow) from workflow/api"); + } +})); +useModel(function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//AnonymousClass6$1#run", __wf_fn); + __wf_fn.stepId = "step//./input//AnonymousClass6$1#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass6$1", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass6$1", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { + async run() { + return 'six'; + } +})); +// A named class expression in the same position keeps its own name. +registerPlugin(function(__wf_cls) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; + __wf_fn = __wf_cls.prototype["run"]; + __wf_reg.set("step//./input//NamedPlugin#run", __wf_fn); + __wf_fn.stepId = "step//./input//NamedPlugin#run"; + Object.defineProperty(__wf_fn, "name", { + value: "run", + configurable: true + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//NamedPlugin", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class NamedPlugin { + async run() { + return 'named'; + } +})); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-workflow.js new file mode 100644 index 0000000000..bab6d7463a --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/output-workflow.js @@ -0,0 +1,130 @@ +// Anonymous class expressions in positions that provide no name (not assigned +// to a variable or property) still register through the IIFE; only their IDs +// need a name, so a deterministic `AnonymousClass` is generated. The plugin +// used to emit a placeholder `AnonymousClass.prototype[...]` reference, which +// is a guaranteed ReferenceError at module evaluation (vercel/workflow#3929). +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; +/**__internal_workflows{"workflows":{"input.js":{"AnonymousClass5.orchestrate":{"workflowId":"workflow//./input//AnonymousClass5.orchestrate"}}},"steps":{"input.js":{"AnonymousClass1#run":{"stepId":"step//./input//AnonymousClass1#run"},"AnonymousClass2.execute":{"stepId":"step//./input//AnonymousClass2.execute"},"AnonymousClass3#status":{"stepId":"step//./input//AnonymousClass3#status"},"AnonymousClass6$1#run":{"stepId":"step//./input//AnonymousClass6$1#run"},"NamedPlugin#run":{"stepId":"step//./input//NamedPlugin#run"}}},"classes":{"input.js":{"AnonymousClass1":{"classId":"class//./input//AnonymousClass1"},"AnonymousClass2":{"classId":"class//./input//AnonymousClass2"},"AnonymousClass3":{"classId":"class//./input//AnonymousClass3"},"AnonymousClass4":{"classId":"class//./input//AnonymousClass4"},"AnonymousClass6$1":{"classId":"class//./input//AnonymousClass6$1"},"NamedPlugin":{"classId":"class//./input//NamedPlugin"}}}}*/; +// Not counted: an anonymous class with nothing to register does not shift +// the numbering of the ones that follow. +export const plain = class { + greet() { + return 'hi'; + } +}; +// AnonymousClass1: class passed directly as an argument +registerPlugin(function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//AnonymousClass1#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass1", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass1", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { +})); +// AnonymousClass2: class as an array element (static step) +export const handlers = [ + function(__wf_cls) { + __wf_cls.execute = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//AnonymousClass2.execute"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass2", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass2", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + }) +]; +// AnonymousClass3: class chosen by a conditional (step getter) +export const Worker = process.env.FAST ? function(__wf_cls) { + var __step_AnonymousClass3$status = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//AnonymousClass3#status"); + Object.defineProperty(__wf_cls.prototype, "status", { + get () { + return __step_AnonymousClass3$status.call(this); + }, + configurable: true, + enumerable: false + }); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass3", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass3", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { +}) : null; +// AnonymousClass4: custom serialization only +const registry = new Map([ + [ + 'point', + function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass4", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass4", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; + }(class { + static [WORKFLOW_SERIALIZE](inst) { + return { + x: inst.x + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return { + x: data.x + }; + } + }) + ] +]); +// AnonymousClass5: static "use workflow" method +useModel(function(__wf_cls) { + __wf_cls.orchestrate.workflowId = "workflow//./input//AnonymousClass5.orchestrate"; + globalThis.__private_workflows.set("workflow//./input//AnonymousClass5.orchestrate", __wf_cls.orchestrate); + return __wf_cls; +}(class { + static async orchestrate() { + return 'done'; + } +})); +useModel(function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//AnonymousClass6$1#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//AnonymousClass6$1", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//AnonymousClass6$1", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class { +})); +// A named class expression in the same position keeps its own name. +registerPlugin(function(__wf_cls) { + __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//NamedPlugin#run"); + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//NamedPlugin", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class NamedPlugin { +})); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js index 02126c0251..f8ebeb7d06 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js @@ -2,6 +2,7 @@ // binding that is in scope at module level. Bundlers emit several shapes for // `class Foo {}` and all of them must resolve to the assigned binding rather // than falling back to a placeholder name that does not exist at runtime. +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; // tsdown/rolldown and esbuild emit this for classes that do not self-reference // (this is the shape shipped by @vercel/sandbox, see vercel/workflow#3929). @@ -80,6 +81,17 @@ export const handlers = { }, }; +// A binding that nothing else references is still kept: evaluating the +// initializer is what registers the class. +const Unreferenced = class { + static [WORKFLOW_SERIALIZE](inst) { + return { v: inst.v }; + } + static [WORKFLOW_DESERIALIZE](data) { + return { v: data.v }; + } +}; + // Named class expression in an arbitrary position: its own name is used. registerPlugin( class Plugin { diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js index 8605c755e0..b72c896d4e 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js @@ -1,8 +1,9 @@ -/**__internal_workflows{"steps":{"input.js":{"Alpha#run":{"stepId":"step//./input//Alpha#run"},"Beta#run":{"stepId":"step//./input//Beta#run"},"Delta#run":{"stepId":"step//./input//Delta#run"},"Epsilon.make":{"stepId":"step//./input//Epsilon.make"},"FileSystem#readFile":{"stepId":"step//./input//FileSystem#readFile"},"Gamma#run":{"stepId":"step//./input//Gamma#run"},"Job.execute":{"stepId":"step//./input//Job.execute"},"Plugin#run":{"stepId":"step//./input//Plugin#run"},"Zeta#run":{"stepId":"step//./input//Zeta#run"},"kebab-job#status":{"stepId":"step//./input//kebab-job#status"}}},"classes":{"input.js":{"Alpha":{"classId":"class//./input//Alpha"},"Beta":{"classId":"class//./input//Beta"},"Delta":{"classId":"class//./input//Delta"},"Epsilon":{"classId":"class//./input//Epsilon"},"FileSystem":{"classId":"class//./input//FileSystem"},"Gamma":{"classId":"class//./input//Gamma"},"Job":{"classId":"class//./input//Job"},"Plugin":{"classId":"class//./input//Plugin"},"Zeta":{"classId":"class//./input//Zeta"},"kebab-job":{"classId":"class//./input//kebab-job"}}}}*/; // Class expressions with "use step" methods must be registered through the // binding that is in scope at module level. Bundlers emit several shapes for // `class Foo {}` and all of them must resolve to the assigned binding rather // than falling back to a placeholder name that does not exist at runtime. +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; +/**__internal_workflows{"steps":{"input.js":{"Alpha#run":{"stepId":"step//./input//Alpha#run"},"Beta#run":{"stepId":"step//./input//Beta#run"},"Delta#run":{"stepId":"step//./input//Delta#run"},"Epsilon.make":{"stepId":"step//./input//Epsilon.make"},"FileSystem#readFile":{"stepId":"step//./input//FileSystem#readFile"},"Gamma#run":{"stepId":"step//./input//Gamma#run"},"Job.execute":{"stepId":"step//./input//Job.execute"},"Plugin#run":{"stepId":"step//./input//Plugin#run"},"Zeta#run":{"stepId":"step//./input//Zeta#run"},"kebab-job#status":{"stepId":"step//./input//kebab-job#status"}}},"classes":{"input.js":{"Alpha":{"classId":"class//./input//Alpha"},"Beta":{"classId":"class//./input//Beta"},"Delta":{"classId":"class//./input//Delta"},"Epsilon":{"classId":"class//./input//Epsilon"},"FileSystem":{"classId":"class//./input//FileSystem"},"Gamma":{"classId":"class//./input//Gamma"},"Job":{"classId":"class//./input//Job"},"Plugin":{"classId":"class//./input//Plugin"},"Unreferenced":{"classId":"class//./input//Unreferenced"},"Zeta":{"classId":"class//./input//Zeta"},"kebab-job":{"classId":"class//./input//kebab-job"}}}}*/; // tsdown/rolldown and esbuild emit this for classes that do not self-reference // (this is the shape shipped by @vercel/sandbox, see vercel/workflow#3929). var FileSystem = function(__wf_cls) { @@ -237,6 +238,30 @@ export const handlers = { } }) }; +// A binding that nothing else references is still kept: evaluating the +// initializer is what registers the class. +const Unreferenced = function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Unreferenced", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Unreferenced", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Unreferenced { + static [WORKFLOW_SERIALIZE](inst) { + return { + v: inst.v + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return { + v: data.v + }; + } +}); // Named class expression in an arbitrary position: its own name is used. registerPlugin(function(__wf_cls) { var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()), __wf_fn; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js index 3e871ed5b8..6f1a8e4e35 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js @@ -1,8 +1,9 @@ -/**__internal_workflows{"steps":{"input.js":{"Alpha#run":{"stepId":"step//./input//Alpha#run"},"Beta#run":{"stepId":"step//./input//Beta#run"},"Delta#run":{"stepId":"step//./input//Delta#run"},"Epsilon.make":{"stepId":"step//./input//Epsilon.make"},"FileSystem#readFile":{"stepId":"step//./input//FileSystem#readFile"},"Gamma#run":{"stepId":"step//./input//Gamma#run"},"Job.execute":{"stepId":"step//./input//Job.execute"},"Plugin#run":{"stepId":"step//./input//Plugin#run"},"Zeta#run":{"stepId":"step//./input//Zeta#run"},"kebab-job#status":{"stepId":"step//./input//kebab-job#status"}}},"classes":{"input.js":{"Alpha":{"classId":"class//./input//Alpha"},"Beta":{"classId":"class//./input//Beta"},"Delta":{"classId":"class//./input//Delta"},"Epsilon":{"classId":"class//./input//Epsilon"},"FileSystem":{"classId":"class//./input//FileSystem"},"Gamma":{"classId":"class//./input//Gamma"},"Job":{"classId":"class//./input//Job"},"Plugin":{"classId":"class//./input//Plugin"},"Zeta":{"classId":"class//./input//Zeta"},"kebab-job":{"classId":"class//./input//kebab-job"}}}}*/; // Class expressions with "use step" methods must be registered through the // binding that is in scope at module level. Bundlers emit several shapes for // `class Foo {}` and all of them must resolve to the assigned binding rather // than falling back to a placeholder name that does not exist at runtime. +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; +/**__internal_workflows{"steps":{"input.js":{"Alpha#run":{"stepId":"step//./input//Alpha#run"},"Beta#run":{"stepId":"step//./input//Beta#run"},"Delta#run":{"stepId":"step//./input//Delta#run"},"Epsilon.make":{"stepId":"step//./input//Epsilon.make"},"FileSystem#readFile":{"stepId":"step//./input//FileSystem#readFile"},"Gamma#run":{"stepId":"step//./input//Gamma#run"},"Job.execute":{"stepId":"step//./input//Job.execute"},"Plugin#run":{"stepId":"step//./input//Plugin#run"},"Zeta#run":{"stepId":"step//./input//Zeta#run"},"kebab-job#status":{"stepId":"step//./input//kebab-job#status"}}},"classes":{"input.js":{"Alpha":{"classId":"class//./input//Alpha"},"Beta":{"classId":"class//./input//Beta"},"Delta":{"classId":"class//./input//Delta"},"Epsilon":{"classId":"class//./input//Epsilon"},"FileSystem":{"classId":"class//./input//FileSystem"},"Gamma":{"classId":"class//./input//Gamma"},"Job":{"classId":"class//./input//Job"},"Plugin":{"classId":"class//./input//Plugin"},"Unreferenced":{"classId":"class//./input//Unreferenced"},"Zeta":{"classId":"class//./input//Zeta"},"kebab-job":{"classId":"class//./input//kebab-job"}}}}*/; // tsdown/rolldown and esbuild emit this for classes that do not self-reference // (this is the shape shipped by @vercel/sandbox, see vercel/workflow#3929). var FileSystem = function(__wf_cls) { @@ -151,6 +152,30 @@ export const handlers = { }(class { }) }; +// A binding that nothing else references is still kept: evaluating the +// initializer is what registers the class. +const Unreferenced = function(__wf_cls) { + var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map()); + __wf_cls_reg.set("class//./input//Unreferenced", __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: "class//./input//Unreferenced", + writable: false, + enumerable: false, + configurable: false + }); + return __wf_cls; +}(class Unreferenced { + static [WORKFLOW_SERIALIZE](inst) { + return { + v: inst.v + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return { + v: data.v + }; + } +}); // Named class expression in an arbitrary position: its own name is used. registerPlugin(function(__wf_cls) { __wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Plugin#run");