Apply workflow transformation with export { fnName } syntax (#547)

Added support for workflow transformation with `export { fnName }` syntax.

### What changed?

Enhanced the SWC plugin to properly handle workflow functions that are exported using the named export syntax (`export { fnName }`). Previously, the plugin only properly handled workflow functions that were directly exported with `export function` or `export const`.

The implementation now:
- Collects names that are exported via `export { ... }` syntax in a first pass
- Applies the appropriate transformations to workflow functions that are later exported
- Ensures workflowId is correctly assigned to functions exported with this syntax
- Works across all transformation modes (Client, Step, and Workflow)

### How to test?

Test with files that use the named export syntax:

```javascript
async function workflowFunction() {
  'use workflow';
  // function body
}

export { workflowFunction };
```

Verify that the transformation correctly:
- Adds workflowId to the function
- Applies the appropriate mode-specific transformations
- Preserves the export statement

### Why make this change?

This change ensures consistent behavior across different export syntaxes. Previously, workflow functions exported with the `export { fnName }` syntax weren't properly transformed, which could lead to runtime errors or unexpected behavior. This enhancement provides developers with more flexibility in how they structure and export their workflow functions.
This commit is contained in:
Nathan Rajlich
2025-12-05 14:20:40 -08:00
committed by GitHub
parent 48b3a12fd0
commit f46c51e30f
6 changed files with 121 additions and 14 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@workflow/swc-plugin": patch
---
Apply workflow transformation with `export { fnName }` syntax
@@ -3503,6 +3503,27 @@ impl VisitMut for StepTransform {
}
}
// First pass: Collect names that are exported via `export { ... }` syntax
// This is needed to determine which non-exported workflow functions need workflowId in Workflow mode
let mut named_export_names: HashSet<String> = HashSet::new();
for item in items.iter() {
if let ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) = item {
// Only process local exports (not re-exports from other modules)
if named.src.is_none() {
for specifier in &named.specifiers {
if let ExportSpecifier::Named(named_spec) = specifier {
// Get the local name (the original identifier, not the exported alias)
let local_name = match &named_spec.orig {
ModuleExportName::Ident(ident) => ident.sym.to_string(),
ModuleExportName::Str(s) => s.value.to_string_lossy().to_string(),
};
named_export_names.insert(local_name);
}
}
}
}
}
// Process items and collect functions that need workflowId assignments
let mut items_to_insert = Vec::new();
@@ -3700,9 +3721,14 @@ impl VisitMut for StepTransform {
item.visit_mut_with(self);
// After visiting the item, check if we need to add a workflowId assignment
if matches!(self.mode, TransformMode::Client | TransformMode::Step) {
match item {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
// In Client/Step modes: add for all workflow functions
// In Workflow mode: add for non-exported workflow functions that are later exported via `export { ... }`
// (directly exported ones are handled by workflow_exports_to_expand at end of file)
match item {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
// Directly exported - only handle in Client/Step modes
// (Workflow mode handles these via workflow_exports_to_expand)
if matches!(self.mode, TransformMode::Client | TransformMode::Step) {
if let Decl::Fn(fn_decl) = &export_decl.decl {
let fn_name = fn_decl.ident.sym.to_string();
if self.workflow_function_names.contains(&fn_name) {
@@ -3737,7 +3763,10 @@ impl VisitMut for StepTransform {
}
}
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(default_decl)) => {
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(default_decl)) => {
// Default exports - only handle in Client/Step modes
if matches!(self.mode, TransformMode::Client | TransformMode::Step) {
if let DefaultDecl::Fn(fn_expr) = &default_decl.decl {
// Check if this is a workflow function by checking for "default" key
if self.workflow_function_names.contains("default") {
@@ -3758,9 +3787,18 @@ impl VisitMut for StepTransform {
}
}
}
ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => {
let fn_name = fn_decl.ident.sym.to_string();
if self.workflow_function_names.contains(&fn_name) {
}
ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => {
// Non-exported function declaration
let fn_name = fn_decl.ident.sym.to_string();
if self.workflow_function_names.contains(&fn_name) {
// In Client/Step modes: always add workflowId
// In Workflow mode: only add if later exported via `export { ... }`
let should_add = match self.mode {
TransformMode::Client | TransformMode::Step => true,
TransformMode::Workflow => named_export_names.contains(&fn_name),
};
if should_add {
items_to_insert.push((
i + 1,
ModuleItem::Stmt(self.create_workflow_id_assignment(
@@ -3770,11 +3808,20 @@ impl VisitMut for StepTransform {
));
}
}
ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => {
for declarator in &var_decl.decls {
if let Pat::Ident(binding) = &declarator.name {
let name = binding.id.sym.to_string();
if self.workflow_function_names.contains(&name) {
}
ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => {
// Non-exported variable declaration
for declarator in &var_decl.decls {
if let Pat::Ident(binding) = &declarator.name {
let name = binding.id.sym.to_string();
if self.workflow_function_names.contains(&name) {
// In Client/Step modes: always add workflowId
// In Workflow mode: only add if later exported via `export { ... }`
let should_add = match self.mode {
TransformMode::Client | TransformMode::Step => true,
TransformMode::Workflow => named_export_names.contains(&name),
};
if should_add {
if let Some(init) = &declarator.init {
let span = match &**init {
Expr::Fn(fn_expr) => fn_expr.function.span,
@@ -3792,8 +3839,8 @@ impl VisitMut for StepTransform {
}
}
}
_ => {}
}
_ => {}
}
}
@@ -3802,7 +3849,8 @@ impl VisitMut for StepTransform {
items.insert(index, item);
}
// In workflow mode, add workflowId property to workflow functions
// In workflow mode, add workflowId property to workflow functions (at end of file)
// Note: This handles directly exported workflow functions
if self.mode == TransformMode::Workflow && !self.workflow_exports_to_expand.is_empty() {
// Process workflow functions to add workflowId property
let workflow_functions: Vec<_> = self.workflow_exports_to_expand.drain(..).collect();
@@ -0,0 +1,17 @@
async function stepFunction(a, b) {
'use step';
return a + b;
}
async function workflowFunction(a, b) {
'use workflow';
const result = await stepFunction(a, b);
return result * 2;
}
async function normalFunction(a, b) {
return a * b;
}
export { workflowFunction, stepFunction, normalFunction };
@@ -0,0 +1,12 @@
/**__internal_workflows{"workflows":{"input.js":{"workflowFunction":{"workflowId":"workflow//input.js//workflowFunction"}}},"steps":{"input.js":{"stepFunction":{"stepId":"step//input.js//stepFunction"}}}}*/;
async function stepFunction(a, b) {
return a + b;
}
async function workflowFunction(a, b) {
throw new Error("You attempted to execute workflow workflowFunction function directly. To start a workflow, use start(workflowFunction) from workflow/api");
}
workflowFunction.workflowId = "workflow//input.js//workflowFunction";
async function normalFunction(a, b) {
return a * b;
}
export { workflowFunction, stepFunction, normalFunction };
@@ -0,0 +1,14 @@
import { registerStepFunction } from "workflow/internal/private";
/**__internal_workflows{"workflows":{"input.js":{"workflowFunction":{"workflowId":"workflow//input.js//workflowFunction"}}},"steps":{"input.js":{"stepFunction":{"stepId":"step//input.js//stepFunction"}}}}*/;
async function stepFunction(a, b) {
return a + b;
}
async function workflowFunction(a, b) {
throw new Error("You attempted to execute workflow workflowFunction function directly. To start a workflow, use start(workflowFunction) from workflow/api");
}
workflowFunction.workflowId = "workflow//input.js//workflowFunction";
async function normalFunction(a, b) {
return a * b;
}
export { workflowFunction, stepFunction, normalFunction };
registerStepFunction("step//input.js//stepFunction", stepFunction);
@@ -0,0 +1,11 @@
/**__internal_workflows{"workflows":{"input.js":{"workflowFunction":{"workflowId":"workflow//input.js//workflowFunction"}}},"steps":{"input.js":{"stepFunction":{"stepId":"step//input.js//stepFunction"}}}}*/;
var stepFunction = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepFunction");
async function workflowFunction(a, b) {
const result = await stepFunction(a, b);
return result * 2;
}
workflowFunction.workflowId = "workflow//input.js//workflowFunction";
async function normalFunction(a, b) {
return a * b;
}
export { workflowFunction, stepFunction, normalFunction };