Commit Graph

64 Commits

Author SHA1 Message Date
Nathan Colosimo c29200fac5 docs(ai): clean up WorkflowAgent docs and examples (#3891)
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
2026-09-11 13:39:43 -07:00
Nathan Rajlich acb6b1370a test(swc-plugin): verify class-name preservation at runtime (#4015) 2026-09-10 14:34:59 -07:00
Nathan Rajlich ae5ee5ba2e fix(swc-plugin): register class expressions via an IIFE instead of by name (#3971)
* fix(swc-plugin): register class expressions via an IIFE and reject unnameable classes

Class expressions with "use step" methods or custom serialization were
registered by module-level statements referencing the class by name. When no
module-scope binding could be resolved the plugin fell back to a placeholder
`AnonymousClass` identifier, which is a guaranteed ReferenceError at module
evaluation (vercel/workflow#3929). Other shapes were silently wrong as well:
`var A = class {}, B = class {}` registered A's steps under B, `X = class {}`
assignments and classes nested inside functions emitted unresolvable
references.

Class expressions are now wrapped in a single IIFE that receives the class,
performs every registration recorded for it, and returns it, so the
registration no longer depends on a name being in scope. The class name is
still needed for step/class IDs and is derived from the assigned variable,
the class's own identifier, or the property key it is assigned to
(`exports.Foo = class {}`, `{ Foo: class {} }`). When none is available, or
the class is declared inside a function, the plugin emits a compile error
instead of broken code.

Class declarations keep their existing module-level output; the emitters
were factored so both paths share the same statement builders.

* fix(swc-plugin): generate names for anonymous class expressions instead of erroring

With registration happening inside the IIFE, an anonymous class expression
in a position that provides no name (`foo(class { ... })`, an array element,
a conditional branch) only needs a name for its step/class IDs. Generate a
deterministic `AnonymousClass<N>`, counting only anonymous classes that have
something to register, instead of rejecting them. Classes declared inside a
function remain an error.

Dead-code elimination now keeps module-level declarations whose initializer
contains a wrapped class expression: evaluating the initializer is what
registers the class, and the binding may be otherwise unreferenced.
2026-09-08 10:14:31 -07:00
Karthik Kalyan da373493d2 [swc-plugin] Fix eager discovery for object property steps (#2484)
* Fix eager discovery for object property steps

* Add changeset for object property step discovery
2026-06-17 12:44:21 -07:00
Pranay Prakash 53ede3079c fix(swc-plugin): count destructuring-default references in DCE usage analysis (#2398)
The DCE usage collector skipped the entire variable name pattern when
visiting a `VarDeclarator` (to avoid marking the binding name as "used").
But default-value initializers inside destructuring patterns live in that
pattern — e.g. the `TTL` in `const { ttl = TTL } = options;` — so those
references were invisible to the collector. A module-scope `const`
referenced only through such a default was treated as unused and stripped,
while the surviving code kept reading it, producing a runtime
`ReferenceError` when the default fired.

Traverse the default-value initializer expressions (and computed keys)
within destructuring patterns while still not marking the binding names
themselves, so the referenced declaration is preserved. Function-parameter
defaults were already covered (params are visited in full).

Fixes #2396.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:16:13 -07:00
Nathan Rajlich 1d4f83a29a [swc-plugin] Preserve imports referenced by hoisted nested steps (#1944)
* [swc-plugin] Preserve imports referenced by hoisted nested steps

Dead-code elimination ran before nested step functions were hoisted out of workflow bodies, so imports referenced only by hoisted step bodies were incorrectly stripped from the step bundle, causing a ReferenceError at runtime. Move DCE to run after hoisting in visit_mut_program.

* [swc-plugin] Namespace nested step IDs under non-exported workflow functions

Anonymous steps nested inside callback properties of a non-exported workflow function were registered with an unnamespaced step ID in step mode while the workflow-mode proxy looked them up under the workflow function name, causing a runtime 'step not found' failure. Set current_workflow_function_name in visit_mut_fn_decl for non-exported workflow functions to match the behavior in visit_mut_export_decl. Also clarify the fixture comment to distinguish step-mode and workflow-mode behavior per reviewer feedback.

* [swc-plugin] Namespace nested step IDs across all workflow declaration shapes

Extends the previous fix to cover all three non-exported workflow declaration forms (async function decl, const arrow, const fn-expr) by visiting the workflow body with workflow context before replacing it, and corrects the __internal_workflows manifest comment to report the same prefixed step IDs that are registered at runtime and looked up by the workflow-mode WORKFLOW_USE_STEP proxy. Adds a dedicated regression fixture covering all three shapes.
2026-05-05 21:36:54 +00:00
Nathan Rajlich d0e3f2722b [swc-plugin] Capture lexical this for nested arrow step functions (#1935)
* [swc-plugin] Capture lexical `this` for nested arrow step functions

When a nested arrow `"use step"` references the enclosing function/method's
`this`, plumb that `this` through the workflow runtime so the step body
sees the correct receiver.

- Workflow mode wraps the step proxy with `.bind(this)`, so invoking the
  proxy captures the caller's `this` as `thisVal` on the queue item.
- Step mode hoists the body as a regular `function` (not an arrow) so the
  runtime's `stepFn.apply(thisVal, args)` rebinds `this` inside the
  hoisted body.

Detection only fires for arrows, since arrows inherit `this` lexically.
Nested non-arrow functions/methods/getters/setters introduce their own
`this`, so the detector stops at those boundaries.

The runtime already supported `thisVal` for instance-method steps; this
PR is purely a compiler change to feed the existing pipeline.

Caveat: capture works at runtime only when the captured value is
serializable across the workflow->step boundary (i.e. the enclosing
class implements `WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE`).

Refs vercel/workflow#1865

* Address PR review: preserve step proxy metadata + tighter `this` detection

- core: Override `.bind` on step proxies so the bound function retains
  `stepId` and `__closureVarsFn`. Without this, a bound proxy that flows
  through workflow serialization (e.g. as a step argument) would be
  treated as a non-serializable plain function by `getStepFunctionReducer`.
- swc-plugin: Detector now also walks `arrow.params` so `this` references
  in default values / destructuring initializers (e.g. `(x = this.foo) =>
  ...`) trigger the `.bind(this)` path.
- swc-plugin: Class bodies inside the arrow body are now treated as
  `this`-binding boundaries — `this` inside class field initializers,
  methods, etc. is bound to the class instance, not the outer arrow. The
  detector still walks `extends` clauses and computed property keys
  because those are evaluated in the surrounding scope.
- spec.md: Sharpen the note about `this` in step bodies — it's
  syntactically allowed but only meaningful for instance-method steps and
  lexical-`this` arrow steps; other shapes compile but `this` will be
  whatever the caller of the step proxy passes.
- Add `lexical-this-detector-edge-cases` fixture covering both the
  default-param positive case and the inner-class false-positive guard.
- Strengthen the runtime test to assert `stepId` / `__closureVarsFn`
  survive `.bind(...)`.

* [swc-plugin] Fix `arguments` closure-var capture; drop dead `this`/`arguments` checks

- Add `arguments` to `is_global_identifier` so it's not captured as a
  closure variable. Previously a nested `function`-form step like

      function step() { 'use step'; return arguments[0]; }

  was hoisted with `const { arguments } = ...` (a strict-mode syntax
  error) and the body's `arguments[0]` resolved against the destructured
  binding instead of the function's intrinsic `arguments` object.
- Remove dead `ForbiddenExpression` checks for `this` and `arguments` in
  `visit_mut_this_expr` / `visit_mut_ident`. The `'use step'` /
  `'use workflow'` directives are stripped during the module-level
  traversal before children are visited, so `in_step_function` /
  `in_workflow_function` are never observed as true here in practice.
  The existing `step-with-this-arguments-super` fixture explicitly
  documents that all three identifiers are allowed in step bodies.
- Tighten the spec note about `arguments` accordingly: it works in
  `function`-form steps (reflecting positional args) but is not captured
  for arrow-form steps; use `...args` for that case.
- Add `nested-step-arguments` fixture pinning down the new behavior.
2026-05-05 19:19:42 +00:00
Nathan Rajlich 417c4930be refactor(swc-plugin): remove client transform mode, merge into step mode (#1686)
* refactor(swc-plugin): remove client transform mode, merge into step mode

Remove the `client` transform mode from the SWC compiler plugin. The
`client` and `step` modes were nearly identical — both preserved step
function bodies, replaced workflow bodies with throw stubs, and emitted
the same JSON manifest. Step mode now absorbs all client-mode behaviors:

- Dead code elimination (previously only workflow + client)
- Hoisted variable references for object property steps
- All integrations use mode: 'step' instead of 'client'

BREAKING CHANGE: The `client` value for the SWC plugin `mode` option is
no longer accepted. Use `step` instead.

* fix(nitro): force-inline workflow packages in dev mode for serde classId registration

In dev mode, Nitro's Rollup externalizes npm packages like @workflow/core,
so the SWC transform plugin never processes files like run.js. This means
serde classes (e.g. Run) never get the classId registration IIFE, causing
serialization failures when step functions return Run instances.

Uses a Rollup resolveId hook to force workflow SDK packages to be bundled
(non-external) while leaving all other dependencies external. This is more
targeted than noExternals=true which bundles everything and causes TDZ
errors from circular imports in packages like vue-bundle-renderer/h3.

The Nitro module now also ignores .nitro/workflow/** in watchOptions so
writing generated workflow bundles does not retrigger Nitro's own dev
bundle rebuild loop.

Also wraps dev:reload workflow rebuilds and makes LocalBuilder.build()
atomic (writes to temp files, renames on success) to avoid partial output
state during HMR.

For Nuxt, also configures Vite's ssr.noExternal to bundle workflow
packages in the SSR context.

* fix(nitro,nuxt): address review feedback on dev-mode classId fix

- nitro builders: use crypto.randomUUID() for temp file suffix instead of
  Date.now() to avoid collisions under rapid/concurrent build() calls,
  and serialize concurrent build() calls through an internal queue so
  two overlapping dev rebuilds cannot clobber each other's temp outputs.

- nitro index: use fileURLToPath() to convert file:// URLs to filesystem
  paths, which correctly handles Windows paths (file:///C:/... -> C:\...)
  and percent-decoding, instead of relying on new URL(...).pathname.

- nuxt module: normalize vite.ssr.noExternal to an array (preserving any
  existing string/RegExp/array entry) before appending workflow package
  matchers, so the force-bundle behavior is not a no-op when noExternal
  is already set to a non-array value.
2026-04-16 22:50:46 +00:00
Nathan Rajlich 136bd35a98 fix(swc-plugin): preserve step function names in stack traces after bundler minification (#1743)
* fix(swc-plugin): preserve step function names in stack traces after bundler minification

Add Object.defineProperty(fn, 'name', { value, configurable: true }) to the
IIFE step registration so the original function name survives bundler name
mangling (e.g. Turbopack renaming errorStepFn to eD). V8 uses the .name
property for stack traces, so this ensures 'at errorStepFn (...)' appears
instead of 'at eD (...)'.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 20:20:26 +00:00
Nathan Rajlich 66585fd467 feat(swc): dead code eliminate unreferenced private class members in workflow mode (#1671)
* feat(swc): dead code eliminate unreferenced private class members in workflow mode

After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.

The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).

* test(swc): add fixture for JS native private member DCE (#field, #method)

* fix(swc): address review feedback on private member DCE

- Namespace JS native private names with # prefix to avoid collisions
  with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
  static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
  expressions
- Extract retain logic into shared retain_referenced_private_members()
  helper used by both visit_mut_class_decl and visit_mut_class_expr
2026-04-09 12:27:06 -07:00
Nathan Rajlich ebb0a4a4e3 Followup fixes for sync step function support (#1664)
* 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)

* 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

* 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)
2026-04-09 17:32:47 +00:00
Nathan Rajlich d040182932 Allow synchronous functions to use "use step" directive (#1633)
Lift the async function restriction from "use step" in both the SWC
compiler plugin and the TypeScript language service plugin. This enables
using "use step" as a mechanism to strip Node.js-dependent code from
the workflow VM bundle without requiring the function to be async.

The async restriction is preserved for "use workflow" functions.

- SWC plugin: removed async guards from all step function code paths,
  updated should_transform_function, updated InvalidExport validation
- TypeScript plugin: removed error 9002 for sync step functions
- Added sync-step fixture test, cleaned up error test fixtures
- Updated spec.md error documentation
2026-04-09 00:34:42 +00:00
Nathan Rajlich 0a86de3afd Inline all SWC plugin step registrations, remove workflow/internal/private (#1632)
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
2026-04-08 16:58:17 -07:00
Nathan Rajlich 12791cf646 Add missing Detect arms for getter step match blocks 2026-04-07 17:33:07 -07:00
Nathan Rajlich 35b539b146 Add detect mode to SWC plugin for false positive directive filtering (#1641)
* 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
2026-04-07 16:53:55 -07:00
Nathan Rajlich bab8cddf98 Support getter functions with "use step" directive (#1630)
* 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
2026-04-07 23:35:55 +00:00
Nathan Rajlich 7c996a76c5 fix(swc-plugin): rewrite anonymous export default class to const declaration (#1601)
* 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
2026-04-03 19:54:20 +00:00
Nathan Rajlich 5d22e61446 fix(swc-plugin): use binding name for class expression method registrations (#1599)
* 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.
2026-04-03 18:36:27 +00:00
Nathan Rajlich 77fd9ad355 Inline class serialization registration to fix 3rd-party package support (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)

* Inline class serialization registration to fix 3rd-party package support

The SWC plugin previously generated:
  import { registerSerializationClass } from "workflow/internal/class-serialization";
  registerSerializationClass("class//...", ClassName);

This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.

Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:

  (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", { ... });
  })(ClassName, "class//...");

This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.

* Address review feedback: fix comment and update docstring

- Fix inaccurate IIFE comment in lib.rs: the second arg is the
  generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
  SWC plugin now inlines equivalent logic rather than importing it

* Update CJS require fixture outputs for inline class serialization

The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
2026-03-24 09:30:55 -03:00
Pranay Prakash 7c31cd7714 Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
This reverts commit 7dcddb5f33.
2026-03-23 17:41:04 -07:00
Nathan Rajlich 7dcddb5f33 Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support

The SWC plugin previously generated:
  import { registerSerializationClass } from "workflow/internal/class-serialization";
  registerSerializationClass("class//...", ClassName);

This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.

Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:

  (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", { ... });
  })(ClassName, "class//...");

This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.

* Address review feedback: fix comment and update docstring

- Fix inaccurate IIFE comment in lib.rs: the second arg is the
  generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
  SWC plugin now inlines equivalent logic rather than importing it
2026-03-23 23:06:28 +00:00
Nathan Rajlich 992d768f80 Add CommonJS require() support for class serialization detection in SWC plugin (#1144)
* Add CommonJS require() support for class serialization detection in SWC plugin

The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```

into either:

```ts
const serde_1 = require("@workflow/serde")    // namespace require
const { WORKFLOW_SERIALIZE } = require(...)    // destructured require
```

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

* Address review: tighten is_require_call and extract track_serialization_bindings helper

- Tighten is_require_call to require exactly one string literal argument,
  rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
  tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
2026-03-23 11:30:42 -07:00
Peter Wielander 823f58e5c6 Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
This reverts commit e889860984.
2026-03-20 17:04:28 -07:00
Pranay Prakash e889860984 Add support for calling start() inside workflow functions (#1133)
* Add support for calling `start()` directly inside workflow functions

Enable `start()` to work in workflow context by routing through an
internal step (`__workflow_start`), reusing existing step infrastructure
with no new event types or server changes needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address PR review feedback

- Use typeof check instead of truthiness for WORKFLOW_START symbol
- Validate start() options in workflow context (reject unsupported options like world)
- Set maxRetries=0 on __workflow_start step to prevent orphaned child runs
- Add unit tests for createStart factory (6 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Make Run serializable in workflow context with step-backed methods

- Add Run serialization via __serializable marker + custom Run reducer/reviver
  in the serialization module (avoids SWC plugin injecting class-serialization imports)
- Create WorkflowRun class factory (packages/core/src/workflow/run.ts) with
  step-backed methods: cancel(), status, returnValue, workflowName, createdAt,
  startedAt, completedAt, exists
- Register 8 built-in steps (__run_cancel, __run_status, etc.) in step-handler
- Update __workflow_start to return full Run object (serialized → WorkflowRun in VM)
- Update createStart to pass through step result directly
- Update docs to reflect full Run support in workflow context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix start() in workflow VM by delegating from api-workflow stub

The workflow VM loads api-workflow.ts (via the "workflow" export condition)
which stubs all runtime functions. The start stub needs to check for the
injected WORKFLOW_START symbol and delegate to it, otherwise start() throws
"doesn't allow this runtime usage" in the workflow context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR review: fix stale WORKFLOW_SERIALIZE comments and register Run in host registry

- Update comments in step-handler.ts and start.ts to reference the actual
  serialization mechanism (Run reducer with __serializable marker) instead
  of the stale WORKFLOW_SERIALIZE reference
- Register Run class in the host's class registry from step-handler.ts so
  the Run reviver can deserialize Run/WorkflowRun instances in step context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add docs for recursive/repeating workflows and deploymentId: "latest"

- Document using start() for self-chaining workflows to avoid large event logs
- Add examples for batch processing and cron-like repeating patterns
- Document deploymentId: "latest" option with type safety warning
- Update skill file with same patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Return full Run object from startFromWorkflow e2e workflow

Update the e2e workflow to return the childRun object directly instead of
just childRun.runId, exercising Run serialization across the workflow boundary.
Update e2e test assertions to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add recursive fibonacci e2e test for start() in workflow

Demonstrates recursive workflow composition: fibonacciWorkflow starts
new instances of itself via start() + Promise.all to compute fib(6)=8,
fanning out across independent workflow runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move Run method steps to builtins with "use step" directives

Refactor: instead of manually registering Run method steps via
registerStepFunction in step-handler.ts, define them as proper "use step"
functions in builtins.ts with __builtin_ prefix. This leverages the
existing SWC plugin infrastructure — functions starting with "__builtin"
get stable bare-name step IDs.

- Add __builtin_run_{cancel,status,return_value,...} to both builtins files
- Use dynamic import() for getRun inside step bodies to avoid pulling
  Node.js modules into the workflow bundle
- Remove manual registerStepFunction calls from step-handler.ts
- Update WorkflowRun step references to __builtin_run_* names
- Fix step name display in web observability: fall back to raw name
  instead of "?" for built-in steps that don't follow step//module//fn format
- Add fibonacciWorkflow default args for nextjs-turbopack workbench UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Render Run objects as clickable links in web observability UI

- Add RunRef type and Run reviver to observabilityRevivers so serialized
  Run objects are hydrated as RunRef instead of showing raw Uint8Array
- Add RunRefInline component (purple badge with run ID) that navigates
  to the target run on click, matching the StreamRef pattern
- Thread onRunClick callback through the component chain:
  WorkflowTraceViewer → EntityDetailPanel → AttributePanel → DataInspector
- Wire up navigation in the web app's run-detail-view
- Add startFromWorkflow default args for workbench UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Throw error instead of silent fallback when Run class not in registry

Address PR review: the Run reviver now throws if the class isn't found
in the registry, instead of silently returning a plain { runId } object
that would break the assumption of getting a valid Run instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix e2e failures: allow retries on Run getter steps, fix docs code samples

- Remove maxRetries=0 from read-only Run getter steps (status, returnValue,
  workflowName, etc.) — these are safe to retry and need retries when the
  child workflow hasn't completed within the step timeout. Only cancel
  keeps maxRetries=0.
- Fix docs code samples: use correct import path (workflow/api not workflow),
  add declare statements for helper functions used in examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use standard step//module//function naming for built-in steps

Update the SWC plugin's __builtin_ special case to generate proper
step//@workflow/core//{name} IDs instead of bare function names. This
makes parseStepName work correctly for built-in steps, showing:
- StepName: "Run#returnValue" (not "__builtin_run_return_value")
- ModuleSpecifier: "@workflow/core" (not the raw function name)

Convention: __builtin_Run_cancel → step//@workflow/core//Run#cancel
(uppercase prefix + underscore → instance method # notation)

- Move __workflow_start to builtins.ts as __builtin_start
- Rename __builtin_run_* to __builtin_Run_* for proper # notation
- Update WorkflowRun step refs to use full step// IDs
- Remove manual registerStepFunction from step-handler.ts
- Update SWC spec.md with new naming examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove SWC __builtin special case, use standard step naming for builtins

Remove the SWC plugin's __builtin_ special case so built-in steps get
standard step//{module}@{version}//{fn} IDs like any other step. This
makes parseStepName work correctly, showing proper StepName and
ModuleSpecifier in observability.

The VM reconstructs the same IDs via builtinStepId() which uses the
@workflow/core version to build: step//workflow/internal/builtins@{v}//{fn}

- Remove __builtin special case from SWC plugin (revert to original)
- Add builtinStepId() helper shared by workflow.ts, start.ts, run.ts
- Rename Run steps: __builtin_Run_cancel → Run_cancel, etc.
- Rename start step: __builtin_start → start
- Move start step from manual registerStepFunction to builtins.ts
- Keep __builtin_response_* names unchanged (pre-existing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use static class methods for Run steps to get Run.method naming

Refactor Run method steps from standalone functions (Run_cancel) to
static methods on a Run class, so the SWC plugin generates step IDs
with the standard static method convention: Run.cancel, Run.returnValue,
Run.status, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR review: tests, docs warnings, skill fix

- Add TODO on Run.returnValue about polling blocking (replace with system
  hooks once AbortSignal/AbortController PR lands)
- Add docs callout warning about returnValue holding workers alive
- Fix SKILL.md contradiction that said start() can't be used in workflows
- Enhance suspension test to assert step arguments are forwarded
- Add WorkflowRun unit tests: serializable marker, runId, registry, delegation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix response builtins: adopt this-serialization from PR #1413

The rebase onto main didn't fully adopt PR #1413's refactor of response
builtins to use `this` instead of explicit parameters. The old pattern
(resJson(this) wrappers) passed `this` as an argument, but the step
functions now expect `this` to be set via method call context.

Switch to Object.defineProperties on Request/Response prototypes,
matching main's approach. Also document WORKFLOW_PUBLIC_MANIFEST=1
for local e2e testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address docs review: returnValue polling is temporary, link to start() API ref

- Update returnValue warning to note this is a temporary implementation
  that will be replaced with internal hooks
- Replace inline deploymentId: "latest" docs with link to the existing
  start() API reference which already covers it comprehensively

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix e2e tests: replace collectedRunIds with trackRun API

PR #1426 replaced the manual collectedRunIds array with a trackRun()
helper. The start() wrapper already auto-tracks, so just remove the
manual push calls and add trackRun for the child run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 13:00:17 -07:00
Nathan Rajlich 5d95abf941 fix(swc-plugin): closure variable detection for new expressions and module-level declarations (#1368)
* fix(swc-plugin): closure variable detection for `new` expressions and module-level declarations

Fix two SWC compiler plugin bugs related to closure variable detection:

1. Add Expr::New handling to ClosureVariableCollector so `new Class(...args)`
   properly captures both the callee and arguments as closure variables.

2. Exclude module-level declarations (functions, variables, classes) from
   closure variable detection, preventing over-capturing of identifiers
   that are already available in all bundles. This also allows DCE to
   properly remove step-only helpers and their imports from the workflow
   bundle.

Fixes #1365

* fix(swc-plugin): handle additional expression/statement types in closure variable collector

Expand closure variable detection to cover more AST patterns:

Expressions: Seq (comma), Yield, OptChain, Prop::Shorthand,
computed property keys, Prop::Assign defaults, Class (skip bodies)

Statements: Throw, Try/Catch/Finally, Switch, ForIn, ForOf,
DoWhile, Labeled

Also fix existing Prop::Shorthand bug where object shorthand
properties like { url } were not being collected as closure vars.

Extend test fixture with cases for all newly handled patterns.
Fix spec.md wording per review feedback.

* fix(swc-plugin): preserve original step function bodies in enclosing functions

In step mode, nested step functions were replaced with bare references
to the hoisted copy (e.g., `return hoisted$fn;`). This broke direct
calls because the hoisted copy uses `__private_getClosureVars()` which
only works in workflow context.

Now the original function body is preserved inline with just the
directive stripped, so JavaScript's normal closure semantics work for
direct calls. The hoisted copy with `__private_getClosureVars()` is
still registered for workflow-driven execution.

Fixes #1369

* fix(swc-plugin): restore metadata tracking for object property steps in step mode

The previous commit accidentally removed the
object_property_workflow_conversions tracking from the step mode path,
causing __internal_workflows metadata to be stripped from step bundle
output for object property step functions.

* fix(swc-plugin): detect closure variables inside nested function/method bodies

The closure variable collector was skipping nested function expressions,
arrow functions, and method bodies entirely. This meant closure
variables used deep inside inner functions (e.g., a variable used inside
a ReadableStream's start() method) were not captured.

Now the collector walks into nested function/arrow/method/getter/setter
bodies while adding their parameters to the local var set, so only
truly free variables from the outer step scope are captured.

Also add ReadableStream, WritableStream, TransformStream, and other
common Web API globals to the known globals list.

* update changeset to include Bug 4

* fix(swc-plugin): handle TypeScript expression wrappers and class bodies in closure detection

After comparing with Next.js's SWC plugin closure detection approach,
identified and fixed remaining gaps:

- TypeScript expression wrappers (as, satisfies, !, type assertions,
  const assertions, instantiation expressions) now traverse to the
  inner expression instead of being silently skipped
- Class expressions and declarations now walk their body members
  (methods, properties, constructors, static blocks) to detect
  closure variables used inside them
- Document all remaining safe-to-skip Expr variants (This, Lit,
  SuperProp, MetaProp, PrivateName, Invalid, JSX)

* test: add fixture cases for TypeScript wrappers and class body closure detection

* test: add TypeScript fixture for closure detection through TS expression wrappers

Add a proper input.ts fixture that tests closure variable detection
through real TypeScript syntax: `as`, `satisfies`, `!` (non-null),
angle-bracket type assertions, `as const`, and generic function calls.

Update test harness to support input.ts files by adding swc_ecma_parser
dev-dependency and auto-detecting TypeScript syntax from file extension.

Remove the incorrectly placed TypeScript-related test cases from the JS
fixture (they were using plain JS syntax, not actual TS wrappers).
2026-03-16 23:04:36 +00:00
Nathan Colosimo d72c82220f Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation
* first pass

* fix failing test

* Update changeset to fix SWC compiler issue

Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation

Signed-off-by: Nathan Rajlich <n@n8.io>

* DCO Remediation Commit for nathancolosimo <nathancolosimo@gmail.com>

I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 150d175e7d
I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 429747fcc3

Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-03-11 09:11:19 -07:00
Nathan Rajlich 054e40c91b Fix anonymous class expression names for serialization classes (#991)
When upstream bundlers like tsup/esbuild transform class declarations into anonymous class expressions (e.g. `var Foo = class { ... }`), the SWC plugin now re-inserts the binding name into the class expression (e.g. `var Foo = class Foo { ... }`). This preserves the class `.name` property through subsequent bundling passes, preventing runtime failures in downstream bundlers like Nitro's Rollup bundler that rely on the class expression name for serialization class registration.
2026-02-11 23:34:01 +00:00
Nathan Rajlich fcfaf8bbaa Support step function serialization in client mode (#924) 2026-02-07 15:34:29 -08:00
Nathan Rajlich ef23b0be77 Fix step functions nested multiple levels deep in an object (#923)
Fixed a bug where step functions nested multiple levels deep in an object weren't being properly transformed.

### What changed?

- Enhanced the SWC plugin to recursively process nested objects to find step functions
- Added support for step functions defined inside deeply nested object properties, including function call arguments
- Implemented proper path handling for nested step IDs, using compound paths (e.g., `vade/tools/VercelRequest/execute`)
- Fixed variable name generation by replacing slashes with `$` to create valid JavaScript identifiers
- Updated documentation with examples of nested object property step functions
- Added test cases for deeply nested step functions and agent tool steps

### How to test?

Test with code that has step functions nested multiple levels deep in objects:

```javascript
export const config = createConfig({
  level1: {
    level2: {
      level3: {
        myStep: async (input) => {
          "use step";
          return input * 2;
        },
      },
    },
  },
});
```

Or with agent tools:

```javascript
export const vade = agent({
  tools: {
    VercelRequest: {
      execute: async (input, ctx) => {
        "use step";
        return 1 + 1;
      },
    },
  },
});
```

### Why make this change?

This fix ensures that step functions can be properly identified and transformed regardless of how deeply they are nested in object structures. This is particularly important for complex configurations like agent tools where step functions might be several levels deep in the object hierarchy.
2026-02-07 11:14:20 -08:00
Nathan Rajlich 35a9f0cb03 Add subpath export resolution for package IDs (#901)
Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

### What changed?

- Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
- Added support for subpath exports in package IDs (e.g., `workflow/internal/builtins@4.0.0`)
- Improved module resolution by passing absolute file paths to the SWC transform
- Enhanced manifest merging to properly combine results from both workflow and step bundles
- Updated builders to return and merge manifests from both workflow and step bundles

### How to test?

1. Build a project that uses subpath exports in packages
2. Verify that workflow IDs correctly include the subpath (e.g., `workflow/internal/builtins@4.0.0`)
3. Test with a project that has multiple builds to ensure module specifier caching works correctly

### Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.
2026-02-04 16:49:16 -08:00
Nathan Rajlich 73bf7be925 Change compiler ID generation logic to use Node.js import specifier (#899)
## Summary

This PR changes how the SWC compiler generates IDs for workflows, steps, and classes. Instead of using raw file paths, IDs are now based on **Node.js module specifiers** when the file belongs to a package (either in `node_modules` or a workspace package).

## Motivation

Previously, IDs were generated using file paths like `step//src/jobs/order.ts//fetchData`. This caused several issues:

1. **Package exports conditions**: When a package uses conditional exports (e.g., `"workflow"` vs `"default"` conditions in `package.json`), the same import specifier can resolve to different files. Using file paths meant IDs could differ based on which export condition was used.
2. **Cross-bundle consistency**: Classes serialized in one bundle couldn't be deserialized in another if the file paths differed.
3. **Version tracking**: No way to include package versions in IDs for cache invalidation.

## Changes

### New ID Format

IDs now use the format `{type}//{modulePath}//{identifier}` where `modulePath` is either:

- A **module specifier** like `point@0.0.1` or `@myorg/shared@1.2.3` for package files
- A **relative path** prefixed with `./` like `./src/jobs/order` for local app files

Examples:

- `step//workflow@4.0.1-beta.50//fetch` (SDK step)
- `step//./workflows/order//processOrder` (local step)
- `class//point@0.0.1//Point` (package class)
- `class//./src/models/User//User` (local class)

### New Module Specifier Resolution

Added `packages/builders/src/module-specifier.ts` which:

- Detects if a file is in `node_modules` or a workspace package
- Finds the nearest `package.json` and extracts name/version
- Returns the module specifier for the SWC plugin to use

### SWC Plugin Changes

- Added `moduleSpecifier` option to plugin config
- Updated `naming.rs` to support both module specifiers and relative paths
- Added `get_module_path()` helper that uses specifier when available, falls back to `./filename` format

### Special Cases

- **Builtin functions** (`__builtin_*`): Continue to use just the function name as the ID for stable, version-independent lookup from the workflow VM runtime.

## Testing

- Updated all 125+ SWC plugin test fixtures to use new ID format
- Added tests for module specifier resolution
- Added tests for Windows path normalization in naming

## Breaking Changes

This is technically a breaking change for any persisted workflow runs that reference the old ID format. However, since IDs are internal implementation details and not user-facing, this should not affect end users.

## Files Changed

- `packages/builders/src/module-specifier.ts` - **NEW**: Module specifier resolution logic
- `packages/builders/src/apply-swc-transform.ts` - Pass module specifier to SWC plugin
- `packages/builders/src/base-builder.ts` - Use `getImportPath` for virtual entry imports
- `packages/swc-plugin-workflow/transform/src/lib.rs` - Accept and use module specifier
- `packages/swc-plugin-workflow/transform/src/naming.rs` - New ID formatting with module paths
- `packages/swc-plugin-workflow/spec.md` - Updated documentation
- `packages/core/e2e/e2e.test.ts` - Updated test assertions for new ID format
2026-02-04 14:23:02 -08:00
Nathan Rajlich c1d7c8dbb4 Add support for "use step" functions in class instance methods (#777)
Added support for `"use step"` directive in class instance methods, allowing instance methods to be used as workflow steps.

### What changed?

- Modified the SWC plugin to recognize and transform instance methods with the "use step" directive
- Added registration logic for instance method steps using `ClassName.prototype.methodName`
- Implemented proper serialization of class instances to preserve the `this` context across workflow/step boundaries
- Added comprehensive end-to-end tests for instance method steps
- Updated error handling to allow "use step" in instance methods while still preventing "use workflow" in instance methods

### How to test?

The PR includes a new end-to-end test `instanceMethodStepWorkflow` that demonstrates the functionality:

1. Run the e2e tests to verify the new instance method step functionality
2. The test creates a `Counter` class with instance methods marked as steps
3. It verifies that the instance methods can be called as steps with proper serialization of the `this` context
4. It also verifies that multiple instances of the same class can be used independently

### Why make this change?

This change enables a more natural object-oriented programming model when working with workflows. Previously, only static methods, standalone functions, and object methods could be marked as steps. Now, developers can create classes with instance methods that are steps, allowing for better encapsulation and more intuitive code organization. This is particularly useful for complex workflows that need to maintain state across multiple step invocations.
2026-02-03 00:11:38 -08:00
Nathan Rajlich b9c782d75f Fix class ID generation when class is bound to a variable (#872)
Fixed class ID generation for class expressions bound to variables in the SWC plugin.

### What changed?

- Added support for correctly handling class expressions with binding names that differ from internal class names
- Updated the SWC plugin to use the variable name (binding name) for registration instead of the internal class name
- Added test cases for class expressions with different binding names and anonymous class expressions
- Updated documentation to explain how class expressions with binding names are handled

### How to test?

Test with code patterns like:
```javascript
var Bash = class _Bash {
  // Class implementation with serialization methods
};
```

Verify that the generated code correctly uses the binding name "Bash" for registration rather than the internal name "_Bash".

### Why make this change?

When a class expression is assigned to a variable, the internal class name is only accessible inside the class body, not at module level. Using the binding name ensures that the registration call references a symbol that's actually in scope at module level, preventing runtime errors when the code attempts to register the class for serialization.
2026-02-02 23:22:28 -08:00
Nathan Rajlich 244b94a066 Add support for top-level using declarations inside of step / workflow functions (#866) 2026-01-27 10:21:11 -08:00
Nathan Rajlich 81c5a835ae Add "classes" object to manifest.json file (#864) 2026-01-27 00:35:34 -08:00
Nathan Rajlich b4113da954 Enable custom class serialization transformations for "client" mode (#860) 2026-01-26 23:34:32 -08:00
Nathan Rajlich 1843704b83 Add support for custom class instance serialization (#762)
Added support for custom class instance serialization across workflow/step boundaries.

### What changed?

- Introduced a new `@workflow/serde` package with `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` symbols
- Enhanced the serialization system to handle custom class instances using these symbols
- Updated the SWC plugin to detect classes with serialization methods and register them
- Added class registry mechanism that works in both step and workflow contexts
- Implemented comprehensive tests for various serialization scenarios

### How to test?

The PR includes a new e2e test `customSerializationWorkflow` that demonstrates the feature:

```typescript
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';

// Define a class with custom serialization
class Point {
  constructor(public x: number, public y: number) {}

  static [WORKFLOW_SERIALIZE](instance: Point) {
    return { x: instance.x, y: instance.y };
  }

  static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) {
    return new Point(data.x, data.y);
  }
}

// Use in workflow and steps
export async function customSerializationWorkflow(x: number, y: number) {
  'use workflow';
  const point = new Point(x, y);
  const scaled = await transformPoint(point, 2);
  // ...
}
```

Run the e2e test to verify that class instances are properly serialized and deserialized.

### Why make this change?

Previously, user-defined class instances couldn't be passed between workflows and steps without losing their prototype chain and methods. This change allows developers to define custom serialization/deserialization logic for their classes, enabling proper reconstruction of instances with their full functionality intact when crossing workflow/step boundaries.
2026-01-19 15:38:19 -08:00
Nathan Rajlich 7906429541 Add support for serializing this when invoking step functions (#754)
Added support for serializing `this` context when invoking step functions.

### What changed?

- Modified the step function implementation to capture and serialize the `this` context when it's defined and not the global object
- Updated the step invocation queue item interface to include an optional `thisVal` property
- Enhanced the step handler to apply the hydrated `thisVal` when executing step functions

### Why make this change?

This enhancement allows step functions to be invoked with an explicit context object using standard JavaScript methods like `.call()` and `.apply()`. This is particularly useful for step functions that need to access properties from a specific context, enabling more flexible and idiomatic JavaScript patterns within workflows.
2026-01-14 00:07:29 -08:00
Nathan Rajlich a2fc53a0dc Support class static methods with "use step" / "use workflow" (#753)
The SWC compiler plugin had logic to walk through class static methods
with "use step" / "use workflow", but no actual transformation was being
applied. This fixes that.
2026-01-13 23:52:26 -08:00
JJ Kasper b29d8a80e0 Revert "fix: import stripping in client mode for step functions (#563)" (#577)
This reverts commit 8b8135fb37.
2025-12-08 14:23:05 -08:00
Ariel Weinberger 8b8135fb37 fix: import stripping in client mode for step functions (#563)
* fix client output

* inputs

* add fixtures

* test: wip

* fix: update step-with-imports fixture to match transform output

* DCO Remediation Commit for Ariel Weinberger <weinberger.ariel@gmail.com>

I, Ariel Weinberger <weinberger.ariel@gmail.com>, hereby add my Signed-off-by to this commit: 949bc0cc68
I, Ariel Weinberger <weinberger.ariel@gmail.com>, hereby add my Signed-off-by to this commit: 79e31ad4b2
I, Ariel Weinberger <weinberger.ariel@gmail.com>, hereby add my Signed-off-by to this commit: 5f6a825815
I, Ariel Weinberger <weinberger.ariel@gmail.com>, hereby add my Signed-off-by to this commit: fb20e60836
I, Ariel Weinberger <weinberger.ariel@gmail.com>, hereby add my Signed-off-by to this commit: ed8e944f0b

Signed-off-by: Ariel Weinberger <weinberger.ariel@gmail.com>

* add changeset

Signed-off-by: Ariel Weinberger <weinberger.ariel@gmail.com>

---------

Signed-off-by: Ariel Weinberger <weinberger.ariel@gmail.com>
2025-12-08 12:49:06 -08:00
Nathan Rajlich 43f2dec31b Improved workflow registration in workflow mode (#557)
Primary motivation here is to allow a `use workflow` function to be non-exported, meaning that it is called by `start()` in i.e. an API route within the same function:

- SWC plugin now emits `globalThis.__private_workflows.set(workflowId, fn)` directly after setting `workflowId`
- Non-exported workflow functions are now properly registered and can be invoked
- Removed runtime iteration over exports in the workflow bundle - registration happens at transform time
- Simplified virtual entry generation in base-builder
2025-12-05 15:19:24 -08:00
Nathan Rajlich af5b005ac8 Set workflowId property in workflow mode for non-exported workflow functions (#555)
### TL;DR

Modified the SWC plugin to set the `workflowId` property for all workflow functions in workflow mode, regardless of whether they are exported.

### What changed?

- Removed conditional logic that only added the `workflowId` property to exported workflow functions in workflow mode
- Now all workflow functions will have the `workflowId` property set, regardless of export status
- Updated test fixtures to reflect this change

### How to test?

1. Create a workflow file with non-exported workflow functions
2. Verify that the compiled output includes `workflowId` properties for all workflow functions
3. Run the existing test suite to ensure all tests pass with the updated behavior

### Why make this change?

This change ensures consistent behavior for all workflow functions. Previously, only exported workflow functions received the `workflowId` property in workflow mode, which could lead to inconsistent behavior when referencing non-exported workflow functions within the same file. This change makes the behavior more predictable and ensures all workflow functions are properly identified.
2025-12-05 14:31:54 -08:00
Nathan Rajlich fa37d26275 Set workflowId property directly after function declarations (#548)
Set `workflowId` property directly after function declarations in the SWC plugin.

### What changed?

Modified the SWC plugin to add the `workflowId` property immediately after workflow function declarations instead of collecting and adding them at the end of the file. This change applies to all transform modes (Client, Step, and Workflow).

The key changes include:
- Removed mode-specific conditions for adding `workflowId` assignments
- Simplified the code by adding `workflowId` inline for all workflow functions
- Cleared the `workflow_exports_to_expand` collection since it's no longer needed

### How to test?

1. Run the test suite to verify that all tests pass with the updated output format
2. Check the test fixtures to confirm that `workflowId` properties now appear directly after their respective function declarations
3. Verify that the behavior is consistent across all transform modes

### Why make this change?

This change improves code organization by placing the `workflowId` property closer to its associated function declaration, making the generated code more readable and maintainable. It also simplifies the plugin's logic by using a consistent approach across all transform modes rather than handling different modes separately.
2025-12-05 14:30:04 -08:00
Nathan Rajlich f46c51e30f Apply workflow transformation with export { fnName } syntax (#547)
Added support for workflow transformation with `export { fnName }` syntax.

### What changed?

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

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

### How to test?

Test with files that use the named export syntax:

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

export { workflowFunction };
```

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

### Why make this change?

This change ensures consistent behavior across different export syntaxes. Previously, workflow functions exported with the `export { fnName }` syntax weren't properly transformed, which could lead to runtime errors or unexpected behavior. This enhancement provides developers with more flexibility in how they structure and export their workflow functions.
2025-12-05 14:20:40 -08:00
JJ Kasper ac7997b855 Update to latest swc/core and preserve JSX (#507) 2025-12-03 11:29:27 -08:00
Nathan Rajlich 555d7a69de Normalize anonymous default export workflow IDs to "default" (#484) 2025-12-02 11:26:29 -08:00
Nathan Rajlich 5b918611fc Apply workflow function transformation in "step" mode (#420) 2025-11-26 23:35:53 -08:00
Nathan Rajlich 0cacb99fcc Support nested "use step" declarations in non-workflow functions (#418) 2025-11-26 23:20:04 -08:00
Nathan Rajlich 07800c29ee Support closure variables for serialized step functions (#366) 2025-11-25 00:29:15 -08:00