Skip to content

feat(builders): add after-bundle hook - #3751

Open
lucamaraschi wants to merge 1 commit into
vercel:mainfrom
lucamaraschi:feature/on-after-bundle
Open

lucamaraschi wants to merge 1 commit into
vercel:mainfrom
lucamaraschi:feature/on-after-bundle

Conversation

@lucamaraschi

@lucamaraschi lucamaraschi commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Adds an awaited onAfterBundle hook to @workflow/builders.

The hook runs after a successful workflow bundle and manifest build and provides:

  • The build target and working directory
  • The generated workflow manifest
  • Absolute paths for the steps bundle, workflows bundle, and manifest
  • Consistent invocation after successful watch rebuilds, including when the manifest contents are unchanged

Hook errors propagate to the caller. Failed or incomplete bundle and manifest builds do not invoke the hook.

This enables integrations to consume completed workflow build artifacts without reproducing the builder lifecycle internally.

Nitro-specific integration is intentionally excluded and will be handled separately.

How did you test your changes?

  • Added coverage using the real combined-bundle and manifest lifecycle
  • Verified invocation after successful watch rebuilds
  • Verified relative manifest directories report the actual written path
  • Verified the hook is awaited and errors propagate
  • Verified failed bundle and manifest builds do not invoke the hook
  • Built and typechecked @workflow/builders
  • Ran the complete builders test suite: 243 tests passed

PR Checklist - Required to merge

  • 📦 Added a minor changeset for @workflow/builders
  • 🔒 Commit includes the required DCO Signed-off-by trailer
  • 📝 Ping @vercel/workflow once the PR is ready for review

@lucamaraschi
lucamaraschi requested a review from a team as a code owner August 22, 2026 22:47
@changeset-bot

changeset-bot Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 761d0c8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@workflow/next Patch
@workflow/nitro Patch
@workflow/nuxt Patch
@workflow/astro Patch
@workflow/sveltekit Patch
@workflow/nest Patch
@workflow/builders Minor
workflow Patch
@workflow/cli Patch
@workflow/rollup Patch
@workflow/vite Patch
@workflow/vitest Patch
@workflow/world-testing Patch
@workflow/core Patch
@workflow/web-shared Patch
@workflow/web Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

@lucamaraschi is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@lucamaraschi
lucamaraschi force-pushed the feature/on-after-bundle branch from 25a8465 to 5ff146a Compare September 3, 2026 00:40
@lucamaraschi
lucamaraschi force-pushed the feature/on-after-bundle branch from 5ff146a to 15f0641 Compare September 23, 2026 20:21
@lucamaraschi

Copy link
Copy Markdown
Contributor Author

@vercel/workflow - ready for review

@pranaygp pranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a clean, small addition and the tests go through the real createCombinedBundle → createManifest path, which I like. A few correctness problems in how "completed bundle" is tracked, plus some API and docs questions.

Correctness

1. A failed rebuild after an earlier success still fires the hook with stale artifacts. base-builder.ts:1844, :2435
The entry for a workflows path is only ever set, never cleared. The sequence "successful bundle → failed createCombinedBundle for the same flowOutfile → createManifest" still calls onAfterBundle. I reproduced it with the PR's own TestBuilder: the sequence gives 2 hook calls; with correct gating it would be 1. The "does not register artifacts when createCombinedBundle fails" test only covers failure on the first build. This contradicts "Failed or incomplete bundle and manifest builds do not invoke the hook."

It's also a problem when a failed rebuild has already overwritten the steps file before the workflows bundle threw: the hook is then told the pair is consistent when it isn't. Today's in-repo callers happen not to call createManifest after a failed bundle, but nothing enforces that, so it's a latent bug for any BaseBuilder subclass.

Suggested fix: this.completedBundleArtifacts.delete(resolve(workingDir, flowOutfile)) at the top of createCombinedBundle, before startWorkflowBuildTimer() at line 1709. Consider also consuming the entry in createManifest rather than leaving it in the map. Please add a test for "success → failed rebuild → manifest ⇒ no second call."

