From 0cbd515f55194e35f1baabaefb131737bf660fb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 21:52:05 +0000 Subject: [PATCH] [swc-plugin] Preserve imports referenced by hoisted nested steps (#1944) * [swc-plugin] Preserve imports referenced by hoisted nested steps Dead-code elimination ran before nested step functions were hoisted out of workflow bodies, so imports referenced only by hoisted step bodies were incorrectly stripped from the step bundle, causing a ReferenceError at runtime. Move DCE to run after hoisting in visit_mut_program. * [swc-plugin] Namespace nested step IDs under non-exported workflow functions Anonymous steps nested inside callback properties of a non-exported workflow function were registered with an unnamespaced step ID in step mode while the workflow-mode proxy looked them up under the workflow function name, causing a runtime 'step not found' failure. Set current_workflow_function_name in visit_mut_fn_decl for non-exported workflow functions to match the behavior in visit_mut_export_decl. Also clarify the fixture comment to distinguish step-mode and workflow-mode behavior per reviewer feedback. * [swc-plugin] Namespace nested step IDs across all workflow declaration shapes Extends the previous fix to cover all three non-exported workflow declaration forms (async function decl, const arrow, const fn-expr) by visiting the workflow body with workflow context before replacing it, and corrects the __internal_workflows manifest comment to report the same prefixed step IDs that are registered at runtime and looked up by the workflow-mode WORKFLOW_USE_STEP proxy. Adds a dedicated regression fixture covering all three shapes. Signed-off-by: Nathan Rajlich --- .../preserve-imports-used-by-hoisted-steps.md | 9 + .../swc-plugin-workflow/transform/src/lib.rs | 228 +++++++++++++----- .../output-step.js | 2 +- .../output-workflow.js | 2 +- .../output-step.js | 2 +- .../output-workflow.js | 2 +- .../factory-with-step-method/output-client.js | 1 + .../output-step.js | 2 +- .../nested-step-in-workflow/output-step.js | 2 +- .../output-workflow.js | 2 +- .../input.js | 35 +++ .../output-client.js | 18 ++ .../output-step.js | 28 +++ .../output-workflow.js | 31 +++ .../nested-step-with-closure/output-step.js | 2 +- .../output-workflow.js | 2 +- .../output-step.js | 2 +- .../output-workflow.js | 2 +- .../non-exported-workflow-shapes/input.js | 38 +++ .../output-client.js | 24 ++ .../output-step.js | 31 +++ .../output-workflow.js | 45 ++++ 22 files changed, 437 insertions(+), 73 deletions(-) create mode 100644 .changeset/preserve-imports-used-by-hoisted-steps.md create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-workflow.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-workflow.js diff --git a/.changeset/preserve-imports-used-by-hoisted-steps.md b/.changeset/preserve-imports-used-by-hoisted-steps.md new file mode 100644 index 0000000000..74e5903488 --- /dev/null +++ b/.changeset/preserve-imports-used-by-hoisted-steps.md @@ -0,0 +1,9 @@ +--- +"@workflow/swc-plugin": patch +--- + +Fix three bugs affecting nested step functions that get hoisted out of an enclosing function (workflows in any declaration form, plus regular factory-style functions returning objects with step methods): + +1. Module-level imports referenced only by hoisted step bodies were stripped by dead-code elimination, causing a `ReferenceError` at runtime. +2. The step ID generated for nested anonymous steps inside a non-exported workflow declared as `const foo = async () => {}` or `const foo = async function() {}` was not namespaced under the workflow name in step mode, so it did not match the ID looked up by the workflow-mode proxy and caused a runtime "step not found" failure. Steps inside `async function foo()` workflows were already namespaced correctly; this brings the const-arrow and const-fn-expression forms into agreement. +3. The `__internal_workflows` manifest comment reported nested anonymous step IDs without the workflow-name prefix even though the runtime registration and proxy lookup used the prefixed form, so downstream tooling (e.g. builders consuming the manifest) saw the wrong step ID. diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index 97fec5125f..9b71323710 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -356,6 +356,15 @@ pub struct StepTransform { // Track object properties that need to be converted to initializer calls in workflow mode // (parent_var_name, prop_name, step_id) object_property_workflow_conversions: Vec<(String, String, String)>, + // Map from a step's bare name (e.g. "_anonymousStep0") to the full + // namespaced step name used to compute its registered step ID + // (e.g. "myWorkflow/_anonymousStep0"). Steps not present in this map + // use their bare name (which produces the same ID as the registered + // one). Populated when a nested step is hoisted out of a workflow + // function so that the `__internal_workflows` manifest comment matches + // the actual runtime step ID used by both the step-mode registration + // and the workflow-mode `WORKFLOW_USE_STEP` proxy lookup. + nested_step_full_names: HashMap, // Current context: variable name being processed when visiting object properties #[allow(dead_code)] current_var_context: Option, @@ -1284,13 +1293,12 @@ impl StepTransform { } TransformMode::Workflow => { // Include parent workflow name in step ID - let step_fn_name = if let Some(parent) = - &self.current_workflow_function_name - { - format!("{}/{}", parent, fn_name) - } else { - fn_name.clone() - }; + let parent_workflow = self + .current_workflow_function_name + .clone() + .unwrap_or_default(); + let step_fn_name = + self.record_nested_step_name(&fn_name, &parent_workflow); let step_id = self.create_id( Some(&step_fn_name), fn_decl.function.span, @@ -1551,6 +1559,7 @@ impl StepTransform { nested_step_functions: Vec::new(), anonymous_fn_counter: 0, object_property_workflow_conversions: Vec::new(), + nested_step_full_names: HashMap::new(), current_var_context: None, module_imports: HashSet::new(), current_class_name: None, @@ -1575,6 +1584,28 @@ impl StepTransform { // Create an identifier by combining module path and function name or line number // with appropriate prefix based on function type + /// Compute the namespaced step name for a nested step extracted from a + /// workflow function, and record the mapping so that the + /// `__internal_workflows` manifest emits the same step ID that is + /// registered at runtime in step mode and looked up by the + /// `WORKFLOW_USE_STEP` proxy in workflow mode. + /// + /// `fn_name` is the bare step name (e.g. `_anonymousStep0`). + /// `parent_workflow_name` is the enclosing workflow function's name + /// (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 { + if parent_workflow_name.is_empty() { + fn_name.to_string() + } else { + let full = format!("{}/{}", parent_workflow_name, fn_name); + self.nested_step_full_names + .insert(fn_name.to_string(), full.clone()); + full + } + } + fn create_id( &self, fn_name: Option<&str>, @@ -3918,7 +3949,18 @@ impl StepTransform { .step_function_names .iter() .map(|fn_name| { - let step_id = self.create_id(Some(fn_name), DUMMY_SP, false); + // If this step was hoisted out of a workflow function it + // is registered (and looked up) under a namespaced name + // like "myWorkflow/_anonymousStep0". The manifest must + // report the same step ID so downstream tooling (e.g. + // builders consuming `__internal_workflows`) sees the + // correct ID. + let id_name = self + .nested_step_full_names + .get(fn_name) + .map(|s| s.as_str()) + .unwrap_or(fn_name.as_str()); + let step_id = self.create_id(Some(id_name), DUMMY_SP, false); format!("\"{}\":{{\"stepId\":\"{}\"}}", fn_name, step_id) }) .collect(); @@ -4404,11 +4446,8 @@ impl VisitMut for StepTransform { current_insert_pos += 1; // Create a registration call or stepId assignment with parent workflow name in the step ID - let step_fn_name = if parent_workflow_name.is_empty() { - fn_name.clone() - } else { - format!("{}/{}", parent_workflow_name, fn_name) - }; + 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); if self.mode == TransformMode::Client { @@ -5018,6 +5057,14 @@ impl VisitMut for StepTransform { // Note: workflowId assignments are now handled in visit_mut_module_items + // Run dead-code elimination after nested step functions have + // been hoisted to the module top level. Running DCE earlier + // (e.g. inside `visit_mut_module_items`) would incorrectly + // strip imports referenced only by hoisted step bodies — such + // as a step nested inside a workflow function that references + // a module-level import. + self.remove_dead_code(&mut module.body); + // Add metadata comment at the beginning of the file let metadata_comment = self.generate_metadata_comment(); if !metadata_comment.is_empty() { @@ -6112,8 +6159,10 @@ impl VisitMut for StepTransform { } } - // Perform dead code elimination in workflow and client mode - self.remove_dead_code(items); + // Note: dead-code elimination runs once at the end of + // `visit_mut_program` (after nested step functions have been hoisted + // to the module top level). Running it here would prematurely strip + // imports referenced only by hoisted step bodies. } fn visit_mut_fn_decl(&mut self, fn_decl: &mut FnDecl) { @@ -6181,12 +6230,24 @@ impl VisitMut for StepTransform { // Track parent function name for nested step hoisting let old_parent_name = self.current_parent_function_name.clone(); - self.current_parent_function_name = Some(fn_name); + self.current_parent_function_name = Some(fn_name.clone()); + + // For non-exported workflow functions, set current_workflow_function_name + // so that nested step IDs are namespaced under the workflow name. Exported + // workflow functions set this in `visit_mut_export_decl`. Without this, + // step IDs registered in step mode (via `visit_mut_object_lit`) would not + // match the IDs looked up in workflow mode (via `WORKFLOW_USE_STEP`), + // producing a runtime "step not found" failure. + let old_workflow_name = self.current_workflow_function_name.clone(); + if self.workflow_function_names.contains(&fn_name) { + self.current_workflow_function_name = Some(fn_name); + } fn_decl.visit_mut_children_with(self); // Restore parent function name self.current_parent_function_name = old_parent_name; + self.current_workflow_function_name = old_workflow_name; } fn visit_mut_stmt(&mut self, stmt: &mut Stmt) { @@ -6892,6 +6953,29 @@ impl VisitMut for StepTransform { // It's valid - proceed with transformation self.workflow_function_names.insert(name.clone()); + // Visit the function body with workflow context so that + // any nested steps (including those inside callback + // object literals) are extracted and namespaced under + // this workflow's name. This must happen before the + // body is replaced (in step mode) or the directive is + // removed (in workflow mode). + 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_in_module = self.in_module_level; + self.in_workflow_function = true; + self.current_workflow_function_name = Some(name.clone()); + self.current_parent_function_name = Some(name.clone()); + self.in_module_level = false; + if let Some(body) = &mut fn_expr.function.body { + body.visit_mut_with(self); + } + self.in_workflow_function = old_in_workflow; + self.current_workflow_function_name = old_workflow_name; + self.current_parent_function_name = old_parent_name; + self.in_module_level = old_in_module; + match self.mode { TransformMode::Step => { // In step mode, transform workflow function with throw error @@ -7085,13 +7169,14 @@ impl VisitMut for StepTransform { TransformMode::Workflow => { // Replace with proxy reference (not a function call) // Include parent workflow name in step ID - let step_fn_name = if let Some(parent) = - &self.current_workflow_function_name - { - format!("{}/{}", parent, name) - } else { - name.clone() - }; + let parent_workflow = self + .current_workflow_function_name + .clone() + .unwrap_or_default(); + let step_fn_name = self.record_nested_step_name( + &name, + &parent_workflow, + ); let step_id = self.create_id( Some(&step_fn_name), arrow_expr.span, @@ -7185,6 +7270,28 @@ impl VisitMut for StepTransform { // It's valid - proceed with transformation self.workflow_function_names.insert(name.clone()); + // Visit the arrow body with workflow context so that any + // nested steps (including those inside callback object + // literals like `tools: () => ({ exec: async () => { + // 'use step'; ... } })`) are extracted and namespaced + // under this workflow's name. This must happen before + // the body is replaced (in step mode) or the directive + // is removed (in workflow mode). + 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_in_module = self.in_module_level; + self.in_workflow_function = true; + self.current_workflow_function_name = Some(name.clone()); + self.current_parent_function_name = Some(name.clone()); + self.in_module_level = false; + arrow_expr.body.visit_mut_with(self); + self.in_workflow_function = old_in_workflow; + self.current_workflow_function_name = old_workflow_name; + self.current_parent_function_name = old_parent_name; + self.in_module_level = old_in_module; + match self.mode { TransformMode::Step => { // In step mode, transform workflow arrow function with throw error @@ -7894,16 +8001,12 @@ impl VisitMut for StepTransform { TransformMode::Workflow => { // Replace with proxy reference // Use current_parent_function_name to match step mode's ID generation + let parent_workflow = self + .current_parent_function_name + .clone() + .unwrap_or_default(); let step_fn_name = - if let Some(parent) = &self.current_parent_function_name { - if !parent.is_empty() { - format!("{}/{}", parent, name) - } else { - name.clone() - } - } else { - name.clone() - }; + self.record_nested_step_name(&name, &parent_workflow); let step_id = self.create_id( Some(&step_fn_name), fn_expr.function.span, @@ -8010,16 +8113,12 @@ impl VisitMut for StepTransform { TransformMode::Workflow => { // Replace with proxy reference // Use current_parent_function_name to match step mode's ID generation + let parent_workflow = self + .current_parent_function_name + .clone() + .unwrap_or_default(); let step_fn_name = - if let Some(parent) = &self.current_parent_function_name { - if !parent.is_empty() { - format!("{}/{}", parent, name) - } else { - name.clone() - } - } else { - name.clone() - }; + self.record_nested_step_name(&name, &parent_workflow); let step_id = self.create_id(Some(&step_fn_name), arrow_expr.span, false); @@ -8612,13 +8711,15 @@ impl VisitMut for StepTransform { &mut arrow_expr.body, ); // Include parent workflow name in step ID - let step_fn_name = if let Some(parent) = - &self.current_workflow_function_name - { - format!("{}/{}", parent, generated_name) - } else { - generated_name.clone() - }; + let parent_workflow = self + .current_workflow_function_name + .clone() + .unwrap_or_default(); + let step_fn_name = self + .record_nested_step_name( + &generated_name, + &parent_workflow, + ); let step_id = self.create_id( Some(&step_fn_name), arrow_expr.span, @@ -8702,13 +8803,15 @@ impl VisitMut for StepTransform { &mut fn_expr.function.body, ); // Include parent workflow name in step ID - let step_fn_name = if let Some(parent) = - &self.current_workflow_function_name - { - format!("{}/{}", parent, generated_name) - } else { - generated_name.clone() - }; + let parent_workflow = self + .current_workflow_function_name + .clone() + .unwrap_or_default(); + let step_fn_name = self + .record_nested_step_name( + &generated_name, + &parent_workflow, + ); let step_id = self.create_id( Some(&step_fn_name), fn_expr.function.span, @@ -8800,13 +8903,14 @@ impl VisitMut for StepTransform { &mut method_prop.function.body, ); // Include parent workflow name in step ID - let step_fn_name = if let Some(parent) = - &self.current_workflow_function_name - { - format!("{}/{}", parent, generated_name) - } else { - generated_name.clone() - }; + let parent_workflow = self + .current_workflow_function_name + .clone() + .unwrap_or_default(); + let step_fn_name = self.record_nested_step_name( + &generated_name, + &parent_workflow, + ); let step_id = self.create_id( Some(&step_fn_name), method_prop.function.span, diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-step.js index eb51d3cdf2..eefa9b17ae 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-step.js @@ -2,7 +2,7 @@ import { __private_getClosureVars, registerStepFunction } from "workflow/interna // https://github.com/vercel/workflow/issues/1365 import { MockLanguageModelV3 } from 'ai/test'; import { xai as xaiProvider } from '@ai-sdk/xai'; -/**__internal_workflows{"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"},"_anonymousStep10":{"stepId":"step//./input//_anonymousStep10"},"_anonymousStep11":{"stepId":"step//./input//_anonymousStep11"},"_anonymousStep12":{"stepId":"step//./input//_anonymousStep12"},"_anonymousStep13":{"stepId":"step//./input//_anonymousStep13"},"_anonymousStep14":{"stepId":"step//./input//_anonymousStep14"},"_anonymousStep15":{"stepId":"step//./input//_anonymousStep15"},"_anonymousStep16":{"stepId":"step//./input//_anonymousStep16"},"_anonymousStep17":{"stepId":"step//./input//_anonymousStep17"},"_anonymousStep2":{"stepId":"step//./input//_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//_anonymousStep5"},"_anonymousStep6":{"stepId":"step//./input//_anonymousStep6"},"_anonymousStep7":{"stepId":"step//./input//_anonymousStep7"},"_anonymousStep8":{"stepId":"step//./input//_anonymousStep8"},"_anonymousStep9":{"stepId":"step//./input//_anonymousStep9"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//mockModel/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//xai/_anonymousStep1"},"_anonymousStep10":{"stepId":"step//./input//withForIn/_anonymousStep10"},"_anonymousStep11":{"stepId":"step//./input//withDoWhile/_anonymousStep11"},"_anonymousStep12":{"stepId":"step//./input//withShorthandProps/_anonymousStep12"},"_anonymousStep13":{"stepId":"step//./input//withComputedKey/_anonymousStep13"},"_anonymousStep14":{"stepId":"step//./input//mockTextModel/_anonymousStep14"},"_anonymousStep15":{"stepId":"step//./input//withClassExpr/_anonymousStep15"},"_anonymousStep16":{"stepId":"step//./input//withClassSuper/_anonymousStep16"},"_anonymousStep17":{"stepId":"step//./input//withClassProp/_anonymousStep17"},"_anonymousStep2":{"stepId":"step//./input//mockModelWrapped/_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//configuredStep/_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//withOptionalChaining/_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//withSequenceExpr/_anonymousStep5"},"_anonymousStep6":{"stepId":"step//./input//withTryCatch/_anonymousStep6"},"_anonymousStep7":{"stepId":"step//./input//withThrow/_anonymousStep7"},"_anonymousStep8":{"stepId":"step//./input//withSwitch/_anonymousStep8"},"_anonymousStep9":{"stepId":"step//./input//withForOf/_anonymousStep9"}}}}*/; var mockModel$_anonymousStep0 = async ()=>{ const { args } = __private_getClosureVars(); return new MockLanguageModelV3(...args); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-workflow.js index e1f8eae803..c7a2668cf9 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-workflow.js @@ -1,5 +1,5 @@ // https://github.com/vercel/workflow/issues/1365 -/**__internal_workflows{"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"},"_anonymousStep10":{"stepId":"step//./input//_anonymousStep10"},"_anonymousStep11":{"stepId":"step//./input//_anonymousStep11"},"_anonymousStep12":{"stepId":"step//./input//_anonymousStep12"},"_anonymousStep13":{"stepId":"step//./input//_anonymousStep13"},"_anonymousStep14":{"stepId":"step//./input//_anonymousStep14"},"_anonymousStep15":{"stepId":"step//./input//_anonymousStep15"},"_anonymousStep16":{"stepId":"step//./input//_anonymousStep16"},"_anonymousStep17":{"stepId":"step//./input//_anonymousStep17"},"_anonymousStep2":{"stepId":"step//./input//_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//_anonymousStep5"},"_anonymousStep6":{"stepId":"step//./input//_anonymousStep6"},"_anonymousStep7":{"stepId":"step//./input//_anonymousStep7"},"_anonymousStep8":{"stepId":"step//./input//_anonymousStep8"},"_anonymousStep9":{"stepId":"step//./input//_anonymousStep9"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//mockModel/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//xai/_anonymousStep1"},"_anonymousStep10":{"stepId":"step//./input//withForIn/_anonymousStep10"},"_anonymousStep11":{"stepId":"step//./input//withDoWhile/_anonymousStep11"},"_anonymousStep12":{"stepId":"step//./input//withShorthandProps/_anonymousStep12"},"_anonymousStep13":{"stepId":"step//./input//withComputedKey/_anonymousStep13"},"_anonymousStep14":{"stepId":"step//./input//mockTextModel/_anonymousStep14"},"_anonymousStep15":{"stepId":"step//./input//withClassExpr/_anonymousStep15"},"_anonymousStep16":{"stepId":"step//./input//withClassSuper/_anonymousStep16"},"_anonymousStep17":{"stepId":"step//./input//withClassProp/_anonymousStep17"},"_anonymousStep2":{"stepId":"step//./input//mockModelWrapped/_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//configuredStep/_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//withOptionalChaining/_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//withSequenceExpr/_anonymousStep5"},"_anonymousStep6":{"stepId":"step//./input//withTryCatch/_anonymousStep6"},"_anonymousStep7":{"stepId":"step//./input//withThrow/_anonymousStep7"},"_anonymousStep8":{"stepId":"step//./input//withSwitch/_anonymousStep8"},"_anonymousStep9":{"stepId":"step//./input//withForOf/_anonymousStep9"}}}}*/; // Bug 1: `new` expressions should have their arguments captured as closure vars export function mockModel(...args) { return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//mockModel/_anonymousStep0", ()=>({ diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-step.js index 95aaeb47c5..dab2cda578 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-step.js @@ -1,5 +1,5 @@ import { __private_getClosureVars, registerStepFunction } from "workflow/internal/private"; -/**__internal_workflows{"steps":{"input.ts":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//_anonymousStep5"}}}}*/; +/**__internal_workflows{"steps":{"input.ts":{"_anonymousStep0":{"stepId":"step//./input//withTsAs/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//withTsSatisfies/_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//withTsNonNull/_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//withTsTypeAssertion/_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//withTsConstAssertion/_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//withGenericCall/_anonymousStep5"}}}}*/; var withTsAs$_anonymousStep0 = async ()=>{ const { config } = __private_getClosureVars(); return config as Config.timeout; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-workflow.js index 7d0744e605..9f46531119 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-workflow.js @@ -1,4 +1,4 @@ -/**__internal_workflows{"steps":{"input.ts":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//_anonymousStep5"}}}}*/; +/**__internal_workflows{"steps":{"input.ts":{"_anonymousStep0":{"stepId":"step//./input//withTsAs/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//withTsSatisfies/_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//withTsNonNull/_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//withTsTypeAssertion/_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//withTsConstAssertion/_anonymousStep4"},"_anonymousStep5":{"stepId":"step//./input//withGenericCall/_anonymousStep5"}}}}*/; // TypeScript expression wrappers should not prevent closure variable detection. // The plugin must traverse through `as`, `satisfies`, `!`, type assertions, // const assertions, and instantiation expressions to reach the inner expression. diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/factory-with-step-method/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/factory-with-step-method/output-client.js index 04764550df..0580b7c0ea 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/factory-with-step-method/output-client.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/factory-with-step-method/output-client.js @@ -1,3 +1,4 @@ +import fs from 'fs/promises'; /**__internal_workflows{"steps":{"input.js":{"myFactory/myStep":{"stepId":"step//./input//myFactory/myStep"}}}}*/; var myFactory$myStep = async function() { await fs.mkdir('test'); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js index 95212c2bee..bf1335e279 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js @@ -1,6 +1,6 @@ import { registerStepFunction } from "workflow/internal/private"; import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; -/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//./input//Service#process"},"helper":{"stepId":"step//./input//helper"}}},"classes":{"input.js":{"Service":{"classId":"class//./input//Service"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//./input//Service#process"},"helper":{"stepId":"step//./input//Service$process/helper"}}},"classes":{"input.js":{"Service":{"classId":"class//./input//Service"}}}}*/; var Service$process$helper = async (x)=>x * 2; export class Service { static [WORKFLOW_SERIALIZE](instance) { diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-step.js index 89feb33f1d..556b52aa16 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-step.js @@ -1,5 +1,5 @@ import { registerStepFunction } from "workflow/internal/private"; -/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//./input//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//./input//arrowStep"},"helpers/objectStep":{"stepId":"step//./input//example/helpers/objectStep"},"letArrowStep":{"stepId":"step//./input//letArrowStep"},"step":{"stepId":"step//./input//step"},"varArrowStep":{"stepId":"step//./input//varArrowStep"}}}}*/; +/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//./input//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//./input//example/arrowStep"},"helpers/objectStep":{"stepId":"step//./input//example/helpers/objectStep"},"letArrowStep":{"stepId":"step//./input//example/letArrowStep"},"step":{"stepId":"step//./input//example/step"},"varArrowStep":{"stepId":"step//./input//example/varArrowStep"}}}}*/; // Function declaration step async function example$step(a, b) { return a + b; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-workflow.js index d2f951f77a..68d9e64d68 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-in-workflow/output-workflow.js @@ -1,4 +1,4 @@ -/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//./input//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//./input//arrowStep"},"helpers/objectStep":{"stepId":"step//./input//example/helpers/objectStep"},"letArrowStep":{"stepId":"step//./input//letArrowStep"},"step":{"stepId":"step//./input//step"},"varArrowStep":{"stepId":"step//./input//varArrowStep"}}}}*/; +/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//./input//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//./input//example/arrowStep"},"helpers/objectStep":{"stepId":"step//./input//example/helpers/objectStep"},"letArrowStep":{"stepId":"step//./input//example/letArrowStep"},"step":{"stepId":"step//./input//example/step"},"varArrowStep":{"stepId":"step//./input//example/varArrowStep"}}}}*/; export async function example(a, b) { var step = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//example/step"); // Arrow function with const diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/input.js new file mode 100644 index 0000000000..6c25bfc5cc --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/input.js @@ -0,0 +1,35 @@ +// Regression test for two related bugs: +// +// 1. (step mode) Imports used by a step that gets hoisted out of a +// workflow body must NOT be stripped by dead-code elimination. The +// workflow body is replaced with a `throw` proxy, so any import +// referenced only by the workflow body (and not by a hoisted step) +// should still be stripped. Truly unused imports should also be +// stripped. +// 2. (cross-mode) The step ID generated for a nested anonymous step +// inside a *non-exported* workflow function must agree between step +// mode (where the step is registered) and workflow mode (where the +// step proxy looks it up). Both must namespace the step under the +// workflow function name (e.g. `step//./input//w/_anonymousStep0`). +import { db } from './db'; // step-mode: kept (used by hoisted step) +import { unused } from './unused'; // step-mode: stripped (truly unused) +import * as logger from './logger'; // step-mode: kept (used by hoisted step) +import { tool, z } from 'some-agent-lib'; // step-mode: stripped (only referenced by replaced workflow body); workflow-mode: kept + +async function w() { + 'use workflow'; + const agent = new WorkflowAgent({ + model: 'anthropic/claude-opus-4.5', + tools: () => ({ + queryDatabase: tool({ + description: 'Query the database', + inputSchema: z.object({ query: z.string() }), + execute: async (input) => { + 'use step'; + logger.info('querying', input.query); + return db.query(input.query); + }, + }), + }), + }); +} diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-client.js new file mode 100644 index 0000000000..4ea5a01b24 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-client.js @@ -0,0 +1,18 @@ +// Regression test for two related bugs: +// +// 1. (step mode) Imports used by a step that gets hoisted out of a +// workflow body must NOT be stripped by dead-code elimination. The +// workflow body is replaced with a `throw` proxy, so any import +// referenced only by the workflow body (and not by a hoisted step) +// should still be stripped. Truly unused imports should also be +// stripped. +// 2. (cross-mode) The step ID generated for a nested anonymous step +// inside a *non-exported* workflow function must agree between step +// mode (where the step is registered) and workflow mode (where the +// step proxy looks it up). Both must namespace the step under the +// workflow function name (e.g. `step//./input//w/_anonymousStep0`). +/**__internal_workflows{"workflows":{"input.js":{"w":{"workflowId":"workflow//./input//w"}}}}*/; +async function w() { + throw new Error("You attempted to execute workflow w function directly. To start a workflow, use start(w) from workflow/api"); +} +w.workflowId = "workflow//./input//w"; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-step.js new file mode 100644 index 0000000000..4f8f2857ea --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-step.js @@ -0,0 +1,28 @@ +import { registerStepFunction } from "workflow/internal/private"; +// Regression test for two related bugs: +// +// 1. (step mode) Imports used by a step that gets hoisted out of a +// workflow body must NOT be stripped by dead-code elimination. The +// workflow body is replaced with a `throw` proxy, so any import +// referenced only by the workflow body (and not by a hoisted step) +// should still be stripped. Truly unused imports should also be +// stripped. +// 2. (cross-mode) The step ID generated for a nested anonymous step +// inside a *non-exported* workflow function must agree between step +// mode (where the step is registered) and workflow mode (where the +// step proxy looks it up). Both must namespace the step under the +// workflow function name (e.g. `step//./input//w/_anonymousStep0`). +import { db } from './db'; // step-mode: kept (used by hoisted step) +import { unused } from './unused'; // step-mode: stripped (truly unused) +import * as logger from './logger'; // step-mode: kept (used by hoisted step) +import { tool, z } from 'some-agent-lib'; // step-mode: stripped (only referenced by replaced workflow body); workflow-mode: kept +/**__internal_workflows{"workflows":{"input.js":{"w":{"workflowId":"workflow//./input//w"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//w/_anonymousStep0"}}}}*/; +var w$_anonymousStep0 = async (input)=>{ + logger.info('querying', input.query); + return db.query(input.query); +}; +async function w() { + throw new Error("You attempted to execute workflow w function directly. To start a workflow, use start(w) from workflow/api"); +} +w.workflowId = "workflow//./input//w"; +registerStepFunction("step//./input//w/_anonymousStep0", w$_anonymousStep0); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-workflow.js new file mode 100644 index 0000000000..4208311bf5 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-references-module-import/output-workflow.js @@ -0,0 +1,31 @@ +// Regression test for two related bugs: +// +// 1. (step mode) Imports used by a step that gets hoisted out of a +// workflow body must NOT be stripped by dead-code elimination. The +// workflow body is replaced with a `throw` proxy, so any import +// referenced only by the workflow body (and not by a hoisted step) +// should still be stripped. Truly unused imports should also be +// stripped. +// 2. (cross-mode) The step ID generated for a nested anonymous step +// inside a *non-exported* workflow function must agree between step +// mode (where the step is registered) and workflow mode (where the +// step proxy looks it up). Both must namespace the step under the +// workflow function name (e.g. `step//./input//w/_anonymousStep0`). +import { tool, z } from 'some-agent-lib'; // step-mode: stripped (only referenced by replaced workflow body); workflow-mode: kept +/**__internal_workflows{"workflows":{"input.js":{"w":{"workflowId":"workflow//./input//w"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//w/_anonymousStep0"}}}}*/; +async function w() { + const agent = new WorkflowAgent({ + model: 'anthropic/claude-opus-4.5', + tools: ()=>({ + queryDatabase: tool({ + description: 'Query the database', + inputSchema: z.object({ + query: z.string() + }), + execute: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//w/_anonymousStep0") + }) + }) + }); +} +w.workflowId = "workflow//./input//w"; +globalThis.__private_workflows.set("workflow//./input//w", w); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-step.js index 0efac7f9d1..4579db80ef 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-step.js @@ -1,7 +1,7 @@ import { __private_getClosureVars, registerStepFunction } from "workflow/internal/private"; import { DurableAgent } from '@workflow/ai/agent'; import { gateway } from 'ai'; -/**__internal_workflows{"workflows":{"input.js":{"wflow":{"workflowId":"workflow//./input//wflow"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//_anonymousStep4"},"f":{"stepId":"step//./input//f"},"fn":{"stepId":"step//./input//fn"},"namedStepWithClosureVars":{"stepId":"step//./input//namedStepWithClosureVars"}}}}*/; +/**__internal_workflows{"workflows":{"input.js":{"wflow":{"workflowId":"workflow//./input//wflow"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//stepWrapperReturnArrowFunction/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//arrowWrapperReturnArrowFunction/_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//wflow/_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//wflow/_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//wflow/_anonymousStep4"},"f":{"stepId":"step//./input//arrowWrapperReturnNamedFunction/f"},"fn":{"stepId":"step//./input//arrowWrapperReturnNamedFunctionVar/fn"},"namedStepWithClosureVars":{"stepId":"step//./input//wflow/namedStepWithClosureVars"}}}}*/; var stepWrapperReturnArrowFunctionVar$fn = async ()=>{ const { a, b, c } = __private_getClosureVars(); return a + b + c; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-workflow.js index c3424a7a20..0fac12f288 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-step-with-closure/output-workflow.js @@ -1,5 +1,5 @@ import { DurableAgent } from '@workflow/ai/agent'; -/**__internal_workflows{"workflows":{"input.js":{"wflow":{"workflowId":"workflow//./input//wflow"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//_anonymousStep4"},"f":{"stepId":"step//./input//f"},"fn":{"stepId":"step//./input//fn"},"namedStepWithClosureVars":{"stepId":"step//./input//namedStepWithClosureVars"}}}}*/; +/**__internal_workflows{"workflows":{"input.js":{"wflow":{"workflowId":"workflow//./input//wflow"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//stepWrapperReturnArrowFunction/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//arrowWrapperReturnArrowFunction/_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//wflow/_anonymousStep2"},"_anonymousStep3":{"stepId":"step//./input//wflow/_anonymousStep3"},"_anonymousStep4":{"stepId":"step//./input//wflow/_anonymousStep4"},"f":{"stepId":"step//./input//arrowWrapperReturnNamedFunction/f"},"fn":{"stepId":"step//./input//fn"},"namedStepWithClosureVars":{"stepId":"step//./input//wflow/namedStepWithClosureVars"}}}}*/; function stepWrapperReturnArrowFunctionVar(a, b, c) { const fn = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//fn", ()=>({ a, diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-step.js index ee7abe703a..b1cd2aa2cd 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-step.js @@ -2,7 +2,7 @@ import { registerStepFunction } from "workflow/internal/private"; import { DurableAgent } from '@workflow/ai/agent'; import { gateway, tool } from 'ai'; import * as z from 'zod'; -/**__internal_workflows{"workflows":{"input.js":{"test":{"workflowId":"workflow//./input//test"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"}}}}*/; +/**__internal_workflows{"workflows":{"input.js":{"test":{"workflowId":"workflow//./input//test"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//test/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//test/_anonymousStep1"}}}}*/; var test$_anonymousStep0 = async ()=>gateway('openai/gpt-5'); var test$_anonymousStep1 = async ({ location })=>`Weather in ${location}: Sunny, 72°F`; export async function test() { diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-workflow.js index 9c2c754c81..a17e8971bd 100644 --- a/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/nested-steps-in-object-constructor/output-workflow.js @@ -1,7 +1,7 @@ import { DurableAgent } from '@workflow/ai/agent'; import { tool } from 'ai'; import * as z from 'zod'; -/**__internal_workflows{"workflows":{"input.js":{"test":{"workflowId":"workflow//./input//test"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"}}}}*/; +/**__internal_workflows{"workflows":{"input.js":{"test":{"workflowId":"workflow//./input//test"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//test/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//test/_anonymousStep1"}}}}*/; export async function test() { const agent = new DurableAgent({ model: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//test/_anonymousStep0"), diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/input.js new file mode 100644 index 0000000000..8044e20582 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/input.js @@ -0,0 +1,38 @@ +// Regression test: non-exported workflow functions in three different +// declaration shapes must each emit a step ID that is namespaced under +// the workflow function's name, and step mode and workflow mode must +// agree on that ID. Without the fix, only `async function fnDecl()` +// produced a namespaced ID; the `const constArrow = async () => {}` and +// `const constFnExpr = async function() {}` shapes produced bare IDs in +// step mode while workflow mode looked them up under the workflow name, +// causing a runtime "step not found" failure. + +// 1. async function declaration +async function fnDecl() { + 'use workflow'; + const agent = new WorkflowAgent({ + tools: () => ({ + a: { execute: async () => { 'use step'; return 1; } }, + }), + }); +} + +// 2. const arrow expression +const constArrow = async () => { + 'use workflow'; + const agent = new WorkflowAgent({ + tools: () => ({ + b: { execute: async () => { 'use step'; return 2; } }, + }), + }); +}; + +// 3. const function expression +const constFnExpr = async function () { + 'use workflow'; + const agent = new WorkflowAgent({ + tools: () => ({ + c: { execute: async () => { 'use step'; return 3; } }, + }), + }); +}; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-client.js new file mode 100644 index 0000000000..a455b032df --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-client.js @@ -0,0 +1,24 @@ +/**__internal_workflows{"workflows":{"input.js":{"constArrow":{"workflowId":"workflow//./input//constArrow"},"constFnExpr":{"workflowId":"workflow//./input//constFnExpr"},"fnDecl":{"workflowId":"workflow//./input//fnDecl"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//_anonymousStep1"}}}}*/; +// Regression test: non-exported workflow functions in three different +// declaration shapes must each emit a step ID that is namespaced under +// the workflow function's name, and step mode and workflow mode must +// agree on that ID. Without the fix, only `async function fnDecl()` +// produced a namespaced ID; the `const constArrow = async () => {}` and +// `const constFnExpr = async function() {}` shapes produced bare IDs in +// step mode while workflow mode looked them up under the workflow name, +// causing a runtime "step not found" failure. +// 1. async function declaration +async function fnDecl() { + throw new Error("You attempted to execute workflow fnDecl function directly. To start a workflow, use start(fnDecl) from workflow/api"); +} +fnDecl.workflowId = "workflow//./input//fnDecl"; +// 2. const arrow expression +const constArrow = async ()=>{ + throw new Error("You attempted to execute workflow constArrow function directly. To start a workflow, use start(constArrow) from workflow/api"); +}; +constArrow.workflowId = "workflow//./input//constArrow"; +// 3. const function expression +const constFnExpr = async function() { + throw new Error("You attempted to execute workflow constFnExpr function directly. To start a workflow, use start(constFnExpr) from workflow/api"); +}; +constFnExpr.workflowId = "workflow//./input//constFnExpr"; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-step.js new file mode 100644 index 0000000000..7458d3700b --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-step.js @@ -0,0 +1,31 @@ +import { registerStepFunction } from "workflow/internal/private"; +/**__internal_workflows{"workflows":{"input.js":{"constArrow":{"workflowId":"workflow//./input//constArrow"},"constFnExpr":{"workflowId":"workflow//./input//constFnExpr"},"fnDecl":{"workflowId":"workflow//./input//fnDecl"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//fnDecl/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//constArrow/_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//constFnExpr/_anonymousStep2"}}}}*/; +var fnDecl$_anonymousStep0 = async ()=>1; +var constArrow$_anonymousStep1 = async ()=>2; +var constFnExpr$_anonymousStep2 = async ()=>3; +// Regression test: non-exported workflow functions in three different +// declaration shapes must each emit a step ID that is namespaced under +// the workflow function's name, and step mode and workflow mode must +// agree on that ID. Without the fix, only `async function fnDecl()` +// produced a namespaced ID; the `const constArrow = async () => {}` and +// `const constFnExpr = async function() {}` shapes produced bare IDs in +// step mode while workflow mode looked them up under the workflow name, +// causing a runtime "step not found" failure. +// 1. async function declaration +async function fnDecl() { + throw new Error("You attempted to execute workflow fnDecl function directly. To start a workflow, use start(fnDecl) from workflow/api"); +} +fnDecl.workflowId = "workflow//./input//fnDecl"; +// 2. const arrow expression +const constArrow = async ()=>{ + throw new Error("You attempted to execute workflow constArrow function directly. To start a workflow, use start(constArrow) from workflow/api"); +}; +constArrow.workflowId = "workflow//./input//constArrow"; +// 3. const function expression +const constFnExpr = async function() { + throw new Error("You attempted to execute workflow constFnExpr function directly. To start a workflow, use start(constFnExpr) from workflow/api"); +}; +constFnExpr.workflowId = "workflow//./input//constFnExpr"; +registerStepFunction("step//./input//fnDecl/_anonymousStep0", fnDecl$_anonymousStep0); +registerStepFunction("step//./input//constArrow/_anonymousStep1", constArrow$_anonymousStep1); +registerStepFunction("step//./input//constFnExpr/_anonymousStep2", constFnExpr$_anonymousStep2); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-workflow.js new file mode 100644 index 0000000000..68b1b3a11f --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/non-exported-workflow-shapes/output-workflow.js @@ -0,0 +1,45 @@ +/**__internal_workflows{"workflows":{"input.js":{"constArrow":{"workflowId":"workflow//./input//constArrow"},"constFnExpr":{"workflowId":"workflow//./input//constFnExpr"},"fnDecl":{"workflowId":"workflow//./input//fnDecl"}}},"steps":{"input.js":{"_anonymousStep0":{"stepId":"step//./input//fnDecl/_anonymousStep0"},"_anonymousStep1":{"stepId":"step//./input//constArrow/_anonymousStep1"},"_anonymousStep2":{"stepId":"step//./input//constFnExpr/_anonymousStep2"}}}}*/; +// Regression test: non-exported workflow functions in three different +// declaration shapes must each emit a step ID that is namespaced under +// the workflow function's name, and step mode and workflow mode must +// agree on that ID. Without the fix, only `async function fnDecl()` +// produced a namespaced ID; the `const constArrow = async () => {}` and +// `const constFnExpr = async function() {}` shapes produced bare IDs in +// step mode while workflow mode looked them up under the workflow name, +// causing a runtime "step not found" failure. +// 1. async function declaration +async function fnDecl() { + const agent = new WorkflowAgent({ + tools: ()=>({ + a: { + execute: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//fnDecl/_anonymousStep0") + } + }) + }); +} +fnDecl.workflowId = "workflow//./input//fnDecl"; +globalThis.__private_workflows.set("workflow//./input//fnDecl", fnDecl); +// 2. const arrow expression +const constArrow = async ()=>{ + const agent = new WorkflowAgent({ + tools: ()=>({ + b: { + execute: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//constArrow/_anonymousStep1") + } + }) + }); +}; +constArrow.workflowId = "workflow//./input//constArrow"; +globalThis.__private_workflows.set("workflow//./input//constArrow", constArrow); +// 3. const function expression +const constFnExpr = async function() { + const agent = new WorkflowAgent({ + tools: ()=>({ + c: { + execute: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//constFnExpr/_anonymousStep2") + } + }) + }); +}; +constFnExpr.workflowId = "workflow//./input//constFnExpr"; +globalThis.__private_workflows.set("workflow//./input//constFnExpr", constFnExpr);