diff --git a/.changeset/class-expression-registration-iife.md b/.changeset/class-expression-registration-iife.md new file mode 100644 index 0000000000..0425d1a67d --- /dev/null +++ b/.changeset/class-expression-registration-iife.md @@ -0,0 +1,5 @@ +--- +'@workflow/swc-plugin': patch +--- + +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 1449f97ac9..b5404f819b 100644 --- a/packages/swc-plugin-workflow/spec.md +++ b/packages/swc-plugin-workflow/spec.md @@ -716,146 +716,91 @@ 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; - } - - static [WORKFLOW_SERIALIZE](instance) { - return { command: instance.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_DESERIALIZE](data) { - return new Bash(data.command); - } +Input (e.g., after tsdown/esbuild pre-bundling; this is the shape `@vercel/sandbox` ships): +```javascript +var FileSystem = class { + constructor(sandbox) { this.sandbox = sandbox; } + async readFile(path) { "use step"; return this.sandbox.read(path); } }; +export { FileSystem }; ``` -Output: +Output (step mode): ```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); - } -}; -(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"); +/**__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 }; ``` -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 +Output (workflow mode): +```javascript +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; } +}); +``` -For example, a class expression with step methods: +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. -Input: -```javascript -import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; +#### Class names for IDs -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 }; } -}; -``` +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: -Output (step 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"); -``` +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. -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. +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. -### Anonymous class expression name re-insertion +Classes that already have an identifier (e.g. `class _Bash { ... }`) are never renamed. -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). +#### Dead-code elimination -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. +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... }]])`). -Input (e.g., after tsup pre-bundling): -```javascript -var Shell = class { - constructor(cmd) { - this.cmd = cmd; - } +#### Nested classes are errors - static [Symbol.for('workflow-serialize')](instance) { - return { cmd: instance.cmd }; - } +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: - static [Symbol.for('workflow-deserialize')](data) { - return new Shell(data.cmd); - } -}; ``` - -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"); +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 ``` -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 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 -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 +905,7 @@ 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"` | +| 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..ad4dd0270f 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -35,6 +35,14 @@ enum WorkflowErrorKind { span: swc_core::common::Span, directive: &'static str, }, + /// 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, + feature: &'static str, + }, } #[derive(Debug, Clone)] @@ -43,6 +51,28 @@ 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, + /// 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, +} + /// 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 +144,13 @@ fn emit_error(error: WorkflowErrorKind) { ) }, ), + WorkflowErrorKind::NestedClass { span, feature } => ( + span, + 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 + ), + ), }; HANDLER.with(|handler| handler.struct_span_err(span, &msg).emit()); @@ -403,6 +440,25 @@ 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 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)>, @@ -444,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 @@ -1563,7 +1625,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 +1909,10 @@ 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_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(), @@ -1859,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(), } } @@ -1881,11 +1950,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 +3278,92 @@ impl StepTransform { has_serialize && has_deserialize } + /// 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 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::NestedClass { span, feature }); + } + // Only report once per class. + self.current_class_is_nested = false; + true + } + + /// 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. 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( + &mut self, + class_expr: &ClassExpr, + binding_name: Option, + ) -> Option { + binding_name + .or_else(|| class_expr.ident.as_ref().map(|i| i.sym.to_string())) + .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. + 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, - }; + })), + }); + + 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, + }))) + } - // __wf_reg.set(__wf_id, __wf_cls); - let set_call = Stmt::Expr(ExprStmt { + /// `.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 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)), + ], + ), + ] + } - // The IIFE: (function(__wf_cls, __wf_id) { ... })(ClassName, /* generated class ID string */); + /// `(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 { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: ident("__wf_sym"), - 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/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 { + // var __wf_ctx = globalThis[Symbol.for("WORKFLOW_STEP_CONTEXT_STORAGE")] + let ctx_decl = VarDeclarator { span: DUMMY_SP, name: Pat::Ident(BindingIdent { - id: ident("__wf_reg"), + id: ident("__wf_ctx"), type_ann: None, }), - init: Some(Box::new(Expr::Bin(BinExpr { + init: Some(Box::new(Expr::Member(MemberExpr { span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: global_sym_access.clone(), - right: Box::new(Expr::Paren(ParenExpr { + obj: ident_expr("globalThis"), + prop: MemberProp::Computed(ComputedPropName { 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 { @@ -4450,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) { @@ -4482,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 { @@ -5109,836 +5066,1004 @@ 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 { - span: DUMMY_SP, - obj: 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( - "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, - }))), - }, - ], - type_args: None, - })), - prop: MemberProp::Ident(IdentName::new("get".into(), DUMMY_SP)), - }); + 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 registration_call = self.create_inline_step_registration( - &step_id, - getter_ref, - &getter_name, - ); - module.body.push(ModuleItem::Stmt(registration_call)); - } + let cls = Self::ident_expr(class_ref); + let proto = || Self::member(Self::ident_expr(class_ref), "prototype"); - // 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)); + 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, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: AssignTarget::Simple(SimpleAssignTarget::Ident( + BindingIdent { + id: Self::ident("__wf_fn"), + type_ann: None, + }, + )), + right: fn_ref, + })), + })); + 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, + } - // 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 + 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, + name_to_define, + .. + } = 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"); + }; + 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() { + // `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)); + } + } + + 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; - } - } - - // 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(..) - { - // 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 { - 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, - }); - - 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::Ident(Ident::new( - class_name.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)); - } + // Process nested step functions FIRST (they typically appear earlier in source) + let nested_functions: Vec<_> = self.nested_step_functions.drain(..).collect(); - // 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(..) + for ( + fn_name, + mut fn_expr, + span, + closure_vars, + was_arrow, + parent_workflow_name, + references_lexical_this, + ) in nested_functions { - // 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(), - 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, - }); + // 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: 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 { + // Create destructuring statement using inline IIFE: + // const { var1, var2 } = (function() { ... })(); + let closure_destructure = + Stmt::Decl(Decl::Var(Box::new(VarDecl { 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 { + ctxt: SyntaxContext::empty(), + kind: VarDeclKind::Const, + decls: vec![VarDeclarator { span: DUMMY_SP, - expr: Box::new(Expr::Lit(Lit::Str(Str { + name: Pat::Object(ObjectPat { span: DUMMY_SP, - value: method_name.into(), - raw: None, - }))), - }), - }, - )), - op: AssignOp::Assign, - right: Box::new(proxy_expr), - })), - }); - module.body.push(ModuleItem::Stmt(assignment)); - } + 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, + }))); - // 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 - ); + // Prepend to function body + body.stmts.insert(0, closure_destructure); + } + } - // 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 { + // 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, - 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( + hoisted_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::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, + }))) }; - 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, - }))), - }))), - ], - }); + // Insert at current position and increment for next iteration + module.body.insert(current_insert_pos, hoisted_decl); + current_insert_pos += 1; - let define_property_call = Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Call(CallExpr { + // 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; + } + + // 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(); + + // 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, ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + kind: VarDeclKind::Var, + decls: vec![VarDeclarator { 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)); + 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, + })))); + + // 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; + } + + // 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. + + // 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 +6072,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 +6081,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 +6235,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 +7854,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 +8122,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 +8210,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 +8268,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 +8296,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_is_nested = std::mem::replace(&mut self.current_class_is_nested, false); - // 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 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 a registration that cannot be resolved. + self.current_class_is_nested = true; + if self.has_custom_serialization_methods(&class_decl.class) { + self.report_nested_class( + class_decl.class.span, + "custom serialization (WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE)", + ); + } } // Visit the class body (this populates static_step_methods_to_strip) @@ -8396,91 +8398,106 @@ impl VisitMut for StepTransform { // Restore previous class name self.current_class_name = old_class_name; + self.current_class_is_nested = old_is_nested; } // 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`), 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, 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(); - self.current_class_name = Some(tracked_class_name.clone()); + let old_is_nested = std::mem::replace(&mut self.current_class_is_nested, false); + + 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); 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_nested_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. + // 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); // 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 +8534,18 @@ 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, + name_to_define, + has_custom_serialization: has_serde, + }); + // Restore previous class name self.current_class_name = old_class_name; + self.current_class_is_nested = old_is_nested; } // Handle class methods @@ -8556,6 +8583,10 @@ impl VisitMut for StepTransform { let class_name = match &self.current_class_name { Some(name) => name.clone(), None => { + // 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_nested_class(method.span, "\"use step\" getters"); method.visit_mut_children_with(self); return; } @@ -8660,7 +8691,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 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,7 +8788,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 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" + } else { + "\"use step\" methods" + }, + ); method.visit_mut_children_with(self); return; } @@ -8890,8 +8934,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 +8983,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 +9385,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/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..716d8a80b6 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr @@ -0,0 +1,32 @@ + 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() { + 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. 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() { + 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. 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 { + 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..716d8a80b6 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr @@ -0,0 +1,32 @@ + 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() { + 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. 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() { + 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. 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 { + 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-anonymous-generated-names/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/input.js new file mode 100644 index 0000000000..a1813e9e69 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-anonymous-generated-names/input.js @@ -0,0 +1,79 @@ +// 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'; + +// 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'; + return 'plugin'; + } +}); + +// AnonymousClass2: class as an array element (static step) +export const handlers = [ + class { + static async execute() { + 'use step'; + return 'job'; + } + }, +]; + +// AnonymousClass3: class chosen by a conditional (step getter) +export const Worker = process.env.FAST + ? class { + get status() { + 'use step'; + return 'ok'; + } + } + : null; + +// AnonymousClass4: custom serialization only +const registry = new Map([ + ['point', class { + static [WORKFLOW_SERIALIZE](inst) { + return { x: inst.x }; + } + static [WORKFLOW_DESERIALIZE](data) { + return { x: data.x }; + } + }], +]); + +// AnonymousClass5: static "use workflow" method +useModel(class { + static async orchestrate() { + 'use workflow'; + return 'done'; + } +}); + +// Generated names avoid identifiers already declared in the module. +const AnonymousClass6 = 'taken'; +useModel(class { + async run() { + 'use step'; + return 'six'; + } +}); + +// A named class expression in the same position keeps its own name. +registerPlugin(class NamedPlugin { + async run() { + 'use step'; + return 'named'; + } +}); 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-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..f8ebeb7d06 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js @@ -0,0 +1,105 @@ +// 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'; + +// 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'; + } + }, +}; + +// 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 { + 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..b72c896d4e --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js @@ -0,0 +1,289 @@ +// 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) { + 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'; + } + }) +}; +// 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; + __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..6f1a8e4e35 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js @@ -0,0 +1,193 @@ +// 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) { + __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 { + }) +}; +// 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"); + 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 };