2. The reported workflows path can differ from the file that was actually written. base-builder.ts:415-431 vs :2364
recordCompletedBundleArtifacts resolves stepsOutfile and flowOutfile against config.workingDir. When bundleFinalOutput: false, though, the workflows file is written through writeGeneratedFile(flowOutfile) (plain fs, so relative to process.cwd()). The manifest path is resolved against cwd too (resolve(manifestDir, …)).

Reproduced: with a relative f.js/m and cwd !== workingDir, the hook reported <workingDir>/f.js (existsSync false) and <cwd>/m/manifest.json (true). That breaks the documented promise that paths are absolute and point at the artifacts.

The in-repo callers all pass absolute paths today, so this is latent. But the hook is new public API and the README promises absolute, real paths. Please either resolve all three paths the same way the writers do, or require/assert absolute paths (isAbsolute) and say so in the docs. Add a test with workingDir !== cwd and relative steps/workflows paths.

3. workflowManifest is not the manifest that was written. base-builder.ts:2442, types.ts:34
The hook receives the input manifest. What gets written to manifest.json is output, which adds version and converted steps, and adds a graph on every workflow. Repro output:

  • Hook: {"steps":…,"workflows":{…{"run":{"workflowId":…}}},"classes":{}}
  • File: {"version":"1.0.0",…"run":{"workflowId":…,"graph":{"nodes":[],"edges":[]}}…}

Consumers wanting "the generated workflow manifest", as the PR description says, will expect the file's contents. Pick one and document it: pass the serialized output (or its typed form) in addition to or instead of the input, or name the field something like sourceManifest. As it stands, the internal SWC WorkflowManifest shape becomes part of the public hook contract, and its fields are mutable (only the top-level properties are readonly).

Risks / design

  1. "Completed bundle" fires before the build is complete. In VercelBuildOutputAPIBuilder it runs before the static manifest copy and createClientLibrary(). In Next it runs before the public-manifest copy and writeFunctionsConfig. Anyone who, for example, uploads or publishes from the hook can observe a partially built output directory. Either document that the hook runs mid-build, or move the call to the end of build() and the rebuild paths.

  2. A failed hook leaves the new manifest on disk. By the time the hook throws, manifest.json (and the diagnostics copy) are already written and the timer is reset (:2387, :2421), so the caller sees a rejected build whose outputs are updated. Fine if intended; please document it. In the Next watch path the error is swallowed by enqueue's console.error('Failed to process file change', …), so "rejects the rebuild" really means "logged".

  3. The hook fires for only some builders, and no user can reach it yet.

    • SimBuilder (world-sim/src/build.ts) calls createCombinedBundle but never createManifest, so it never fires.
    • None of the framework integrations (Next, SvelteKit, Astro, Nest, Nitro, CLI) pass onAfterBundle through from user config. Today only direct BaseBuilder users can set it.

    The README says "Builder configurations can also provide an onAfterBundle observer" with no caveat. Please list which build targets invoke it, or note that it's builder-API only for now.

  4. Observability. There's no log or timing around the hook. A slow hook blocks every watch rebuild with no signal, and a hook error comes through raw, indistinguishable from a builder failure. Suggestions:

    • wrap errors as new Error('onAfterBundle hook failed', { cause });
    • log the hook duration through the existing logCreateManifestInfo/debug channel, so slow-HMR reports are diagnosable;
    • if the builders package has tracing spans around build phases, add one for the hook.
  5. Performance. The hook is awaited serially on the hot-rebuild path (hotRebuild → writeManifest). Worth a sentence in the docs that it should stay fast or hand heavy work off asynchronously.

  6. Security. The hook receives absolute filesystem paths and runs with builder privileges; expected for build-time config. No new trust boundary, so no concerns beyond documenting that it runs in the build process.

Missing tests

  • Success → failed rebuild → manifest ⇒ not called (1).
  • workingDir !== cwd with relative bundle paths ⇒ every reported path exists (2).
  • A test pinning what workflowManifest contains relative to manifest.json (3).
  • Hook ordering relative to the webhook and public-manifest outputs, for at least one real builder (StandaloneBuilder or VercelBuildOutputAPIBuilder), not just the TestBuilder double.
  • The watch test calls createManifest directly. One test driving an actual rebuild loop, or the Next writeManifest path, would back up the "consistent invocation after watch rebuilds" claim.
  • A non-Error throw (e.g. a string) propagates unchanged; this works today, it's just not pinned by a test.

