The SWC compiler plugin no longer generates import statements. All step
function registrations and closure variable access are now self-contained
inline IIFEs with zero module dependencies
Use named export instead of `export =` in the CJS shim so that
Node.js cjs-module-lexer can detect withWorkflow as a named export,
enabling `import { withWorkflow } from 'workflow/next'` in ESM.
* Add detect mode to SWC plugin for false positive directive filtering
Add a new 'detect' mode to the SWC workflow plugin that walks the AST
to find directives and serde patterns and emits the manifest, but does
not transform any code. The discover-entries plugin now uses a two-phase
approach: fast regexp pre-scan to filter out most files, then SWC detect
mode on candidates to validate at the AST level. This eliminates false
positives where directive-like strings appear inside template literals
or other non-code contexts. The mode:false syntax-only transform is also
removed since esbuild handles TypeScript natively.
* Keep SWC syntax transform in discover phase for decorator support
esbuild does not support legacy decorators or emitDecoratorMetadata,
so all files still need the SWC syntax transform (TS→JS) during
discovery. For regexp-matched files the 'detect' mode handles both
the syntax transform and manifest in a single pass; for all other
files the existing mode:false call is used.
* Use Set for discoveredWorkflows/Steps/SerdeFiles
Eliminates the manual .includes() dedup check and prevents duplicate
entries structurally.
* Update DeferredDiscoveredEntries to use Set<string>
* Add @workflow/next to changeset
* Support getter functions with "use step" directive
Add SWC compiler plugin support for JavaScript getters marked with
"use step", enabling patterns like `await obj.prop` where the getter
triggers a step function invocation.
- Handle Prop::Getter in object literals and MethodKind::Getter in classes
- Emit Object.getOwnPropertyDescriptor registration in step mode
- Emit hoisted proxy + Object.defineProperty in workflow mode
- Emit error for getters with "use workflow"
- Fix @vercel/workflow -> @workflow/serde imports in existing fixtures
- Update spec.md with getter transformation documentation
* Add changeset for getter step support
* Add e2e test for getter step functions
* Add static getter support, sanitize hoisted var identifiers
Address PR review feedback:
- Support static getters with "use step" using ClassName (not .prototype)
- Add sanitize_ident_part() to produce valid JS identifiers from getter
names that may contain special characters (e.g. string literal keys)
- Add static-getter-step test fixture
- Update spec.md with static getter transformation documentation
* Remove duplicate getter workflow error in visit_mut_prop_or_spread
The previous pre-release versions (4.x.y-beta.N) caused two issues:
- semver.inc('4.0.0-beta.N', 'major') returns 4.0.0, not 5.0.0
- Pre-release numbers carried over (beta.61 -> beta.62 instead of beta.0)
Setting all versions to 4.0.0 (non-pre-release) ensures a clean major
bump to 5.0.0-beta.0. Also removes @workflow/swc-playground-wasm from
the changeset and pre.json since it is a private package.
The `dataDir` option was accepted in the type definition but never
read — `WORKFLOW_LOCAL_DATA_DIR` was unconditionally set to
`.next/workflow-data`. Remove the dead option to avoid confusion.
- Pass DOMException through to the workflow VM context alongside other
Web APIs (Headers, URL, TextEncoder, etc.)
- Add DOMException reducer/reviver to the serialization pipeline,
preserving message, name, derived code, and cause when present
- Add DOMException to the Serializable type union
- 8 new tests: round-trip for AbortError, NotFoundError, default name,
cause preservation, cause absence, serialization key, cross-VM
boundaries, and VM context availability
* fix(builders): override sideEffects:false for discovered workflow/step/serde entries
When node_modules packages include "sideEffects": false in their
package.json, esbuild drops bare imports from the virtual-entry.js
file. This is incorrect because the SWC compiler transform injects
side-effectful registration code (workflow IDs, step IDs, class
serialization) into these modules.
Fix: return the resolved path alongside sideEffects: true from the
onResolve handler so esbuild uses the plugin's resolution result
instead of re-reading the package.json.
* refactor(builders): normalize sideEffectEntries with realpaths for symlink compatibility
Extract withRealpaths() helper and use it for both normalizedEntriesToBundle
and sideEffectEntries at all three bundle sites. This ensures the
sideEffects override works correctly under pnpm/workspace symlinked
layouts where enhanced-resolve may return realpaths that differ from
the original discovered file paths.
* perf(builders): skip enhanced-resolve for transitive imports when only sideEffectEntries is set
When entriesToBundle is not set (workflow/client bundles), only top-level
import statements need the sideEffects override — transitive imports
from deep within the bundle are not bare imports and don't need resolution.
Skip enhanced-resolve for non-import-statement kinds to reduce overhead.
* fix(swc-plugin): use binding name for class expression method registrations
When a pre-bundled package (e.g. via tsup) contains class expressions
like `var Foo = class _Foo { ... }`, the internal name `_Foo` is only
scoped inside the class body. The SWC plugin was incorrectly using the
internal name for method step registrations and class serialization
registrations emitted at module scope, causing ReferenceError at runtime.
Fix: always use the binding name (registration_name) for
current_class_name in visit_mut_class_expr, consistent with the existing
handling for anonymous class expressions. This ensures:
- registerStepFunction calls reference the binding name (Foo)
- Only one class registration IIFE is emitted (not duplicates for both
Foo and _Foo)
- Step IDs use the binding name in their qualified path
* refactor(swc-plugin): rename internal_class_name to tracked_class_name for clarity
The variable no longer represents the internal class expression identifier
after being reassigned to the binding name. Rename to tracked_class_name
and eliminate the intermediate registration_name variable to make the
intent clearer and reduce confusion for future readers.
* fix(swc-plugin): rewrite anonymous export default class to const declaration
When an anonymous class with serde/step methods is exported as a default
export (`export default class { ... }`), the generated registration code
(registerStepFunction, class registry IIFE) would reference a nonexistent
variable at module scope, causing a ReferenceError at runtime.
Fix: detect anonymous default class exports in visit_mut_export_default_decl
and visit_mut_export_default_expr, generate a unique binding name
(__defaultClass), and defer a rewrite in visit_mut_module_items that
transforms the export into:
const __defaultClass = class __defaultClass { ... };
export default __defaultClass;
Named default class exports (export default class Foo { ... }) are
handled by setting current_class_binding_name so the transformer
uses the existing class name for registration code.
* refactor(swc-plugin): rename __defaultClass to __DefaultClass for class naming convention
* fix(swc-plugin): fix panic for step-only anonymous default class exports
Address review feedback:
- Remove expect() that panicked when anonymous default class had step
methods but no serde methods (ident was not re-inserted). Keep
const_name in a local variable instead of relying on class_expr.ident.
- Add debug_assert for single default class export invariant.
- Update spec.md and test fixture inputs to use new this() instead of
referencing the generated binding name.
- Add step-only anonymous default class fixture to cover the bug path.
* refactor(swc-plugin): address review feedback for export default class handling
- Remove dead Expr::Class handler in visit_mut_export_default_expr
(SWC wraps parenthesized form in Expr::Paren, so it never fires)
- Extract class_needs_binding_rewrite() helper, eliminating duplicated
detection logic and unnecessary body clones
- Add debug_assert for mutual exclusivity of default_workflow_exports
and default_class_exports
- Clarify spec.md on self-name behavior difference between serde and
step-only classes
* Fix node-module-error plugin matching identifiers in multi-line comments
The findIdentifierUsage function only stripped single-line comments and
same-line block comments, but didn't track multi-line block comment state.
Lines inside JSDoc/block comments (e.g. ` * ... Writable stream`) passed
through unstripped, causing the plugin to point at comments instead of
actual code usage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Strip string literals before scanning for comment delimiters
Move string stripping before comment detection so that comment delimiters
inside string literals (e.g. `const s = "/*"`) don't incorrectly trigger
block comment mode. Adds regression test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
* fix(builders): override sideEffects:false for discovered workflow/step/serde entries
When node_modules packages include "sideEffects": false in their
package.json, esbuild drops bare imports from the virtual-entry.js
file. This is incorrect because the SWC compiler transform injects
side-effectful registration code (workflow IDs, step IDs, class
serialization) into these modules.
Fix: return the resolved path alongside sideEffects: true from the
onResolve handler so esbuild uses the plugin's resolution result
instead of re-reading the package.json.
* refactor(builders): normalize sideEffectEntries with realpaths for symlink compatibility
Extract withRealpaths() helper and use it for both normalizedEntriesToBundle
and sideEffectEntries at all three bundle sites. This ensures the
sideEffects override works correctly under pnpm/workspace symlinked
layouts where enhanced-resolve may return realpaths that differ from
the original discovered file paths.
* perf(builders): skip enhanced-resolve for transitive imports when only sideEffectEntries is set
When entriesToBundle is not set (workflow/client bundles), only top-level
import statements need the sideEffects override — transitive imports
from deep within the bundle are not bare imports and don't need resolution.
Skip enhanced-resolve for non-import-statement kinds to reduce overhead.
* fix(swc-plugin): use binding name for class expression method registrations
When a pre-bundled package (e.g. via tsup) contains class expressions
like `var Foo = class _Foo { ... }`, the internal name `_Foo` is only
scoped inside the class body. The SWC plugin was incorrectly using the
internal name for method step registrations and class serialization
registrations emitted at module scope, causing ReferenceError at runtime.
Fix: always use the binding name (registration_name) for
current_class_name in visit_mut_class_expr, consistent with the existing
handling for anonymous class expressions. This ensures:
- registerStepFunction calls reference the binding name (Foo)
- Only one class registration IIFE is emitted (not duplicates for both
Foo and _Foo)
- Step IDs use the binding name in their qualified path
* refactor(swc-plugin): rename internal_class_name to tracked_class_name for clarity
The variable no longer represents the internal class expression identifier
after being reassigned to the binding name. Rename to tracked_class_name
and eliminate the intermediate registration_name variable to make the
intent clearer and reduce confusion for future readers.
* fix(builders): override sideEffects:false for discovered workflow/step/serde entries
When node_modules packages include "sideEffects": false in their
package.json, esbuild drops bare imports from the virtual-entry.js
file. This is incorrect because the SWC compiler transform injects
side-effectful registration code (workflow IDs, step IDs, class
serialization) into these modules.
Fix: return the resolved path alongside sideEffects: true from the
onResolve handler so esbuild uses the plugin's resolution result
instead of re-reading the package.json.
* refactor(builders): normalize sideEffectEntries with realpaths for symlink compatibility
Extract withRealpaths() helper and use it for both normalizedEntriesToBundle
and sideEffectEntries at all three bundle sites. This ensures the
sideEffects override works correctly under pnpm/workspace symlinked
layouts where enhanced-resolve may return realpaths that differ from
the original discovered file paths.
* perf(builders): skip enhanced-resolve for transitive imports when only sideEffectEntries is set
When entriesToBundle is not set (workflow/client bundles), only top-level
import statements need the sideEffects override — transitive imports
from deep within the bundle are not bare imports and don't need resolution.
Skip enhanced-resolve for non-import-statement kinds to reduce overhead.
* fix: update types and documentation for start function overloads
Ensure types are 'unknown[]' and 'unknown' for 'deploymentId' and update exports and documentation.
Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1773368990070059?thread_ts=1773368990.070059&cid=C09G3EQAL84
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
* fix: use generics in deploymentId overloads to avoid contravariance issue
Addresses PR review feedback: typed workflows like
WorkflowFunction<[string], number> were not assignable to
WorkflowFunction<unknown[], unknown> under strictFunctionTypes.
Changed to generic parameters while keeping Run<unknown> return type.
Also adds type-level tests for overload resolution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add changeset for start() deploymentId type changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Rename 'Workflow Development Kit' / 'DevKit' to 'Workflow SDK' across docs, code, and config
Follow-up to cdf90d5a38 (#1541)
* Fix missing </h1> closing tag and add article 'the' before 'Workflow SDK' in docs
* fix: check target run capabilities before encrypting hook payloads
When resumeHook()/resumeWebhook() is called on a newer deployment that
supports encryption, it would encode the payload with the 'encr' format.
If the target workflow run was created by an older deployment that
predates encryption support, the run would fail with:
Error: Unknown serialization format: "encr". Known formats: devl
Add a capabilities table that maps @workflow/core versions to supported
serialization formats. Before encoding, resumeHook() now checks the
target run's workflowCoreVersion and suppresses encryption when the
run's deployment doesn't support it.
* address review: guard against invalid/non-string workflowCoreVersion
- Validate with semver.valid() before comparing, falling back to
baseline formats for malformed version strings
- Add typeof guard at the call site in resumeHook() since
executionContext is Record<string, any>
- Add tests for invalid version strings (dev, empty, partial, etc.)
- Add encryption commit reference to capabilities module header
In Nitro/Nuxt local production builds, the generated workflow/steps.mjs bundle could be treated as a pure export provider when the virtual handler only imports { POST }. That allowed the step bundle's top-level registerStepFunction(...) calls to disappear from the final Nitro server bundle, which led to runtime failures like Step \"...\" not found when a queued step executes.
Signed-off-by: comfuture <comfuture@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>