Skip to content

Commit 1d4f83a

Browse files
authored
[swc-plugin] Preserve imports referenced by hoisted nested steps (vercel#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.
1 parent 4c165b6 commit 1d4f83a

22 files changed

Lines changed: 430 additions & 73 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@workflow/swc-plugin": patch
3+
---
4+
5+
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):
6+
7+
1. Module-level imports referenced only by hoisted step bodies were stripped by dead-code elimination, causing a `ReferenceError` at runtime.
8+
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.
9+
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.

packages/swc-plugin-workflow/transform/src/lib.rs

Lines changed: 169 additions & 60 deletions
Large diffs are not rendered by default.

packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-step.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// https://github.com/vercel/workflow/issues/1365
22
import { MockLanguageModelV3 } from 'ai/test';
33
import { xai as xaiProvider } from '@ai-sdk/xai';
4-
/**__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"}}}}*/;
4+
/**__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"}}}}*/;
55
var mockModel$_anonymousStep0 = async ()=>{
66
const { args } = function() {
77
var __wf_ctx = globalThis[Symbol.for("WORKFLOW_STEP_CONTEXT_STORAGE")], __wf_store = __wf_ctx && __wf_ctx.getStore();

packages/swc-plugin-workflow/transform/tests/fixture/closure-new-expr-and-module-declarations/output-workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// https://github.com/vercel/workflow/issues/1365
2-
/**__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"}}}}*/;
2+
/**__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"}}}}*/;
33
// Bug 1: `new` expressions should have their arguments captured as closure vars
44
export function mockModel(...args) {
55
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//mockModel/_anonymousStep0", ()=>({

packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-step.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**__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"}}}}*/;
1+
/**__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"}}}}*/;
22
var withTsAs$_anonymousStep0 = async ()=>{
33
const { config } = function() {
44
var __wf_ctx = globalThis[Symbol.for("WORKFLOW_STEP_CONTEXT_STORAGE")], __wf_store = __wf_ctx && __wf_ctx.getStore();

packages/swc-plugin-workflow/transform/tests/fixture/closure-typescript-expressions/output-workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**__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"}}}}*/;
1+
/**__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"}}}}*/;
22
// TypeScript expression wrappers should not prevent closure variable detection.
33
// The plugin must traverse through `as`, `satisfies`, `!`, type assertions,
44
// const assertions, and instantiation expressions to reach the inner expression.

packages/swc-plugin-workflow/transform/tests/fixture/factory-with-step-method/output-step.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'fs/promises';
12
/**__internal_workflows{"steps":{"input.js":{"myFactory/myStep":{"stepId":"step//./input//myFactory/myStep"}}}}*/;
23
var myFactory$myStep = async function() {
34
await fs.mkdir('test');

packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
2-
/**__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"}}}}*/;
2+
/**__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"}}}}*/;
33
var Service$process$helper = async (x)=>x * 2;
44
(function(__wf_fn, __wf_id) {
55
var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());

packages/swc-plugin-workflow/transform/tests/fixture/nested-step-arguments/output-step.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**__internal_workflows{"workflows":{"input.js":{"outer":{"workflowId":"workflow//./input//outer"}}},"steps":{"input.js":{"step":{"stepId":"step//./input//step"}}}}*/;
1+
/**__internal_workflows{"workflows":{"input.js":{"outer":{"workflowId":"workflow//./input//outer"}}},"steps":{"input.js":{"step":{"stepId":"step//./input//outer/step"}}}}*/;
22
async function outer$step() {
33
return arguments[0];
44
}

packages/swc-plugin-workflow/transform/tests/fixture/nested-step-arguments/output-workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**__internal_workflows{"workflows":{"input.js":{"outer":{"workflowId":"workflow//./input//outer"}}},"steps":{"input.js":{"step":{"stepId":"step//./input//step"}}}}*/;
1+
/**__internal_workflows{"workflows":{"input.js":{"outer":{"workflowId":"workflow//./input//outer"}}},"steps":{"input.js":{"step":{"stepId":"step//./input//outer/step"}}}}*/;
22
// `arguments` inside a nested `function`-form step body must NOT be treated
33
// as a closure variable — it's a per-function intrinsic binding that
44
// reflects the positional args the runtime passes via `stepFn.apply(thisVal,

0 commit comments

Comments
 (0)