Docs

  • Only the package README is updated. If onAfterTransform is documented under docs/ anywhere, onAfterBundle belongs next to it; otherwise note that it's builder-API only.
  • The README should state: which targets invoke the hook; that it runs before the rest of the builder's outputs exist; which manifest shape it receives; and that manifest.json stays updated when the hook throws.

Nits

  • base-builder.ts:225: the tuple type for the map could reuse WorkflowBundleArtifacts (e.g. Omit/slice) instead of restating it.
  • The changeset says "observer", while the PR title says "hook". Either works; just be consistent.
  • WorkflowBundleResult.workingDir is the raw config value. Resolve it, or document that it may be relative.
  • Consider freezing the artifacts array passed to the hook (Object.freeze), since the type promises readonly.

Recommendation: request changes. Findings 1 (stale gating) and 2 (path resolution) break the documented contract, and 3 needs a decision before this becomes public API. The rest are small.

@TooTallNate

Copy link
Copy Markdown
Member

This enables integrations to consume completed workflow build artifacts without reproducing the builder lifecycle internally.

I'm not sure what this means exactly. Could you describe your use case?

@lucamaraschi
lucamaraschi force-pushed the feature/on-after-bundle branch from 15f0641 to 61e4899 Compare September 24, 2026 22:17
@lucamaraschi

Copy link
Copy Markdown
Contributor Author

Platformatic is adding support for remote workflow handlers. After Workflow SDK emits the bundles and manifest, we need to map the public handler names and schemas to the built workflow IDs.
This must run after every successful build or rebuild and should fail the build if a handler cannot be resolved. Without this hook, we would need to wrap individual builders and reproduce their build/watch lifecycle to determine when the artifacts are consistent.

@lucamaraschi
lucamaraschi force-pushed the feature/on-after-bundle branch from 61e4899 to cc4c4a9 Compare September 27, 2026 17:21
@lucamaraschi

Copy link
Copy Markdown
Contributor Author

Follow-up: expanded onAfterBundle forwarding across all production framework integrations:

  • @workflow/next
  • @workflow/nitro
  • @workflow/nuxt
  • @workflow/astro
  • @workflow/sveltekit
  • @workflow/nest

The existing generic builder lifecycle is unchanged. These updates expose the optional hook through each framework’s public configuration surface and forward it to the underlying builder. Nitro also now reports an accurate nitro build target instead of the previous placeholder.

Added adapter-level tests, documentation, and a changeset. When the hook is not configured, behavior and performance remain unchanged.

Local validation passed for all affected package tests and builds.

Signed-off-by: Luca Maraschi <luca.maraschi@gmail.com>
@lucamaraschi
lucamaraschi force-pushed the feature/on-after-bundle branch from cc4c4a9 to 761d0c8 Compare September 27, 2026 18:13
@lucamaraschi

Copy link
Copy Markdown
Contributor Author

For context, we’ve prepared the follow-on Platformatic work that will consume this lifecycle capability:

  • Platformatic World #223 — adds the @platformatic/remote-workflow runtime and artifact generation.
  • Platformatic #5145 — adds the Platformatic workflowsdk capability that hosts remote workflow handlers.

These are separate, public PRs and are provided as references for the planned integration. The direct APIs remain usable independently; the automatic build-to-runtime path consumes the onAfterBundle hook from this PR.

This branch was successfully deployed

18 active (outdated) deployments
Preview – workflow-swc-playground — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workflow-docs — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-nuxt-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – example-nextjs-workflow-webpack — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-astro-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-sveltekit-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-nitro-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – example-nextjs-workflow-turbopack — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-vite-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-tanstack-start-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-fastify-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workflow-tarballs — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-express-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-nestjs-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – example-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-hono-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workflow-web — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Preview – workbench-python-workflow — 15f0641f Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants