From 5e9b875a4dc4bc8e5332876ae6e8791e51c5c8ba Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 8 Apr 2026 18:05:35 -0700 Subject: [PATCH 1/3] Followup fixes for sync step function support Address review feedback from #1633 (merged): - Remove dangling incomplete comment from validate_async_function removal - Restore export validation for file-level 'use step' files: allow sync/async function exports, reject non-function exports (constants, classes, re-exports) which can pull Node-only code into bundles - Fix InvalidExport error message: 'Only functions can be exported' for step files vs 'Only async functions can be exported' for workflow - Update spec.md error table and supported function forms to document sync step support - Add sync-step-class test fixture (sync static methods, sync function expressions with var/let) - Add sync-workflow error test (sync workflow still errors) --- .changeset/sync-step-followup.md | 5 + packages/swc-plugin-workflow/spec.md | 19 +-- .../swc-plugin-workflow/transform/src/lib.rs | 111 ++++++++++++++++-- .../invalid-exports/output-client.stderr | 21 ++++ .../errors/invalid-exports/output-step.stderr | 21 ++++ .../invalid-exports/output-workflow.stderr | 21 ++++ .../tests/errors/sync-workflow/input.js | 11 ++ .../errors/sync-workflow/output-client.js | 11 ++ .../errors/sync-workflow/output-client.stderr | 8 ++ .../tests/errors/sync-workflow/output-step.js | 14 +++ .../errors/sync-workflow/output-step.stderr | 8 ++ .../errors/sync-workflow/output-workflow.js | 8 ++ .../sync-workflow/output-workflow.stderr | 8 ++ .../tests/fixture/sync-step-class/input.js | 35 ++++++ .../fixture/sync-step-class/output-client.js | 41 +++++++ .../fixture/sync-step-class/output-step.js | 59 ++++++++++ .../sync-step-class/output-workflow.js | 31 +++++ 17 files changed, 416 insertions(+), 16 deletions(-) create mode 100644 .changeset/sync-step-followup.md create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-workflow.js diff --git a/.changeset/sync-step-followup.md b/.changeset/sync-step-followup.md new file mode 100644 index 0000000000..f7fdb337d2 --- /dev/null +++ b/.changeset/sync-step-followup.md @@ -0,0 +1,5 @@ +--- +"@workflow/swc-plugin": patch +--- + +Restore export validation for file-level `"use step"` files: only function exports (sync or async) are allowed; non-function exports (constants, classes, re-exports) emit an error diff --git a/packages/swc-plugin-workflow/spec.md b/packages/swc-plugin-workflow/spec.md index 8e8cda594f..24b2f97894 100644 --- a/packages/swc-plugin-workflow/spec.md +++ b/packages/swc-plugin-workflow/spec.md @@ -1060,26 +1060,31 @@ The plugin emits errors for invalid usage: | Error | Description | |-------|-------------| -| Non-async function | Functions with `"use step"` or `"use workflow"` must be async | +| Non-async workflow function | Functions with `"use workflow"` must be async (step functions may be sync) | | Instance methods with `"use workflow"` | Only static methods can have `"use workflow"` (not instance methods) | | Getters with `"use workflow"` | Getters cannot be marked with `"use workflow"` | | Misplaced directive | Directive must be at top of file or start of function body | | Conflicting directives | Cannot have both `"use step"` and `"use workflow"` at module level | -| Invalid exports | Module-level directive files can only export async functions | +| Invalid exports (`"use workflow"`) | Module-level `"use workflow"` files can only export async functions | +| Invalid exports (`"use step"`) | Module-level `"use step"` files can only export functions (sync or async) | | Misspelled directive | Detects typos like `"use steps"` or `"use workflows"` | --- ## Supported Function Forms -The plugin supports various function declaration styles: +The plugin supports various function declaration styles. Step functions may be synchronous or asynchronous. Workflow functions must be async. -- `async function name() { "use step"; }` - Function declaration -- `const name = async () => { "use step"; }` - Arrow function with const +- `async function name() { "use step"; }` - Async function declaration +- `function name() { "use step"; }` - Sync function declaration +- `const name = async () => { "use step"; }` - Async arrow function +- `const name = () => { "use step"; }` - Sync arrow function - `let name = async () => { "use step"; }` - Arrow function with let - `var name = async () => { "use step"; }` - Arrow function with var -- `const name = async function() { "use step"; }` - Function expression -- `{ async method() { "use step"; } }` - Object method +- `const name = async function() { "use step"; }` - Async function expression +- `const name = function() { "use step"; }` - Sync function expression +- `{ async method() { "use step"; } }` - Async object method +- `{ method() { "use step"; } }` - Sync object method - `{ nested: { execute: async () => { "use step"; } } }` - Nested object property - `static async method() { "use step"; }` - Static class method - `async method() { "use step"; }` - Instance class method (requires custom serialization) diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index a1facde874..37a5d834d4 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -102,10 +102,17 @@ fn emit_error(error: WorkflowErrorKind) { ), WorkflowErrorKind::InvalidExport { span, directive } => ( span, - format!( - "Only async functions can be exported from a \"{}\" file", - directive - ), + if directive == "use step" { + format!( + "Only functions can be exported from a \"{}\" file", + directive + ) + } else { + format!( + "Only async functions can be exported from a \"{}\" file", + directive + ) + }, ), }; @@ -4054,8 +4061,6 @@ impl StepTransform { } } - // Previously validated that step functions must be async. The restriction - // Check if a function should be treated as a step function fn should_transform_function(&self, function: &Function, is_exported: bool) -> bool { let has_directive = self.has_use_step_directive(&function.body); @@ -6051,9 +6056,97 @@ impl VisitMut for StepTransform { let mut items_to_insert = Vec::new(); for (i, item) in items.iter_mut().enumerate() { - // Validate exports if we have a file-level workflow directive. - // Step files allow any exports (sync or async), but workflow files - // require exported functions to be async. + // Validate exports for file-level step directives. + // Step files allow sync or async function exports but reject + // non-function exports (constants, classes, re-exports) which + // can pull Node-only code into the workflow/client bundles. + if self.has_file_step_directive { + match item { + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => { + match &export.decl { + Decl::Fn(_) => { + // Sync or async function declarations are allowed + } + Decl::Var(var_decl) => { + for decl in &var_decl.decls { + if let Some(init) = &decl.init { + match &**init { + Expr::Fn(_) | Expr::Arrow(_) => { + // Function/arrow expressions are allowed + } + _ => { + emit_error(WorkflowErrorKind::InvalidExport { + span: export.span, + directive: "use step", + }); + } + } + } + } + } + Decl::TsInterface(_) + | Decl::TsTypeAlias(_) + | Decl::TsEnum(_) + | Decl::TsModule(_) => { + // TypeScript declarations are okay + } + _ => { + emit_error(WorkflowErrorKind::InvalidExport { + span: export.span, + directive: "use step", + }); + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => { + if named.src.is_some() { + emit_error(WorkflowErrorKind::InvalidExport { + span: named.span, + directive: "use step", + }); + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(default)) => { + match &default.decl { + DefaultDecl::Fn(_) => { + // Sync or async function declarations are allowed + } + DefaultDecl::TsInterfaceDecl(_) => { + // TypeScript interface is okay + } + _ => { + emit_error(WorkflowErrorKind::InvalidExport { + span: default.span, + directive: "use step", + }); + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(expr)) => { + match &*expr.expr { + Expr::Fn(_) | Expr::Arrow(_) => { + // Function/arrow expressions are allowed + } + _ => { + emit_error(WorkflowErrorKind::InvalidExport { + span: expr.span, + directive: "use step", + }); + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)) => { + emit_error(WorkflowErrorKind::InvalidExport { + span: export_all.span, + directive: "use step", + }); + } + _ => {} + } + } + + // Validate exports for file-level workflow directives. + // Workflow files require exported functions to be async. if self.has_file_workflow_directive { match item { ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => { diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr index e69de29bb2..1c1387e9e8 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr @@ -0,0 +1,21 @@ + x Only functions can be exported from a "use step" file + ,-[input.js:4:1] + 3 | // These should all error - not functions + 4 | export const value = 42; + : ^^^^^^^^^^^^^^^^^^^^^^^^ + 5 | export class MyClass { + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:5:1] + 4 | export const value = 42; + 5 | ,-> export class MyClass { + 6 | | method() {} + 7 | `-> } + 8 | export * from './other'; + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:8:1] + 7 | } + 8 | export * from './other'; + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr index e69de29bb2..1c1387e9e8 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr @@ -0,0 +1,21 @@ + x Only functions can be exported from a "use step" file + ,-[input.js:4:1] + 3 | // These should all error - not functions + 4 | export const value = 42; + : ^^^^^^^^^^^^^^^^^^^^^^^^ + 5 | export class MyClass { + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:5:1] + 4 | export const value = 42; + 5 | ,-> export class MyClass { + 6 | | method() {} + 7 | `-> } + 8 | export * from './other'; + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:8:1] + 7 | } + 8 | export * from './other'; + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr index e69de29bb2..1c1387e9e8 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr @@ -0,0 +1,21 @@ + x Only functions can be exported from a "use step" file + ,-[input.js:4:1] + 3 | // These should all error - not functions + 4 | export const value = 42; + : ^^^^^^^^^^^^^^^^^^^^^^^^ + 5 | export class MyClass { + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:5:1] + 4 | export const value = 42; + 5 | ,-> export class MyClass { + 6 | | method() {} + 7 | `-> } + 8 | export * from './other'; + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:8:1] + 7 | } + 8 | export * from './other'; + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/input.js b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/input.js new file mode 100644 index 0000000000..a40a5ab4c4 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/input.js @@ -0,0 +1,11 @@ +// Sync "use workflow" should still error (workflow functions must be async) +export function syncWorkflow() { + 'use workflow'; + return 'not allowed'; +} + +// Sync "use step" should NOT error (sync steps are allowed) +export function syncStep() { + 'use step'; + return 'allowed'; +} diff --git a/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.js b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.js new file mode 100644 index 0000000000..ba84be55eb --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.js @@ -0,0 +1,11 @@ +/**__internal_workflows{"steps":{"input.js":{"syncStep":{"stepId":"step//./input//syncStep"}}}}*/; +// Sync "use workflow" should still error (workflow functions must be async) +export function syncWorkflow() { + 'use workflow'; + return 'not allowed'; +} +// Sync "use step" should NOT error (sync steps are allowed) +export function syncStep() { + return 'allowed'; +} +syncStep.stepId = "step//./input//syncStep"; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.stderr b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.stderr new file mode 100644 index 0000000000..201f229050 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-client.stderr @@ -0,0 +1,8 @@ + x Functions marked with "use workflow" must be async functions + ,-[input.js:2:1] + 1 | // Sync "use workflow" should still error (workflow functions must be async) + 2 | ,-> export function syncWorkflow() { + 3 | | 'use workflow'; + 4 | | return 'not allowed'; + 5 | `-> } + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.js new file mode 100644 index 0000000000..a2866521e9 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.js @@ -0,0 +1,14 @@ +/**__internal_workflows{"steps":{"input.js":{"syncStep":{"stepId":"step//./input//syncStep"}}}}*/; +// Sync "use workflow" should still error (workflow functions must be async) +export function syncWorkflow() { + throw new Error("You attempted to execute workflow syncWorkflow function directly. To start a workflow, use start(syncWorkflow) from workflow/api"); +} +// Sync "use step" should NOT error (sync steps are allowed) +export function syncStep() { + return 'allowed'; +} +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(syncStep, "step//./input//syncStep"); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.stderr new file mode 100644 index 0000000000..201f229050 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-step.stderr @@ -0,0 +1,8 @@ + x Functions marked with "use workflow" must be async functions + ,-[input.js:2:1] + 1 | // Sync "use workflow" should still error (workflow functions must be async) + 2 | ,-> export function syncWorkflow() { + 3 | | 'use workflow'; + 4 | | return 'not allowed'; + 5 | `-> } + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.js new file mode 100644 index 0000000000..e866124cb2 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.js @@ -0,0 +1,8 @@ +/**__internal_workflows{"steps":{"input.js":{"syncStep":{"stepId":"step//./input//syncStep"}}}}*/; +// Sync "use workflow" should still error (workflow functions must be async) +export function syncWorkflow() { + 'use workflow'; + return 'not allowed'; +} +// Sync "use step" should NOT error (sync steps are allowed) +export var syncStep = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//syncStep"); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.stderr new file mode 100644 index 0000000000..201f229050 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/sync-workflow/output-workflow.stderr @@ -0,0 +1,8 @@ + x Functions marked with "use workflow" must be async functions + ,-[input.js:2:1] + 1 | // Sync "use workflow" should still error (workflow functions must be async) + 2 | ,-> export function syncWorkflow() { + 3 | | 'use workflow'; + 4 | | return 'not allowed'; + 5 | `-> } + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/input.js new file mode 100644 index 0000000000..fcdbf709d8 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/input.js @@ -0,0 +1,35 @@ +// Test sync step functions in class contexts + +export class Service { + // Sync static step method + static getConfig() { + 'use step'; + return { timeout: 30000 }; + } + + // Async static step method (for comparison) + static async fetchData(url) { + 'use step'; + return { url }; + } + + // Async static workflow method that calls sync and async steps + static async run() { + 'use workflow'; + const config = await Service.getConfig(); + const data = await Service.fetchData('/api'); + return { config, data }; + } +} + +// Sync function expression with var +export var syncFnExpr = function process(data) { + 'use step'; + return data * 2; +}; + +// Sync function expression with let +export let syncFnExprLet = function transform(input) { + 'use step'; + return String(input); +}; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-client.js new file mode 100644 index 0000000000..74e642ad05 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-client.js @@ -0,0 +1,41 @@ +/**__internal_workflows{"workflows":{"input.js":{"Service.run":{"workflowId":"workflow//./input//Service.run"}}},"steps":{"input.js":{"Service.fetchData":{"stepId":"step//./input//Service.fetchData"},"Service.getConfig":{"stepId":"step//./input//Service.getConfig"}}},"classes":{"input.js":{"Service":{"classId":"class//./input//Service"}}}}*/; +// Test sync step functions in class contexts +export class Service { + // Sync static step method + static getConfig() { + return { + timeout: 30000 + }; + } + // Async static step method (for comparison) + static async fetchData(url) { + return { + url + }; + } + // Async static workflow method that calls sync and async steps + static async run() { + throw new Error("You attempted to execute workflow Service.run function directly. To start a workflow, use start(workflow) from workflow/api"); + } +} +// Sync function expression with var +export var syncFnExpr = function process(data) { + return data * 2; +}; +syncFnExpr.stepId = "step//./input//syncFnExpr"; +// Sync function expression with let +export let syncFnExprLet = function transform(input) { + return String(input); +}; +syncFnExprLet.stepId = "step//./input//syncFnExprLet"; +(function(__wf_cls, __wf_id) { + var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: __wf_id, + writable: false, + enumerable: false, + configurable: false + }); +})(Service, "class//./input//Service"); +Service.run.workflowId = "workflow//./input//Service.run"; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-step.js new file mode 100644 index 0000000000..e1f45a935b --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-step.js @@ -0,0 +1,59 @@ +/**__internal_workflows{"workflows":{"input.js":{"Service.run":{"workflowId":"workflow//./input//Service.run"}}},"steps":{"input.js":{"Service.fetchData":{"stepId":"step//./input//Service.fetchData"},"Service.getConfig":{"stepId":"step//./input//Service.getConfig"}}},"classes":{"input.js":{"Service":{"classId":"class//./input//Service"}}}}*/; +// Test sync step functions in class contexts +export class Service { + // Sync static step method + static getConfig() { + return { + timeout: 30000 + }; + } + // Async static step method (for comparison) + static async fetchData(url) { + return { + url + }; + } + // Async static workflow method that calls sync and async steps + static async run() { + throw new Error("You attempted to execute workflow Service.run function directly. To start a workflow, use start(workflow) from workflow/api"); + } +} +// Sync function expression with var +export var syncFnExpr = function process(data) { + return data * 2; +}; +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(syncFnExpr, "step//./input//syncFnExpr"); +// Sync function expression with let +export let syncFnExprLet = function transform(input) { + return String(input); +}; +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(syncFnExprLet, "step//./input//syncFnExprLet"); +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(Service.getConfig, "step//./input//Service.getConfig"); +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(Service.fetchData, "step//./input//Service.fetchData"); +(function(__wf_cls, __wf_id) { + var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: __wf_id, + writable: false, + enumerable: false, + configurable: false + }); +})(Service, "class//./input//Service"); +Service.run.workflowId = "workflow//./input//Service.run"; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-workflow.js new file mode 100644 index 0000000000..58cb0c8f00 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-class/output-workflow.js @@ -0,0 +1,31 @@ +/**__internal_workflows{"workflows":{"input.js":{"Service.run":{"workflowId":"workflow//./input//Service.run"}}},"steps":{"input.js":{"Service.fetchData":{"stepId":"step//./input//Service.fetchData"},"Service.getConfig":{"stepId":"step//./input//Service.getConfig"}}},"classes":{"input.js":{"Service":{"classId":"class//./input//Service"}}}}*/; +// Test sync step functions in class contexts +export class Service { + // Async static workflow method that calls sync and async steps + static async run() { + const config = await Service.getConfig(); + const data = await Service.fetchData('/api'); + return { + config, + data + }; + } +} +// Sync function expression with var +export var syncFnExpr = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//syncFnExpr"); +// Sync function expression with let +export let syncFnExprLet = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//syncFnExprLet"); +Service.getConfig = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Service.getConfig"); +Service.fetchData = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Service.fetchData"); +(function(__wf_cls, __wf_id) { + var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_cls); + Object.defineProperty(__wf_cls, "classId", { + value: __wf_id, + writable: false, + enumerable: false, + configurable: false + }); +})(Service, "class//./input//Service"); +Service.run.workflowId = "workflow//./input//Service.run"; +globalThis.__private_workflows.set("workflow//./input//Service.run", Service.run); From c06665fdfacbdd308c9d3d1c625cfa89acfec014 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 8 Apr 2026 23:47:51 -0700 Subject: [PATCH 2/3] Tighten step file export validation, fix spec completeness - Reject uninitialized var exports (export let x) in step files - Reject local named exports (export { value }) in step files since we cannot statically verify the binding is a function - Add test coverage for both new rejection cases - Add sync variants for let/var arrow functions in spec.md --- packages/swc-plugin-workflow/spec.md | 6 ++++-- .../swc-plugin-workflow/transform/src/lib.rs | 16 +++++++++++--- .../tests/errors/invalid-exports/input.js | 11 ++++++++-- .../errors/invalid-exports/output-client.js | 13 +++++++++--- .../invalid-exports/output-client.stderr | 13 ++++++++++++ .../errors/invalid-exports/output-step.js | 21 ++++++++++++++++--- .../errors/invalid-exports/output-step.stderr | 13 ++++++++++++ .../errors/invalid-exports/output-workflow.js | 11 +++++++--- .../invalid-exports/output-workflow.stderr | 13 ++++++++++++ 9 files changed, 101 insertions(+), 16 deletions(-) diff --git a/packages/swc-plugin-workflow/spec.md b/packages/swc-plugin-workflow/spec.md index 24b2f97894..34f204eb1d 100644 --- a/packages/swc-plugin-workflow/spec.md +++ b/packages/swc-plugin-workflow/spec.md @@ -1079,8 +1079,10 @@ The plugin supports various function declaration styles. Step functions may be s - `function name() { "use step"; }` - Sync function declaration - `const name = async () => { "use step"; }` - Async arrow function - `const name = () => { "use step"; }` - Sync arrow function -- `let name = async () => { "use step"; }` - Arrow function with let -- `var name = async () => { "use step"; }` - Arrow function with var +- `let name = async () => { "use step"; }` - Async arrow function with let +- `let name = () => { "use step"; }` - Sync arrow function with let +- `var name = async () => { "use step"; }` - Async arrow function with var +- `var name = () => { "use step"; }` - Sync arrow function with var - `const name = async function() { "use step"; }` - Async function expression - `const name = function() { "use step"; }` - Sync function expression - `{ async method() { "use step"; } }` - Async object method diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index 37a5d834d4..c1652ad98d 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -6069,8 +6069,8 @@ impl VisitMut for StepTransform { } Decl::Var(var_decl) => { for decl in &var_decl.decls { - if let Some(init) = &decl.init { - match &**init { + match &decl.init { + Some(init) => match &**init { Expr::Fn(_) | Expr::Arrow(_) => { // Function/arrow expressions are allowed } @@ -6080,6 +6080,13 @@ impl VisitMut for StepTransform { directive: "use step", }); } + }, + None => { + // Uninitialized exports are not functions + emit_error(WorkflowErrorKind::InvalidExport { + span: export.span, + directive: "use step", + }); } } } @@ -6099,7 +6106,10 @@ impl VisitMut for StepTransform { } } ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => { - if named.src.is_some() { + // Re-exports (`export { x } from '...'`) are not allowed. + // Local named exports (`export { x }`) are also rejected + // because we cannot statically verify the binding is a function. + if named.src.is_some() || !named.specifiers.is_empty() { emit_error(WorkflowErrorKind::InvalidExport { span: named.span, directive: "use step", diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js index ed10f9f8cb..e32361feb6 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js @@ -6,13 +6,20 @@ export class MyClass { method() {} } export * from './other'; +export let uninitVar; -// This is ok - sync functions are allowed in "use step" files +// Local named exports also error (can't verify binding is a function) +const helper = 'not a function'; +export { helper }; + +// These are ok - sync and async functions are allowed in "use step" files export function syncFunc() { return 'allowed'; } -// This is ok export async function validStep() { return 'allowed'; } + +export const arrowStep = () => 'allowed'; +export const asyncArrow = async () => 'allowed'; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js index fc05c00e16..3e80fe9db4 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js @@ -1,17 +1,24 @@ -/**__internal_workflows{"steps":{"input.js":{"syncFunc":{"stepId":"step//./input//syncFunc"},"validStep":{"stepId":"step//./input//validStep"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"arrowStep":{"stepId":"step//./input//arrowStep"},"asyncArrow":{"stepId":"step//./input//asyncArrow"},"syncFunc":{"stepId":"step//./input//syncFunc"},"validStep":{"stepId":"step//./input//validStep"}}}}*/; // These should all error - not functions export const value = 42; export class MyClass { method() {} } export * from './other'; -// This is ok - sync functions are allowed in "use step" files +export let uninitVar; +// Local named exports also error (can't verify binding is a function) +const helper = 'not a function'; +export { helper }; +// These are ok - sync and async functions are allowed in "use step" files export function syncFunc() { return 'allowed'; } syncFunc.stepId = "step//./input//syncFunc"; -// This is ok export async function validStep() { return 'allowed'; } validStep.stepId = "step//./input//validStep"; +export const arrowStep = ()=>'allowed'; +arrowStep.stepId = "step//./input//arrowStep"; +export const asyncArrow = async ()=>'allowed'; +asyncArrow.stepId = "step//./input//asyncArrow"; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr index 1c1387e9e8..7ebdf7ddd6 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr @@ -18,4 +18,17 @@ 7 | } 8 | export * from './other'; : ^^^^^^^^^^^^^^^^^^^^^^^^ + 9 | export let uninitVar; `---- + x Only functions can be exported from a "use step" file + ,-[input.js:9:1] + 8 | export * from './other'; + 9 | export let uninitVar; + : ^^^^^^^^^^^^^^^^^^^^^ + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:13:1] + 12 | const helper = 'not a function'; + 13 | export { helper }; + : ^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js index d2b6dfdd04..6afe5da1b4 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js @@ -1,11 +1,15 @@ -/**__internal_workflows{"steps":{"input.js":{"syncFunc":{"stepId":"step//./input//syncFunc"},"validStep":{"stepId":"step//./input//validStep"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"arrowStep":{"stepId":"step//./input//arrowStep"},"asyncArrow":{"stepId":"step//./input//asyncArrow"},"syncFunc":{"stepId":"step//./input//syncFunc"},"validStep":{"stepId":"step//./input//validStep"}}}}*/; // These should all error - not functions export const value = 42; export class MyClass { method() {} } export * from './other'; -// This is ok - sync functions are allowed in "use step" files +export let uninitVar; +// Local named exports also error (can't verify binding is a function) +const helper = 'not a function'; +export { helper }; +// These are ok - sync and async functions are allowed in "use step" files export function syncFunc() { return 'allowed'; } @@ -14,7 +18,6 @@ export function syncFunc() { __wf_reg.set(__wf_id, __wf_fn); __wf_fn.stepId = __wf_id; })(syncFunc, "step//./input//syncFunc"); -// This is ok export async function validStep() { return 'allowed'; } @@ -23,3 +26,15 @@ export async function validStep() { __wf_reg.set(__wf_id, __wf_fn); __wf_fn.stepId = __wf_id; })(validStep, "step//./input//validStep"); +export const arrowStep = ()=>'allowed'; +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(arrowStep, "step//./input//arrowStep"); +export const asyncArrow = async ()=>'allowed'; +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(asyncArrow, "step//./input//asyncArrow"); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr index 1c1387e9e8..7ebdf7ddd6 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr @@ -18,4 +18,17 @@ 7 | } 8 | export * from './other'; : ^^^^^^^^^^^^^^^^^^^^^^^^ + 9 | export let uninitVar; `---- + x Only functions can be exported from a "use step" file + ,-[input.js:9:1] + 8 | export * from './other'; + 9 | export let uninitVar; + : ^^^^^^^^^^^^^^^^^^^^^ + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:13:1] + 12 | const helper = 'not a function'; + 13 | export { helper }; + : ^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js index ce19634b23..2d36e5966c 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js @@ -1,4 +1,4 @@ -/**__internal_workflows{"steps":{"input.js":{"syncFunc":{"stepId":"step//./input//syncFunc"},"validStep":{"stepId":"step//./input//validStep"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"arrowStep":{"stepId":"step//./input//arrowStep"},"asyncArrow":{"stepId":"step//./input//asyncArrow"},"syncFunc":{"stepId":"step//./input//syncFunc"},"validStep":{"stepId":"step//./input//validStep"}}}}*/; 'use step'; // These should all error - not functions export const value = 42; @@ -6,7 +6,12 @@ export class MyClass { method() {} } export * from './other'; -// This is ok - sync functions are allowed in "use step" files +export let uninitVar; +// Local named exports also error (can't verify binding is a function) +const helper = 'not a function'; +export { helper }; +// These are ok - sync and async functions are allowed in "use step" files export var syncFunc = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//syncFunc"); -// This is ok export var validStep = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//validStep"); +export const arrowStep = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//arrowStep"); +export const asyncArrow = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//asyncArrow"); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr index 1c1387e9e8..7ebdf7ddd6 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr @@ -18,4 +18,17 @@ 7 | } 8 | export * from './other'; : ^^^^^^^^^^^^^^^^^^^^^^^^ + 9 | export let uninitVar; `---- + x Only functions can be exported from a "use step" file + ,-[input.js:9:1] + 8 | export * from './other'; + 9 | export let uninitVar; + : ^^^^^^^^^^^^^^^^^^^^^ + `---- + x Only functions can be exported from a "use step" file + ,-[input.js:13:1] + 12 | const helper = 'not a function'; + 13 | export { helper }; + : ^^^^^^^^^^^^^^^^^^ + `---- From 02d6d56022ebdaecd20582b36acafbebd9df003b Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 8 Apr 2026 23:56:41 -0700 Subject: [PATCH 3/3] Add test fixtures for remaining export validation edge cases - Add re-export with specifiers to invalid-exports (export { x } from) - Add error fixture for default class export in step files - Add fixture for sync default function export (should pass) - Total: 220 tests (165 fixture + 33 error + 22 unit) --- .../tests/errors/invalid-default-exports-step/input.js | 6 ++++++ .../invalid-default-exports-step/output-client.js | 4 ++++ .../invalid-default-exports-step/output-client.stderr | 7 +++++++ .../errors/invalid-default-exports-step/output-step.js | 4 ++++ .../invalid-default-exports-step/output-step.stderr | 7 +++++++ .../invalid-default-exports-step/output-workflow.js | 5 +++++ .../output-workflow.stderr | 7 +++++++ .../transform/tests/errors/invalid-exports/input.js | 3 +++ .../tests/errors/invalid-exports/output-client.js | 2 ++ .../tests/errors/invalid-exports/output-client.stderr | 6 ++++++ .../tests/errors/invalid-exports/output-step.js | 2 ++ .../tests/errors/invalid-exports/output-step.stderr | 6 ++++++ .../tests/errors/invalid-exports/output-workflow.js | 2 ++ .../errors/invalid-exports/output-workflow.stderr | 6 ++++++ .../tests/fixture/sync-step-default-export/input.js | 5 +++++ .../fixture/sync-step-default-export/output-client.js | 6 ++++++ .../fixture/sync-step-default-export/output-step.js | 10 ++++++++++ .../sync-step-default-export/output-workflow.js | 5 +++++ 18 files changed, 93 insertions(+) create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.js create mode 100644 packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.stderr create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/input.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-client.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-step.js create mode 100644 packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-workflow.js diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/input.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/input.js new file mode 100644 index 0000000000..efd17a94ff --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/input.js @@ -0,0 +1,6 @@ +'use step'; + +// Error: default class export +export default class MyClass { + method() {} +} diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.js new file mode 100644 index 0000000000..99415cbbd6 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.js @@ -0,0 +1,4 @@ +// Error: default class export +export default class MyClass { + method() {} +} diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.stderr new file mode 100644 index 0000000000..27e021d88a --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-client.stderr @@ -0,0 +1,7 @@ + x Only functions can be exported from a "use step" file + ,-[input.js:4:1] + 3 | // Error: default class export + 4 | ,-> export default class MyClass { + 5 | | method() {} + 6 | `-> } + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.js new file mode 100644 index 0000000000..99415cbbd6 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.js @@ -0,0 +1,4 @@ +// Error: default class export +export default class MyClass { + method() {} +} diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.stderr new file mode 100644 index 0000000000..27e021d88a --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-step.stderr @@ -0,0 +1,7 @@ + x Only functions can be exported from a "use step" file + ,-[input.js:4:1] + 3 | // Error: default class export + 4 | ,-> export default class MyClass { + 5 | | method() {} + 6 | `-> } + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.js new file mode 100644 index 0000000000..df93752564 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.js @@ -0,0 +1,5 @@ +'use step'; +// Error: default class export +export default class MyClass { + method() {} +} diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.stderr new file mode 100644 index 0000000000..27e021d88a --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-default-exports-step/output-workflow.stderr @@ -0,0 +1,7 @@ + x Only functions can be exported from a "use step" file + ,-[input.js:4:1] + 3 | // Error: default class export + 4 | ,-> export default class MyClass { + 5 | | method() {} + 6 | `-> } + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js index e32361feb6..54cffb01da 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/input.js @@ -12,6 +12,9 @@ export let uninitVar; const helper = 'not a function'; export { helper }; +// Re-export with specifiers also errors +export { something } from './re-export'; + // These are ok - sync and async functions are allowed in "use step" files export function syncFunc() { return 'allowed'; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js index 3e80fe9db4..bd319d78ed 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.js @@ -9,6 +9,8 @@ export let uninitVar; // Local named exports also error (can't verify binding is a function) const helper = 'not a function'; export { helper }; +// Re-export with specifiers also errors +export { something } from './re-export'; // These are ok - sync and async functions are allowed in "use step" files export function syncFunc() { return 'allowed'; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr index 7ebdf7ddd6..4aef762b52 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-client.stderr @@ -32,3 +32,9 @@ 13 | export { helper }; : ^^^^^^^^^^^^^^^^^^ `---- + x Only functions can be exported from a "use step" file + ,-[input.js:16:1] + 15 | // Re-export with specifiers also errors + 16 | export { something } from './re-export'; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js index 6afe5da1b4..70bfcc8d95 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.js @@ -9,6 +9,8 @@ export let uninitVar; // Local named exports also error (can't verify binding is a function) const helper = 'not a function'; export { helper }; +// Re-export with specifiers also errors +export { something } from './re-export'; // These are ok - sync and async functions are allowed in "use step" files export function syncFunc() { return 'allowed'; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr index 7ebdf7ddd6..4aef762b52 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-step.stderr @@ -32,3 +32,9 @@ 13 | export { helper }; : ^^^^^^^^^^^^^^^^^^ `---- + x Only functions can be exported from a "use step" file + ,-[input.js:16:1] + 15 | // Re-export with specifiers also errors + 16 | export { something } from './re-export'; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js index 2d36e5966c..5255267516 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.js @@ -10,6 +10,8 @@ export let uninitVar; // Local named exports also error (can't verify binding is a function) const helper = 'not a function'; export { helper }; +// Re-export with specifiers also errors +export { something } from './re-export'; // These are ok - sync and async functions are allowed in "use step" files export var syncFunc = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//syncFunc"); export var validStep = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//validStep"); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr index 7ebdf7ddd6..4aef762b52 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/invalid-exports/output-workflow.stderr @@ -32,3 +32,9 @@ 13 | export { helper }; : ^^^^^^^^^^^^^^^^^^ `---- + x Only functions can be exported from a "use step" file + ,-[input.js:16:1] + 15 | // Re-export with specifiers also errors + 16 | export { something } from './re-export'; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/input.js new file mode 100644 index 0000000000..5154bdfef1 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/input.js @@ -0,0 +1,5 @@ +// Default export of sync function should be allowed in "use step" files +export default function processData(input) { + 'use step'; + return input * 2; +} diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-client.js new file mode 100644 index 0000000000..a276434513 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-client.js @@ -0,0 +1,6 @@ +/**__internal_workflows{"steps":{"input.js":{"processData":{"stepId":"step//./input//processData"}}}}*/; +// Default export of sync function should be allowed in "use step" files +export default function processData(input) { + return input * 2; +} +processData.stepId = "step//./input//processData"; diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-step.js new file mode 100644 index 0000000000..3588fd651f --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-step.js @@ -0,0 +1,10 @@ +/**__internal_workflows{"steps":{"input.js":{"processData":{"stepId":"step//./input//processData"}}}}*/; +// Default export of sync function should be allowed in "use step" files +export default function processData(input) { + return input * 2; +} +(function(__wf_fn, __wf_id) { + var __wf_sym = Symbol.for("@workflow/core//registeredSteps"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); + __wf_reg.set(__wf_id, __wf_fn); + __wf_fn.stepId = __wf_id; +})(processData, "step//./input//processData"); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-workflow.js new file mode 100644 index 0000000000..0ff40b3f4d --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/sync-step-default-export/output-workflow.js @@ -0,0 +1,5 @@ +/**__internal_workflows{"steps":{"input.js":{"processData":{"stepId":"step//./input//processData"}}}}*/; +// Default export of sync function should be allowed in "use step" files +export default function processData(input) { + return globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//processData")(input); +}