Skip to content

fix(build): bundle @vinext/cloudflare into vinext to break dependency cycle - #1797

Merged
james-elicx merged 3 commits into
mainfrom
fix/bundle-cloudflare-no-cycle
Jun 6, 2026
Merged

fix(build): bundle @vinext/cloudflare into vinext to break dependency cycle#1797
james-elicx merged 3 commits into
mainfrom
fix/bundle-cloudflare-no-cycle

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Problem

vinext and @vinext/cloudflare form a circular dependency:

vinext ──(dependencies: workspace:*)──▶ @vinext/cloudflare
   ▲                                            │
   └──────────(peerDependencies)────────────────┘

vinext only needs a handful of runtime helpers from @vinext/cloudflare:

  • cloudflare/tpr.tsENTRY_PREFIX
  • cloudflare/index.ts → re-exports KVCacheHandler
  • shims/cdn-cache.tsCloudflareCdnCacheAdapter (auto-detect edge default)

Combined with @vinext/cloudflare's peerDependencies on vinext, this cycle forced changesets to force-major @vinext/cloudflare on essentially every vinext release (mitigated defensively in #1764, but the cycle itself remained).

Fix

Bundle the small amount of @vinext/cloudflare code vinext actually imports into vinext's dist (via tsdown's deps.neverBundle), and move @vinext/cloudflare to devDependencies. The published install graph now points one way: @vinext/cloudflare → vinext.

