Apply workflow function transformation in "step" mode (#420)

This commit is contained in:
Nathan Rajlich
2025-11-26 23:35:53 -08:00
committed by GitHub
parent 0cacb99fcc
commit 5b918611fc
18 changed files with 492 additions and 244 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@workflow/swc-plugin": patch
---
Apply workflow function transformation in "step" mode
+430 -129
View File
@@ -189,8 +189,8 @@ pub struct StepTransform {
// Track all declared identifiers in module scope to avoid collisions
declared_identifiers: HashSet<String>,
// Track object property step functions for hoisting in step mode
// (parent_var_name, prop_name, arrow_expr, span)
object_property_step_functions: Vec<(String, String, ArrowExpr, swc_core::common::Span)>,
// (parent_var_name, prop_name, arrow_expr, span, parent_workflow_name)
object_property_step_functions: Vec<(String, String, ArrowExpr, swc_core::common::Span, String)>,
// Track nested step functions inside workflow functions for hoisting in step mode
// (fn_name, fn_expr, span, closure_vars, was_arrow, parent_workflow_name)
nested_step_functions: Vec<(
@@ -688,6 +688,42 @@ fn is_global_identifier(name: &str) -> bool {
)
}
// Visitor to normalize the SyntaxContext of closure variables in a function body.
// This ensures that identifiers in the body match the ones we create in the
// closure destructuring pattern (which use SyntaxContext::empty()).
struct ClosureVariableNormalizer {
closure_vars: HashSet<String>,
}
impl ClosureVariableNormalizer {
fn new(closure_vars: &[String]) -> Self {
Self {
closure_vars: closure_vars.iter().cloned().collect(),
}
}
fn normalize_function_body(closure_vars: &[String], body: &mut BlockStmt) {
let mut normalizer = Self::new(closure_vars);
body.visit_mut_with(&mut normalizer);
}
}
impl VisitMut for ClosureVariableNormalizer {
fn visit_mut_ident(&mut self, ident: &mut Ident) {
if self.closure_vars.contains(&ident.sym.to_string()) {
// Replace with a new identifier that has SyntaxContext::empty()
// This ensures it matches the destructuring pattern we create
*ident = Ident::new(ident.sym.clone(), ident.span, SyntaxContext::empty());
}
}
// Don't descend into nested functions - their closure vars are handled separately
fn visit_mut_function(&mut self, _: &mut Function) {}
fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {}
noop_visit_mut_type!();
}
impl StepTransform {
fn process_stmt(&mut self, stmt: &mut Stmt) {
match stmt {
@@ -874,10 +910,47 @@ impl StepTransform {
} else if self.should_transform_workflow_function(&fn_decl.function, false) {
if self.validate_async_function(&fn_decl.function, fn_decl.function.span) {
self.workflow_function_names.insert(fn_name.clone());
let fn_span = fn_decl.function.span;
match self.mode {
TransformMode::Step => {
// First visit children to process nested step functions
stmt.visit_mut_children_with(self);
// After step hoisting, re-extract fn_decl and replace workflow body with throw error
if let Stmt::Decl(Decl::Fn(fn_decl)) = stmt {
self.remove_use_workflow_directive(&mut fn_decl.function.body);
if let Some(body) = &mut fn_decl.function.body {
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
fn_name, fn_name
);
let error_expr = Expr::New(NewExpr {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
callee: Box::new(Expr::Ident(Ident::new(
"Error".into(),
DUMMY_SP,
SyntaxContext::empty(),
))),
args: Some(vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: error_msg.into(),
raw: None,
}))),
}]),
type_args: None,
});
body.stmts = vec![Stmt::Throw(ThrowStmt {
span: DUMMY_SP,
arg: Box::new(error_expr),
})];
}
}
self.workflow_functions_needing_id
.push((fn_name.clone(), fn_span));
}
TransformMode::Workflow => {
self.remove_use_workflow_directive(&mut fn_decl.function.body);
@@ -914,7 +987,7 @@ impl StepTransform {
})];
}
self.workflow_functions_needing_id
.push((fn_name.clone(), fn_decl.function.span));
.push((fn_name.clone(), fn_span));
stmt.visit_mut_children_with(self);
}
}
@@ -1157,8 +1230,13 @@ impl StepTransform {
parent_var_name: &str,
prop_name: &str,
is_workflow: bool,
workflow_name: Option<&str>,
) -> String {
let fn_name = format!("{}/{}", parent_var_name, prop_name);
let fn_name = if let Some(wf_name) = workflow_name {
format!("{}/{}/{}", wf_name, parent_var_name, prop_name)
} else {
format!("{}/{}", parent_var_name, prop_name)
};
let prefix = if is_workflow { "workflow" } else { "step" };
naming::format_name(prefix, &self.filename, &fn_name)
}
@@ -1210,6 +1288,9 @@ impl StepTransform {
prop_key.clone(),
arrow_expr.clone(),
arrow_expr.span,
self.current_workflow_function_name
.clone()
.unwrap_or_default(),
));
let span = arrow_expr.span;
@@ -1271,6 +1352,9 @@ impl StepTransform {
prop_key.clone(),
arrow_from_fn,
span,
self.current_workflow_function_name
.clone()
.unwrap_or_default(),
));
let _ = fn_expr; // Drop the mutable reference
@@ -1340,18 +1424,27 @@ impl StepTransform {
prop_key.clone(),
arrow_from_method,
span,
self.current_workflow_function_name
.clone()
.unwrap_or_default(),
));
// Now handle the transformation based on mode
match self.mode {
TransformMode::Step => {
// In step mode, replace method with key-value property referencing the hoisted variable
let hoist_var_name =
format!("{}${}", parent_var_name, prop_key);
let hoist_var_name = if let Some(ref workflow_name) =
self.current_workflow_function_name
{
format!("{}${}${}", workflow_name, parent_var_name, prop_key)
} else {
format!("{}${}", parent_var_name, prop_key)
};
let step_id = self.create_object_property_id(
parent_var_name,
&prop_key,
false,
self.current_workflow_function_name.as_deref(),
);
// Replace the method with a key-value property referencing the hoisted function
*boxed_prop = Box::new(Prop::KeyValue(KeyValueProp {
@@ -1374,6 +1467,7 @@ impl StepTransform {
parent_var_name,
&prop_key,
false,
self.current_workflow_function_name.as_deref(),
);
*boxed_prop = Box::new(Prop::KeyValue(KeyValueProp {
key: method_prop.key.clone(),
@@ -1406,12 +1500,23 @@ impl StepTransform {
prop_key: &str,
_span: swc_core::common::Span,
) {
let step_id = self.create_object_property_id(parent_var_name, prop_key, false);
let step_id = self.create_object_property_id(
parent_var_name,
prop_key,
false,
self.current_workflow_function_name.as_deref(),
);
match self.mode {
TransformMode::Step => {
// In step mode, replace with reference to hoisted variable
let hoist_var_name = format!("{}${}", parent_var_name, prop_key);
let hoist_var_name = if let Some(ref workflow_name) =
self.current_workflow_function_name
{
format!("{}${}${}", workflow_name, parent_var_name, prop_key)
} else {
format!("{}${}", parent_var_name, prop_key)
};
*kv_prop.value = Expr::Ident(Ident::new(
hoist_var_name.into(),
DUMMY_SP,
@@ -2880,6 +2985,13 @@ impl VisitMut for StepTransform {
// If there are closure variables, add destructuring as first statement
if !closure_vars.is_empty() {
if let Some(body) = &mut fn_expr.function.body {
// First, normalize the SyntaxContext of closure variable references in the body
// This ensures they match the identifiers we create in the destructuring pattern
ClosureVariableNormalizer::normalize_function_body(
&closure_vars,
body,
);
// Create destructuring statement: const { var1, var2 } = __private_getClosureVars();
let closure_destructure =
Stmt::Decl(Decl::Var(Box::new(VarDecl {
@@ -3020,10 +3132,19 @@ impl VisitMut for StepTransform {
let hoisting_info: Vec<_> = self
.object_property_step_functions
.iter()
.map(|(parent_var, prop_name, arrow_expr, _span)| {
let hoist_var_name = format!("{}${}", parent_var, prop_name);
.map(|(parent_var, prop_name, arrow_expr, _span, workflow_name)| {
let hoist_var_name = if !workflow_name.is_empty() {
format!("{}${}${}", workflow_name, parent_var, prop_name)
} else {
format!("{}${}", 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);
self.create_object_property_id(parent_var, prop_name, false, wf_name);
(
hoist_var_name,
arrow_expr.clone(),
@@ -3348,7 +3469,7 @@ impl VisitMut for StepTransform {
if let ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. })) = &items[0] {
if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr {
let should_remove = match self.mode {
TransformMode::Step => value == "use step",
TransformMode::Step => value == "use step" || value == "use workflow",
TransformMode::Workflow => value == "use workflow",
TransformMode::Client => value == "use step" || value == "use workflow",
};
@@ -3556,7 +3677,7 @@ 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) {
if matches!(self.mode, TransformMode::Client | TransformMode::Step) {
match item {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
if let Decl::Fn(fn_decl) = &export_decl.decl {
@@ -3671,11 +3792,10 @@ impl VisitMut for StepTransform {
}
}
// Handle default workflow exports (workflow and client modes)
// Handle default workflow exports (all modes)
// We need to: 1) find the export default position, 2) replace it with const declaration,
// 3) add workflowId assignment, 4) add export default at the end
if (self.mode == TransformMode::Workflow || self.mode == TransformMode::Client)
&& !self.default_workflow_exports.is_empty()
if !self.default_workflow_exports.is_empty()
{
let default_workflows: Vec<_> = self.default_workflow_exports.drain(..).collect();
let default_exports: Vec<_> = self.default_exports_to_replace.drain(..).collect();
@@ -4106,9 +4226,8 @@ impl VisitMut for StepTransform {
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode, but we need to visit
// their children to handle nested step functions
// (visiting happens at the end of the function)
// Workflow functions need step hoisting first, then transformation
// Store fn_name for later use after visiting children
}
TransformMode::Workflow => {
// Remove directive before cloning (for the metadata)
@@ -4176,13 +4295,13 @@ impl VisitMut for StepTransform {
// Visit children for workflow functions OUTSIDE the match to avoid borrow issues
export_decl.visit_mut_children_with(self);
// After visiting, process the function again for cleanup
// After visiting, process the function again for cleanup and Step mode transformation
if let Decl::Fn(fn_decl) = &mut export_decl.decl {
let fn_name = fn_decl.ident.sym.to_string();
// Remove empty statements from the function body (left by nested step hoisting)
// and filter out var declarations with Invalid expressions
let had_nested_steps = if let Some(body) = &mut fn_decl.function.body {
let original_len = body.stmts.len();
if let Some(body) = &mut fn_decl.function.body {
// Remove empty statements
body.stmts.retain(|stmt| !matches!(stmt, Stmt::Empty(_)));
@@ -4199,15 +4318,41 @@ impl VisitMut for StepTransform {
body.stmts.retain(|stmt| {
!matches!(stmt, Stmt::Decl(Decl::Var(var_decl)) if var_decl.decls.is_empty())
});
}
body.stmts.len() != original_len
} else {
false
};
// In Step mode, only remove workflow directive if there were nested steps
if matches!(self.mode, TransformMode::Step) && had_nested_steps {
// In Step mode, transform workflow function AFTER step hoisting
if matches!(self.mode, TransformMode::Step) {
self.remove_use_workflow_directive(&mut fn_decl.function.body);
if let Some(body) = &mut fn_decl.function.body {
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
fn_name, fn_name
);
let error_expr = Expr::New(NewExpr {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
callee: Box::new(Expr::Ident(Ident::new(
"Error".into(),
DUMMY_SP,
SyntaxContext::empty(),
))),
args: Some(vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: error_msg.into(),
raw: None,
}))),
}]),
type_args: None,
});
body.stmts = vec![Stmt::Throw(ThrowStmt {
span: DUMMY_SP,
arg: Box::new(error_expr),
})];
}
self.workflow_functions_needing_id
.push((fn_name.clone(), fn_decl.function.span));
}
}
} else {
@@ -4274,7 +4419,48 @@ impl VisitMut for StepTransform {
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode
// In step mode, transform workflow function expression with throw error
self.remove_use_workflow_directive(
&mut fn_expr.function.body,
);
if let Some(body) = &mut fn_expr.function.body {
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
name, name
);
let error_expr = Expr::New(NewExpr {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
callee: Box::new(Expr::Ident(
Ident::new(
"Error".into(),
DUMMY_SP,
SyntaxContext::empty(),
),
)),
args: Some(vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(
Lit::Str(Str {
span: DUMMY_SP,
value: error_msg.into(),
raw: None,
}),
)),
}]),
type_args: None,
});
body.stmts = vec![Stmt::Throw(ThrowStmt {
span: DUMMY_SP,
arg: Box::new(error_expr),
})];
}
self.workflow_functions_needing_id.push((
name.clone(),
fn_expr.function.span,
));
}
TransformMode::Workflow => {
// In workflow mode, just remove the directive
@@ -4406,7 +4592,52 @@ impl VisitMut for StepTransform {
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode
// In step mode, transform workflow arrow function with throw error
self.remove_use_workflow_directive_arrow(
&mut arrow_expr.body,
);
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
name, name
);
let error_expr = Expr::New(NewExpr {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
callee: Box::new(Expr::Ident(
Ident::new(
"Error".into(),
DUMMY_SP,
SyntaxContext::empty(),
),
)),
args: Some(vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(
Lit::Str(Str {
span: DUMMY_SP,
value: error_msg.into(),
raw: None,
}),
)),
}]),
type_args: None,
});
arrow_expr.body = Box::new(
BlockStmtOrExpr::BlockStmt(BlockStmt {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
stmts: vec![Stmt::Throw(
ThrowStmt {
span: DUMMY_SP,
arg: Box::new(error_expr),
},
)],
}),
);
self.workflow_functions_needing_id
.push((name.clone(), arrow_expr.span));
}
TransformMode::Workflow => {
// In workflow mode, just remove the directive
@@ -4637,7 +4868,40 @@ impl VisitMut for StepTransform {
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode
// In step mode, transform workflow function with throw error
self.remove_use_workflow_directive(
&mut fn_expr.function.body,
);
if let Some(body) = &mut fn_expr.function.body {
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
name, name
);
let error_expr = Expr::New(NewExpr {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
callee: Box::new(Expr::Ident(Ident::new(
"Error".into(),
DUMMY_SP,
SyntaxContext::empty(),
))),
args: Some(vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: error_msg.into(),
raw: None,
}))),
}]),
type_args: None,
});
body.stmts = vec![Stmt::Throw(ThrowStmt {
span: DUMMY_SP,
arg: Box::new(error_expr),
})];
}
self.workflow_functions_needing_id
.push((name.clone(), fn_expr.function.span));
}
TransformMode::Workflow => {
// In workflow mode, just remove the directive
@@ -4905,7 +5169,43 @@ impl VisitMut for StepTransform {
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode
// In step mode, transform workflow arrow function with throw error
self.remove_use_workflow_directive_arrow(
&mut arrow_expr.body,
);
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
name, name
);
let error_expr = Expr::New(NewExpr {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
callee: Box::new(Expr::Ident(Ident::new(
"Error".into(),
DUMMY_SP,
SyntaxContext::empty(),
))),
args: Some(vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: error_msg.into(),
raw: None,
}))),
}]),
type_args: None,
});
arrow_expr.body =
Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
span: DUMMY_SP,
ctxt: SyntaxContext::empty(),
stmts: vec![Stmt::Throw(ThrowStmt {
span: DUMMY_SP,
arg: Box::new(error_expr),
})],
}));
self.workflow_functions_needing_id
.push((name.clone(), arrow_expr.span));
}
TransformMode::Workflow => {
// In workflow mode, just remove the directive
@@ -5167,9 +5467,14 @@ impl VisitMut for StepTransform {
}
TransformMode::Workflow => {
// Replace with proxy reference
// Use current_parent_function_name to match step mode's ID generation
let step_fn_name =
if let Some(parent) = &self.current_workflow_function_name {
format!("{}/{}", parent, name)
if let Some(parent) = &self.current_parent_function_name {
if !parent.is_empty() {
format!("{}/{}", parent, name)
} else {
name.clone()
}
} else {
name.clone()
};
@@ -5288,9 +5593,14 @@ impl VisitMut for StepTransform {
}
TransformMode::Workflow => {
// Replace with proxy reference
// Use current_parent_function_name to match step mode's ID generation
let step_fn_name =
if let Some(parent) = &self.current_workflow_function_name {
format!("{}/{}", parent, name)
if let Some(parent) = &self.current_parent_function_name {
if !parent.is_empty() {
format!("{}/{}", parent, name)
} else {
name.clone()
}
} else {
name.clone()
};
@@ -5349,44 +5659,8 @@ impl VisitMut for StepTransform {
self.workflow_function_names.insert("default".to_string());
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode
}
TransformMode::Workflow => {
// In workflow mode, just remove the directive
self.remove_use_workflow_directive(&mut fn_expr.function.body);
if fn_name == "default" {
// Anonymous default export: convert to const declaration
// Track for const declaration and workflowId assignment
self.default_workflow_exports.push((
const_name.clone(),
Expr::Fn(fn_expr.clone()),
fn_expr.function.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
fn_name.clone(),
Expr::Ident(Ident::new(
const_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
)),
));
} else {
// Named default export: can reference by name
// export default async function name() { ... }
// name.workflowId = "...";
self.workflow_exports_to_expand.push((
const_name,
Expr::Fn(fn_expr.clone()),
fn_expr.function.span,
));
}
}
TransformMode::Client => {
// In client mode, replace workflow function body with error throw
TransformMode::Step | TransformMode::Client => {
// In step/client mode, replace workflow function body with error throw
self.remove_use_workflow_directive(&mut fn_expr.function.body);
let error_msg = format!(
@@ -5440,7 +5714,40 @@ impl VisitMut for StepTransform {
} else {
// Named function can be referenced directly, just add workflowId
self.workflow_functions_needing_id
.push((const_name, fn_expr.function.span));
.push((const_name.clone(), fn_expr.function.span));
}
}
TransformMode::Workflow => {
// In workflow mode, just remove the directive
self.remove_use_workflow_directive(&mut fn_expr.function.body);
if fn_name == "default" {
// Anonymous default export: convert to const declaration
// Track for const declaration and workflowId assignment
self.default_workflow_exports.push((
const_name.clone(),
Expr::Fn(fn_expr.clone()),
fn_expr.function.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
fn_name.clone(),
Expr::Ident(Ident::new(
const_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
)),
));
} else {
// Named default export: can reference by name
// export default async function name() { ... }
// name.workflowId = "...";
self.workflow_exports_to_expand.push((
const_name,
Expr::Fn(fn_expr.clone()),
fn_expr.function.span,
));
}
}
}
@@ -5522,32 +5829,8 @@ impl VisitMut for StepTransform {
self.workflow_function_names.insert("default".to_string());
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode
}
TransformMode::Workflow => {
// In workflow mode, convert to const declaration
self.remove_use_workflow_directive(&mut fn_expr.function.body);
// Track for const declaration and workflowId assignment
self.default_workflow_exports.push((
unique_name.clone(),
Expr::Fn(fn_expr.clone()),
fn_expr.function.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
"default".to_string(),
Expr::Ident(Ident::new(
unique_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
)),
));
}
TransformMode::Client => {
// In client mode, replace workflow function body with error throw
TransformMode::Step | TransformMode::Client => {
// In step/client mode, replace workflow function body with error throw
self.remove_use_workflow_directive(&mut fn_expr.function.body);
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
@@ -5585,6 +5868,27 @@ impl VisitMut for StepTransform {
fn_expr.function.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
"default".to_string(),
Expr::Ident(Ident::new(
unique_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
)),
));
}
TransformMode::Workflow => {
// In workflow mode, convert to const declaration
self.remove_use_workflow_directive(&mut fn_expr.function.body);
// Track for const declaration and workflowId assignment
self.default_workflow_exports.push((
unique_name.clone(),
Expr::Fn(fn_expr.clone()),
fn_expr.function.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
"default".to_string(),
@@ -5623,32 +5927,8 @@ impl VisitMut for StepTransform {
self.workflow_function_names.insert("default".to_string());
match self.mode {
TransformMode::Step => {
// Workflow functions are not processed in step mode
}
TransformMode::Workflow => {
// In workflow mode, convert to const declaration
self.remove_use_workflow_directive_arrow(&mut arrow_expr.body);
// Track for const declaration and workflowId assignment
self.default_workflow_exports.push((
unique_name.clone(),
Expr::Arrow(arrow_expr.clone()),
arrow_expr.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
"default".to_string(),
Expr::Ident(Ident::new(
unique_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
)),
));
}
TransformMode::Client => {
// In client mode, convert to const declaration so we can assign workflowId
TransformMode::Step | TransformMode::Client => {
// In step/client mode, replace arrow body with throw error
self.remove_use_workflow_directive_arrow(&mut arrow_expr.body);
let error_msg = format!(
"You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
@@ -5689,6 +5969,27 @@ impl VisitMut for StepTransform {
arrow_expr.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
"default".to_string(),
Expr::Ident(Ident::new(
unique_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
)),
));
}
TransformMode::Workflow => {
// In workflow mode, convert to const declaration
self.remove_use_workflow_directive_arrow(&mut arrow_expr.body);
// Track for const declaration and workflowId assignment
self.default_workflow_exports.push((
unique_name.clone(),
Expr::Arrow(arrow_expr.clone()),
arrow_expr.span,
));
// Track for replacement with identifier
self.default_exports_to_replace.push((
"default".to_string(),
@@ -22,7 +22,7 @@ export async function validStep() {
return 42;
}
export const validWorkflow = async ()=>{
'use workflow';
return 'test';
throw new Error("You attempted to execute workflow validWorkflow function directly. To start a workflow, use start(validWorkflow) from workflow/api");
};
validWorkflow.workflowId = "workflow//input.js//validWorkflow";
registerStepFunction("step//input.js//validStep", validStep);
@@ -1,7 +1,7 @@
// Test anonymous default export workflow
/**__internal_workflows{"workflows":{"input.js":{"default":{"workflowId":"workflow//input.js//__default"}}}}*/;
export default async function() {
'use workflow';
const result = await someStep();
return result;
}
const __default = async function() {
throw new Error("You attempted to execute workflow __default function directly. To start a workflow, use start(__default) from workflow/api");
};
__default.workflowId = "workflow//input.js//__default";
export default __default;
@@ -1,7 +1,7 @@
// Test default export arrow workflow
/**__internal_workflows{"workflows":{"input.js":{"default":{"workflowId":"workflow//input.js//__default"}}}}*/;
export default (async (data)=>{
'use workflow';
const processed = await processData(data);
return processed;
});
const __default = async (data)=>{
throw new Error("You attempted to execute workflow __default function directly. To start a workflow, use start(__default) from workflow/api");
};
__default.workflowId = "workflow//input.js//__default";
export default __default;
@@ -15,11 +15,6 @@ async function convertToLanguageModelPrompt({ prompt, supportedUrls, download =
};
}
export async function myWorkflow(input) {
'use workflow';
const result = await convertToLanguageModelPrompt({
prompt: input.prompt,
supportedUrls: {},
download: undefined
});
return result;
throw new Error("You attempted to execute workflow myWorkflow function directly. To start a workflow, use start(myWorkflow) from workflow/api");
}
myWorkflow.workflowId = "workflow//input.js//myWorkflow";
@@ -3,9 +3,8 @@
const __default = "existing variable";
// Use it to avoid unused variable
console.log(__default);
// Anonymous default export should get unique name (__default$1)
export default async function() {
'use workflow';
const result = await someStep();
return result;
}
const __default$1 = async function() {
throw new Error("You attempted to execute workflow __default$1 function directly. To start a workflow, use start(__default$1) from workflow/api");
};
__default$1.workflowId = "workflow//input.js//__default$1";
export default __default$1;
@@ -1,6 +1,6 @@
/**__internal_workflows{"workflows":{"input.js":{"default":{"workflowId":"workflow//input.js//__default"}}}}*/;
export default async function() {
'use workflow';
const result = await someStep();
return result;
}
const __default = async function() {
throw new Error("You attempted to execute workflow __default function directly. To start a workflow, use start(__default) from workflow/api");
};
__default.workflowId = "workflow//input.js//__default";
export default __default;
@@ -7,11 +7,9 @@ async function stepFunctionWithoutExport(a, b) {
return a - b;
}
export async function workflowFunction(a, b) {
'use workflow';
const result = await stepFunction(a, b);
const result2 = await stepFunctionWithoutExport(a, b);
return result + result2;
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";
export async function normalFunction(a, b) {
return a * b;
}
@@ -1,5 +1,4 @@
/**__internal_workflows{"workflows":{"input.js":{"arrowWorkflow":{"workflowId":"workflow//input.js//arrowWorkflow"},"workflow":{"workflowId":"workflow//input.js//workflow"}}}}*/;
'use workflow';
async function local(input) {
return input.foo;
}
@@ -7,8 +6,10 @@ const localArrow = async (input)=>{
return input.bar;
};
export async function workflow(input) {
return input.foo;
throw new Error("You attempted to execute workflow workflow function directly. To start a workflow, use start(workflow) from workflow/api");
}
workflow.workflowId = "workflow//input.js//workflow";
export const arrowWorkflow = async (input)=>{
return input.bar;
throw new Error("You attempted to execute workflow arrowWorkflow function directly. To start a workflow, use start(arrowWorkflow) from workflow/api");
};
arrowWorkflow.workflowId = "workflow//input.js//arrowWorkflow";
@@ -1,5 +1,5 @@
import { registerStepFunction } from "workflow/internal/private";
/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//input.js//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//input.js//arrowStep"},"helpers/objectStep":{"stepId":"step//input.js//helpers/objectStep"},"letArrowStep":{"stepId":"step//input.js//letArrowStep"},"step":{"stepId":"step//input.js//step"},"varArrowStep":{"stepId":"step//input.js//varArrowStep"}}}}*/;
/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//input.js//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//input.js//arrowStep"},"helpers/objectStep":{"stepId":"step//input.js//example/helpers/objectStep"},"letArrowStep":{"stepId":"step//input.js//letArrowStep"},"step":{"stepId":"step//input.js//step"},"varArrowStep":{"stepId":"step//input.js//varArrowStep"}}}}*/;
// Function declaration step
async function example$step(a, b) {
return a + b;
@@ -7,31 +7,15 @@ async function example$step(a, b) {
var example$arrowStep = async (x, y)=>x * y;
var example$letArrowStep = async (x, y)=>x - y;
var example$varArrowStep = async (x, y)=>x / y;
var helpers$objectStep = async (x, y)=>{
var example$helpers$objectStep = async (x, y)=>{
return x + y + 10;
};
export async function example(a, b) {
"use workflow";
const step = example$step;
// Arrow function with const
const arrowStep = example$arrowStep;
// Arrow function with let
let letArrowStep = example$letArrowStep;
// Arrow function with var
var varArrowStep = example$varArrowStep;
// Object with step method
const helpers = {
objectStep: helpers$objectStep
};
const val = await step(a, b);
const val2 = await arrowStep(a, b);
const val3 = await letArrowStep(a, b);
const val4 = await varArrowStep(a, b);
const val5 = await helpers.objectStep(a, b);
return val + val2 + val3 + val4 + val5;
throw new Error("You attempted to execute workflow example function directly. To start a workflow, use start(example) from workflow/api");
}
example.workflowId = "workflow//input.js//example";
registerStepFunction("step//input.js//example/step", example$step);
registerStepFunction("step//input.js//example/arrowStep", example$arrowStep);
registerStepFunction("step//input.js//example/letArrowStep", example$letArrowStep);
registerStepFunction("step//input.js//example/varArrowStep", example$varArrowStep);
registerStepFunction("step//input.js//helpers/objectStep", helpers$objectStep);
registerStepFunction("step//input.js//example/helpers/objectStep", example$helpers$objectStep);
@@ -1,4 +1,4 @@
/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//input.js//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//input.js//arrowStep"},"helpers/objectStep":{"stepId":"step//input.js//helpers/objectStep"},"letArrowStep":{"stepId":"step//input.js//letArrowStep"},"step":{"stepId":"step//input.js//step"},"varArrowStep":{"stepId":"step//input.js//varArrowStep"}}}}*/;
/**__internal_workflows{"workflows":{"input.js":{"example":{"workflowId":"workflow//input.js//example"}}},"steps":{"input.js":{"arrowStep":{"stepId":"step//input.js//arrowStep"},"helpers/objectStep":{"stepId":"step//input.js//example/helpers/objectStep"},"letArrowStep":{"stepId":"step//input.js//letArrowStep"},"step":{"stepId":"step//input.js//step"},"varArrowStep":{"stepId":"step//input.js//varArrowStep"}}}}*/;
export async function example(a, b) {
var step = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//example/step");
// Arrow function with const
@@ -9,7 +9,7 @@ export async function example(a, b) {
var varArrowStep = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//example/varArrowStep");
// Object with step method
const helpers = {
objectStep: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//helpers/objectStep")
objectStep: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//example/helpers/objectStep")
};
const val = await step(a, b);
const val2 = await arrowStep(a, b);
@@ -80,23 +80,9 @@ const arrowWrapperReturnNamedFunctionVar = (a, b, c)=>{
return fn;
};
export async function wflow() {
'use workflow';
let count = 42;
const namedStepWithClosureVars = wflow$namedStepWithClosureVars;
const agent = new DurableAgent({
arrowFunctionWithClosureVars: _anonymousStep2,
namedFunctionWithClosureVars: _anonymousStep3,
methodWithClosureVars: _anonymousStep4
});
await stepWrapperReturnArrowFunctionVar(1, 2, 3)();
await stepWrapperReturnNamedFunction(1, 2, 3)();
await stepWrapperReturnArrowFunction(1, 2, 3)();
await stepWrapperReturnNamedFunctionVar(1, 2, 3)();
await arrowWrapperReturnArrowFunctionVar(1, 2, 3)();
await arrowWrapperReturnNamedFunction(1, 2, 3)();
await arrowWrapperReturnArrowFunction(1, 2, 3)();
await arrowWrapperReturnNamedFunctionVar(1, 2, 3)();
throw new Error("You attempted to execute workflow wflow function directly. To start a workflow, use start(wflow) from workflow/api");
}
wflow.workflowId = "workflow//input.js//wflow";
registerStepFunction("step//input.js//stepWrapperReturnArrowFunctionVar/fn", stepWrapperReturnArrowFunctionVar$fn);
registerStepFunction("step//input.js//stepWrapperReturnNamedFunction/f", stepWrapperReturnNamedFunction$f);
registerStepFunction("step//input.js//stepWrapperReturnArrowFunction/_anonymousStep0", stepWrapperReturnArrowFunction$_anonymousStep0);
@@ -9,14 +9,14 @@ function stepWrapperReturnArrowFunctionVar(a, b, c) {
return fn;
}
function stepWrapperReturnNamedFunction(a, b, c) {
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//f", ()=>({
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWrapperReturnNamedFunction/f", ()=>({
a,
b,
c
}));
}
function stepWrapperReturnArrowFunction(a, b, c) {
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//_anonymousStep0", ()=>({
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWrapperReturnArrowFunction/_anonymousStep0", ()=>({
a,
b,
c
@@ -39,14 +39,14 @@ const arrowWrapperReturnArrowFunctionVar = (a, b, c)=>{
return fn;
};
const arrowWrapperReturnNamedFunction = (a, b, c)=>{
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//f", ()=>({
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//arrowWrapperReturnNamedFunction/f", ()=>({
a,
b,
c
}));
};
const arrowWrapperReturnArrowFunction = (a, b, c)=>{
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//_anonymousStep1", ()=>({
return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//arrowWrapperReturnArrowFunction/_anonymousStep1", ()=>({
a,
b,
c
@@ -6,27 +6,8 @@ import * as z from 'zod';
var test$_anonymousStep0 = async ()=>gateway('openai/gpt-5');
var test$_anonymousStep1 = async ({ location })=>`Weather in ${location}: Sunny, 72°F`;
export async function test() {
'use workflow';
const agent = new DurableAgent({
model: _anonymousStep0,
tools: {
getWeather: tool({
description: 'Get weather for a location',
inputSchema: z.object({
location: z.string()
}),
execute: _anonymousStep1
})
}
});
await agent.stream({
messages: [
{
role: 'user',
content: 'What is the weather in San Francisco?'
}
]
});
throw new Error("You attempted to execute workflow test function directly. To start a workflow, use start(test) from workflow/api");
}
test.workflowId = "workflow//input.js//test";
registerStepFunction("step//input.js//test/_anonymousStep0", test$_anonymousStep0);
registerStepFunction("step//input.js//test/_anonymousStep1", test$_anonymousStep1);
@@ -1,5 +1,5 @@
/**__internal_workflows{"workflows":{"input.js":{"workflow":{"workflowId":"workflow//input.js//workflow"}}}}*/;
export async function workflow(a, b) {
'use workflow';
return add(a, b);
throw new Error("You attempted to execute workflow workflow function directly. To start a workflow, use start(workflow) from workflow/api");
}
workflow.workflowId = "workflow//input.js//workflow";
@@ -1,5 +1,5 @@
/**__internal_workflows{"workflows":{"input.js":{"processData":{"workflowId":"workflow//input.js//processData"}}}}*/;
export const processData = async (data)=>{
'use workflow';
return data.processed;
throw new Error("You attempted to execute workflow processData function directly. To start a workflow, use start(processData) from workflow/api");
};
processData.workflowId = "workflow//input.js//processData";
@@ -1,24 +1,22 @@
// Test workflow functions in client mode
/**__internal_workflows{"workflows":{"input.js":{"arrowWorkflow":{"workflowId":"workflow//input.js//arrowWorkflow"},"default":{"workflowId":"workflow//input.js//defaultWorkflow"},"internalWorkflow":{"workflowId":"workflow//input.js//internalWorkflow"},"myWorkflow":{"workflowId":"workflow//input.js//myWorkflow"}}}}*/;
export async function myWorkflow() {
'use workflow';
const result = await someStep();
return result;
throw new Error("You attempted to execute workflow myWorkflow function directly. To start a workflow, use start(myWorkflow) from workflow/api");
}
myWorkflow.workflowId = "workflow//input.js//myWorkflow";
export const arrowWorkflow = async ()=>{
'use workflow';
const data = await fetchData();
return data;
throw new Error("You attempted to execute workflow arrowWorkflow function directly. To start a workflow, use start(arrowWorkflow) from workflow/api");
};
arrowWorkflow.workflowId = "workflow//input.js//arrowWorkflow";
export default async function defaultWorkflow() {
'use workflow';
return await process();
throw new Error("You attempted to execute workflow defaultWorkflow function directly. To start a workflow, use start(defaultWorkflow) from workflow/api");
}
defaultWorkflow.workflowId = "workflow//input.js//defaultWorkflow";
// Non-export workflow function
async function internalWorkflow() {
'use workflow';
return 'internal';
throw new Error("You attempted to execute workflow internalWorkflow function directly. To start a workflow, use start(internalWorkflow) from workflow/api");
}
internalWorkflow.workflowId = "workflow//input.js//internalWorkflow";
// Use the internal workflow to avoid lint warning
regularFunction(internalWorkflow);
// Regular function should not be affected