Files
Nathan Rajlich 88f5d214d4 fix(builders): shim __dirname/__filename in fully-bundled ESM output (#3876)
* fix(builders): shim __dirname/__filename in fully-bundled ESM output

esbuild leaves the CJS globals __dirname/__filename as free identifiers
when inlining CJS modules into ESM output, so dependencies that reference
them at module scope (google-gax via @google-cloud/pubsub, Prisma's
runtime) crash the deployed Vercel function at init with
'ReferenceError: __dirname is not defined in ES module scope' before any
workflow code runs. v4 was immune because the Build Output API function
was CJS; #1562 switched it to ESM with a banner that shimmed only
require().

Extend the ESM banner to define __filename/__dirname from
import.meta.url, matching the shim verified live in #2770.

* chore(builders): clarify interop banner scope and tighten its regression test

Review follow-ups to the __dirname/__filename shim; no behavior change.

The ESM banner now declares require, __filename and __dirname, but the
flag that suppresses it is still named skipEsmRequireBanner and its
JSDoc described it purely in terms of __createRequire. Document the full
surface instead of renaming, since BaseBuilder is exported and
createStepsBundle is protected, so a rename would break external
subclasses in a patch release. The note calls out #3778 specifically:
reaching for this flag to silence a duplicate require declaration also
drops the dirname shim.

Assert the banner's import binding is emitted exactly once. A duplicated
banner fails at parse time on the redeclared import, before the var ever
runs, so that is the assertion that matches the real failure mode. Also
note that createWorkflowsBundle's final wrapper and createWebhookBundle
emit the same banner and are not covered here.

Finally, record in the changeset that CJS dependencies which
feature-detect via `typeof __dirname !== 'undefined'` now take their CJS
branch, where __dirname is the function root rather than the
dependency's own directory.

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(builders): assert the webhook function bundle carries the ESM interop shim

The webhook route is a separately deployed function built through its own
esbuild pass (createWebhookBundle); a CJS dependency referencing __dirname
reachable from it would have crashed identically, so cover that emit site
rather than only noting it as untested.

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 08:45:33 +00:00
..
2026-08-26 12:36:41 -07:00
2026-08-26 12:36:41 -07:00

@workflow/builders

Shared builder infrastructure for Workflow SDK. This package provides the base builder class and utilities used by framework-specific integrations.

Overview

This package contains the core build logic for transforming workflow source files into deployable bundles. It is used by:

  • @workflow/cli - For standalone/basic builds
  • @workflow/next - For Next.js integration
  • @workflow/nitro - For Nitro/Nuxt integration

Key components

  • BaseBuilder: Abstract base class providing common build logic
  • Build plugins: esbuild plugins for workflow transformations
  • SWC integration: Compiler plugin integration for workflow directives

Usage

This package is typically not used directly. Instead, use one of the framework-specific packages that extend BaseBuilder:

import { BaseBuilder } from '@workflow/builders';

class MyBuilder extends BaseBuilder {
  async build(): Promise<void> {
    // Implement builder-specific logic
  }
}

Observing transforms

Builder configurations can provide an optional onAfterTransform observer for tooling that derives metadata from the exact SWC output used by a build:

import type { WorkflowAfterTransformHook } from '@workflow/builders';

// Pass as `onAfterTransform` in the builder configuration.
const onAfterTransform: WorkflowAfterTransformHook = async ({
  mode,
  filename,
  absolutePath,
  source,
  code,
  workflowManifest,
}) => {
  // Observe the accepted transform result.
};

The observer is awaited after the transform's manifest entries have been accepted. It cannot replace the generated code, and throwing aborts the build. A source file may be observed multiple times across transform modes, bundles, and watch rebuilds, so consumers should deduplicate results when necessary.

Architecture

The builder system uses:

  1. esbuild for bundling and tree-shaking
  2. SWC for transforming workflow directives ("use workflow", "use step")
  3. Enhanced resolve for TypeScript path mapping

License

MIT