@vinext/cloudflare remains a published package — nothing user-facing changes:

  • User vite.config files still import cdnAdapter() / kvDataAdapter() from @vinext/cloudflare/cache/*.
  • The generated worker still resolves its *.runtime.js factories by absolute path from @vinext/cloudflare.
  • The bundled-in code's vinext/shims/* imports stay external and resolve through vinext's own package exports at runtime (Node self-referencing) — identical to how @vinext/cloudflare resolves them today.

Also drops the now-obsolete create-next-app CI steps that packed and overrode @vinext/cloudflare, since vinext's tarball no longer declares it as a dependency.

Verification

  • vp run vinext#builddist has no @vinext/cloudflare runtime imports; internal imports now point at bundled local files (e.g. ./src/cache/kv-data-adapter.runtime.js). Generated .d.ts likewise reference local paths (remaining @vinext/cloudflare strings are JSDoc only).
  • vp run @vinext/cloudflare#build → still builds independently.
  • Tests: deploy, cache-adapters-config, kv-cache-handler, isr-cache, build-optimization all green (463 tests).
  • vp check on the config + pnpm knip clean.

Notes

  • Non-breaking: the supported pattern already requires users who configure caching to declare @vinext/cloudflare directly (see examples/workers-cache). No scaffolding relied on transitive resolution.
  • The pnpm dev-graph cyclic-workspace warning is pre-existing (both packages build from each other's source at dev time) and is unrelated to the published/release cycle this PR removes; changesets ignores devDependencies.

… cycle

vinext consumed a few runtime helpers (KVCacheHandler, CloudflareCdnCacheAdapter,
ENTRY_PREFIX) from @vinext/cloudflare via a runtime `dependencies` edge. Combined
with @vinext/cloudflare's `peerDependencies` on vinext, this formed a cycle that
forced changesets to force-major @vinext/cloudflare on every vinext release
(mitigated defensively in #1764).

Bundle the small amount of @vinext/cloudflare code vinext actually imports into
vinext's dist via tsdown's deps.neverBundle, and move @vinext/cloudflare to
devDependencies. The published install graph now points one way
(@vinext/cloudflare -> vinext).

@vinext/cloudflare remains a published package: user vite.config files still
import cdnAdapter()/kvDataAdapter(), and the generated worker resolves its
*.runtime.js factories by absolute path. Its bundled-in vinext/shims/* imports
stay external and resolve via vinext's own package exports (Node self-reference).

Drop the now-obsolete create-next-app CI steps that packed and overrode
@vinext/cloudflare (vinext's tarball no longer declares it as a dependency).
@pkg-pr-new

pkg-pr-new Bot commented Jun 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1797
npm i https://pkg.pr.new/vinext@1797

commit: 86877f1

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx
james-elicx marked this pull request as draft June 6, 2026 22:09
Use a single intent-revealing neverBundle predicate with a BUNDLED_DEPS
carve-out and document why the carve-out must live in neverBundle (tsdown
rejects skipNodeModulesBundle + alwaysBundle as mutually exclusive, and a
catch-all neverBundle takes precedence over alwaysBundle).
…predicate

The previous approach replaced deps.skipNodeModulesBundle with a custom
neverBundle predicate. That broke tsdown's rewriting of vinext's own
`vinext/shims/*` tsconfig-path self-imports to relative paths, leaving them as
bare `vinext/*` specifiers in 46 files. Across Vite's separate RSC/SSR/client
dev module graphs those bare self-references resolved to distinct module
instances, breaking identity checks (e.g. `instanceof ReadonlyURLSearchParams`).

Keep skipNodeModulesBundle: true untouched and instead bundle @vinext/cloudflare
by aliasing its cache/* subpath to source. skipNodeModulesBundle externalizes
bare package specifiers before tsconfig paths apply, so the alias rewrites the
import to a file path up front; tsdown then treats it as local source and
bundles it. The bundled code's own vinext/shims/* imports still resolve to
vinext's relative output (single module instance).

Output is byte-identical to the pre-change build except the added bundled
cloudflare files and one cosmetic import reorder.
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx marked this pull request as ready for review June 6, 2026 22:54

@ask-bonk ask-bonk Bot 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.

Review: fix(build): bundle @vinext/cloudflare to break dependency cycle

I built vinext with these changes, inspected the dist output, ran the affected test files (399 passing), and traced the resulting dependency/module graph. This is correct and ready to merge. Details below.

Verified ✅

Cycle broken. @vinext/cloudflare is moved dependenciesdevDependencies (and dropped from the lockfile importer's deps). The published install graph now points one way: @vinext/cloudflare → vinext.

Bundling actually happened. After vp run vinext#build, the consuming files import the bundled local copies — no external @vinext/cloudflare runtime imports remain:

  • dist/cloudflare/tpr.jsimport { ENTRY_PREFIX } from "./src/cache/kv-data-adapter.runtime.js"
  • dist/cloudflare/index.jsimport { KVCacheHandler } from "./src/cache/kv-data-adapter.runtime.js"
  • dist/shims/cdn-cache.jsimport { CloudflareCdnCacheAdapter } from "../cloudflare/src/cache/cdn-adapter.runtime.js"

The only remaining @vinext/cloudflare strings in dist are JSDoc comments and the user-facing "configure a cache adapter" error message in deploy.js — both intentional.

Transitive deps came along. cdn-adapter.runtime / kv-data-adapter.runtime import ../utils/cache-control-metadata, and that got bundled too (dist/cloudflare/src/utils/cache-control-metadata.js), as expected once the alias turns the subpath into local source.

No duplicate shim modules (the hazard called out in the config comment). The bundled cloudflare runtime's vinext/shims/request-context import was rewritten to a relative path resolving to the single shared dist/shims/request-context.js (confirmed exactly one request-context.js in dist). So the instanceof / single-module-instance concern is satisfied.

CI cleanup is sound. The removed "Pack @vinext/cloudflare" + "Override @vinext/cloudflare to the local tarball" steps existed only because vinext's tarball declared a dependency on the unpublished @vinext/cloudflare. With that dependency gone, the scaffolded create-next-app no longer needs the cloudflare tarball to install. Nothing else in the create-next-app job references it.

Published consumers unaffected. @vinext/cloudflare keeps its ./cache/* export and peerDependencies: { vinext }; user vite.config imports of cdnAdapter()/kvDataAdapter() and the generated worker's absolute-path *.runtime.js resolution are unchanged. The supported pattern already requires users who configure caching to declare @vinext/cloudflare directly (examples/workers-cache).

Tests + checks. tests/deploy, tests/cache-adapters-config, tests/kv-cache-handler, tests/build-optimization → 399 passed. vp check packages/vinext/vite.config.ts clean (format + lint + types).

Minor notes (non-blocking)

  1. Alias is a prefix match. "@vinext/cloudflare/cache" → <dir> rewrites @vinext/cloudflare/cache/foo<dir>/foo by substring prefix. It's safe today because @vinext/cloudflare only exposes ./cache/*, but a hypothetical future sibling subpath like @vinext/cloudflare/cache-control would be misrewritten by the same prefix. Not a problem now; just worth a mental note if more subpaths are added.

  2. Tight coupling to @vinext/cloudflare's internal source layout. Bundling from ../cloudflare/src/cache means vinext's build now depends on cloudflare's internal file structure (*.runtime.ts, utils/cache-control-metadata.ts), not just its public ./cache/* export. If cloudflare reorganizes those files, vinext's build breaks. The affected tests (deploy, cache-adapters-config) should catch that, so this is acceptable — but a one-line comment in cloudflare/src/cache noting "these paths are bundled into vinext; see vinext/vite.config.ts" would help future maintainers.

Nice, well-documented change — the config comments explaining why skipNodeModulesBundle must stay and how the alias interacts with it are exactly the kind of context that prevents future regressions.

@ask-bonk

ask-bonk Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1797.

Summary

I reviewed PR #1797 by building vinext with the changes, inspecting the dist output, running the affected tests (399 passing), and tracing the dependency/module graph. The PR is correct and ready to merge.

Verified:

  • The circular dependency is broken — @vinext/cloudflare moved to devDependencies, install graph points one way.
  • Bundling actually works — dist imports resolve to local bundled files; no external @vinext/cloudflare runtime imports remain (only JSDoc/error strings).
  • Transitive deps (utils/cache-control-metadata) got bundled too.
  • No duplicate shim modules — the bundled code's vinext/shims/* imports rewrote to the single shared dist/shims/request-context.js instance (the key correctness concern).
  • CI cleanup is sound — removed steps were only needed for the old unpublished-dependency situation.
  • Published consumers unaffected; type/lint clean.

Minor non-blocking notes I raised:

  1. The alias does prefix matching — safe today (only one subpath export) but worth noting if more subpaths are added.
  2. vinext's build now couples to @vinext/cloudflare's internal source layout; a breadcrumb comment in the cloudflare source would help future maintainers (tests would catch breakage regardless).

No correctness issues found.

github run

@james-elicx
james-elicx merged commit f9d83fd into main Jun 6, 2026
80 of 82 checks passed
@james-elicx
james-elicx deleted the fix/bundle-cloudflare-no-cycle branch June 6, 2026 22:58
@github-actions github-actions Bot mentioned this pull request Jun 6, 2026
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.

1 participant