Commit Graph

3 Commits

Author SHA1 Message Date
James Anderson 0c7ff57710 fix(build): exclude Vite build manifests from Cloudflare asset uploads (#1850)
On Cloudflare Workers the ASSETS binding serves any uploaded file whose
path matches the request BEFORE the Worker runs. Cloudflare builds enable
`build.manifest` (and `ssrManifest` for Pages Router) so the worker entry
can compute lazy chunks, which writes Vite's manifests to
`dist/client/.vite/`. Because nothing excluded that directory from the
asset bundle, `/.vite/manifest.json` and `/.vite/ssr-manifest.json` were
publicly fetchable, leaking the full source-file -> chunk mapping
(including the paths of routes that are never linked from the UI).

The Node production server already blocks `/.vite/` explicitly
(server/static-file-cache.ts); this adds the Cloudflare-side equivalent by
writing a `.assetsignore` into `dist/client` during the cloudflare-build
closeBundle hook (right after the existing `_headers` generation, after the
manifests have been read). wrangler matches `.assetsignore` with gitignore
semantics, so the bare `.vite` entry excludes the directory and its
contents from upload, and the file is never served.

The merge logic preserves any user-authored `.assetsignore` and only
appends missing patterns, so it is idempotent across rebuilds. The writer
lives in a small typed helper (build/assets-ignore.ts) with focused unit
tests; a real Cloudflare build test asserts the wiring emits `.assetsignore`
with `.vite` while the manifest still exists on disk.
2026-06-08 19:47:57 +01:00
James Anderson 0b7721dba6 perf(build): minify server build environments by default (#1777)
* fix(build): minify server build environments by default

Vite only minifies the client environment; non-client (rsc/ssr and the
Cloudflare worker) environments default build.minify to false, so the
deployed worker and SSR renderer ship unminified. Add a build-only
configEnvironment default that sets minify: true for server environments,
yielding to any explicit user/plugin minify setting (including false).

* fix(build): patch route classification before minify; build fixtures unminified for output introspection

Minifying server environments by default exposed two problems:

1. Real bug: build-time route classification ran in `generateBundle`, but
   rolldown's minifier renames the top-level `__VINEXT_CLASS` function and
   mangles its `routeIdx` parameter before that hook runs, so the stub regex
   never matched and the patch silently no-op'd — every route fell back to the
   Layer 3 runtime probe in production. Move the injection to a `renderChunk`
   hook with `order: "pre"` so it runs before minification; the patched body is
   then minified along with the rest of the chunk (the runtime calls it by
   reference, not by name).

2. Test artifacts: three build tests introspect emitted server chunks for
   readable identifiers (registerConfiguredCacheAdapters, __VINEXT_CLASS) or
   depend on unminified chunking (MDX frontmatter leak check). These are
   test-only concerns — the runtime behavior is unchanged. Build their fixtures
   with `build.minify: false` (a user-overridable default) so the assertions
   stay deterministic.

* fix(build): drop now-unused RouteClassificationChunk export (knip)

The renderChunk refactor removed the external consumer of this type; keep it
as a local type so knip passes.

* test(build): guard minified classification path; fix stale generateBundle refs

Re-add real regression coverage that the route-classification injector runs
before minification (the production default). The prior fix made three tests
pass by building with minify:false, which is exactly the config where the old
post-minify generateBundle code also passed — so the tests no longer guarded
the bug.

- classification integration: add a production-default (minify on) suite that
  asserts minify-robust signals (injected switch dispatch shape + the
  module-graph reason literal, which is build-time-only). Verified it FAILS when
  the injector is reverted to a post-minify generateBundle hook.
- cache adapters: assert the minify-safe LOCAL_ADAPTER_MARKER under the default
  minified build instead of grepping the renamed registerConfiguredCacheAdapters
  symbol.
- tsconfig path alias (#659): run minified; the old not.toContain('title:
  "Second Post"') was a quote-escaping artifact (frontmatter renders as MDX
  content identically in both builds, not a minify leak) — replaced with a real
  parse-error/raw-YAML guard.
- update stale generateBundle references to renderChunk in injector error
  strings, app-rsc-entry stubs, and index.ts comments; note the per-chunk patch
  assumption near the renderChunk hook.

* test(build): run classification tests against minified output (no minify:false)

Previously the dispatch-logic suites built the fixture with minify:false so they
could regex-extract and eval the readable __VINEXT_CLASS function — but that is
the one config where the original post-minify-hook bug also passes, so it masked
the regression. Rewrite the extractors to be name-agnostic: derive the (mangled)
dispatch function name from its property-keyed call site (__buildTimeClassifications/
__buildTimeReasons — property keys survive minification) and eval the body
(string-literal values like "static" also survive). All suites now build with
vinext's production defaults (minify on). Verified by reproducing the bug
(disabling the patch): the minified tests fail with "was not patched".

* refactor(build): rename buildGenerateBundleReplacement; fix stale generateBundle doc refs

Address review nits: the classification dispatch builder still carried the old
hook name (it now runs in renderChunk, not generateBundle). Rename it to
buildClassificationReplacement (parallel to buildReasonsReplacement) and fix two
stale 'generateBundle' doc comments (route-classification-manifest.ts:203,
layout-classification.ts:130) to say renderChunk.
2026-06-06 22:55:04 +01:00
James Anderson f0f6aa72e0 feat(cache): configure cache adapters from vite plugin config (#1733)
* feat(cache): configure cache adapters from vite plugin config

Add a `cache` option to the vinext() plugin so CDN and data cache
adapters can be declared in vite.config instead of calling
setDataCacheHandler() / setCdnCacheAdapter() from a worker entry:

  vinext({
    cache: {
      cdn:  { adapter: require.resolve('vinext/cloudflare/cache/cdn-adapter') },
      data: { adapter: require.resolve('vinext/cloudflare/cache/kv-data-adapter') },
    },
  })

Each slot points at an adapter module whose default export is a factory
(DataCacheAdapterFactory / CdnCacheAdapterFactory). The plugin generates
a virtual:vinext-cache-adapters module that the App Router worker entry
calls per request (self-guarded, once per isolate), passing the host env
so binding-backed adapters (e.g. KV) can read their namespace.

Ships ready-made Cloudflare adapter entry points:
  - vinext/cloudflare/cache/kv-data-adapter  (KVCacheHandler)
  - vinext/cloudflare/cache/cdn-adapter      (CloudflareCdnCacheAdapter)

* feat(cache): add typed adapter builders (kvDataAdapter/cdnAdapter)

Instead of `{ adapter: require.resolve(...) }`, each adapter module now
also exports a config-time builder from the same path:

  import { cdnAdapter } from 'vinext/cloudflare/cache/cdn-adapter';
  import { kvDataAdapter } from 'vinext/cloudflare/cache/kv-data-adapter';

  vinext({ cache: { cdn: cdnAdapter(), data: kvDataAdapter({ binding: 'MY_KV' }) } })

A builder returns a plain, serializable { adapter, options } descriptor —
it never touches the Workers runtime, so nothing throws at config / build
/ dev time when bindings aren't available. Descriptor `options` (e.g. the
KV binding name) are inlined into the generated registration module and
forwarded to the factory's { env, options } context, where the binding is
resolved lazily on the first request.

- shims/cache-adapter: descriptors + options-aware factory/context types
- kv-data-adapter: kvDataAdapter() builder + configurable binding/appPrefix/ttl
- cdn-adapter: cdnAdapter() builder
- raw { adapter, options } path form still supported

* test(cache): verify absolute (require.resolve) local adapter path bundles

Real Cloudflare build pointing cache.data at a local adapter file by
absolute path (what require.resolve('./adapter') yields). Proves the
generated registration module resolves the absolute import, bundles the
local adapter into the worker, and does not need any Workers context at
build time.

* refactor(cache): builder require.resolve + register across all routers/runtimes

Addresses review feedback:

* Move adapters into their own runtime modules instead of re-exporting.
  Each adapter is now a builder module (kv-data-adapter.ts / cdn-adapter.ts)
  plus a sibling *.runtime.ts holding the default-export factory. Type
  definitions have a single home in shims/cache-adapter.ts (dropped the
  re-export shim; index.ts imports the config type from there).

* The exposed builder utility resolves the relative runtime path internally
  via import.meta.resolve (the ESM require.resolve), so the descriptor carries
  an absolute path to the runtime factory rather than a bare specifier — the
  example is just kvDataAdapter({ binding }), no require.resolve at the call site.

* Register configured cache handlers EVERYWHERE, not just the App Router worker:
  - App Router: the generated RSC entry passes registerConfiguredCacheAdapters
    into createAppRscHandler, which calls it per request — covering Workers,
    the Node server, and dev through the one shared handler.
  - Pages Router: the generated server entry registers in renderPage and
    handleApiRoute (Node/dev), and the generated worker registers with env
    (Workers, for KV bindings).
  Registration self-guards (first call with real env wins) and is now resilient:
  a factory that throws on an incompatible runtime is logged and skipped, so the
  default handler stays in place instead of failing every request.

Tests: generator-level assertions that every router/runtime entry wires
registration, plus the existing builder/codegen/factory and full-build coverage.
vp check clean; app-router (339) and pages-router (272) suites pass.

* refactor(cache): keep all Cloudflare adapter code under cloudflare/

The adapter factory contract lived in shims/cache-adapter.ts (outside
cloudflare/), and the Cloudflare adapters reached out to it. Move the
contract into cloudflare/cache/adapter.ts so every Cloudflare-specific
cache adapter file is self-contained under cloudflare/ — importing only
cloudflare-local modules and the core CacheHandler/CdnCacheAdapter
interfaces it implements.

The plugin's config schema (CacheAdapterDescriptor / VinextCacheConfig)
is genuinely framework-level (it's the vinext() `cache` option), so it
moves into the codegen module the plugin already owns; index.ts imports
it from there. Builders return a structural { adapter, options } so they
don't import the descriptor type either. Deletes shims/cache-adapter.ts.

* refactor(cache): merge KV/CDN classes into the runtime adapter files

All Cloudflare cache code now lives in one directory, cloudflare/cache/,
and each runtime file holds both the implementation class and its
config-driven factory (no separate class module to reach for):

  - kv-cache-handler.ts        -> cache/kv-data-adapter.runtime.ts
    (KVCacheHandler + ENTRY_PREFIX + createKvDataCacheAdapter default export)
  - cloudflare-cdn-cache.ts    -> cache/cdn-adapter.runtime.ts
    (CloudflareCdnCacheAdapter + createCloudflareCdnCacheAdapter default export)

Updated importers: cloudflare/index.ts re-exports the classes from the
runtime files, tpr.ts pulls ENTRY_PREFIX from there, shims/cdn-cache.ts
imports the edge adapter from there, and the tests follow the moved paths.
git mv preserves history.

vp check clean; cache/kv/cdn/app-route/tpr/shims suites pass (1300+ tests).

* chore(cache): trim low-value comments added in this branch

Remove narrating/redundant comments that just restated the code; keep
the non-obvious why (registration ordering/resilience, import.meta.resolve
rationale, edge cache-control semantics). No code changes.

* review: address PR #1733 feedback

- Make registerCacheAdapters a required field on the RSC handler options
  (the generated entry already passes it; test factory updated).
- Remove the separate cloudflare/cache/adapter.ts contract file; inline the
  factory param types directly into the two runtime adapters.
- Drop the CloudflareCdnCacheAdapter re-export from cloudflare/index.ts.
- Fold the virtual:vinext-cache-adapters declaration into global.d.ts and
  delete the standalone .d.ts.
- Remove the ./cloudflare/cache/* package.json export for now; README uses a
  local-adapter require.resolve example with a note that the built-in adapter
  export paths are pending.
- Rename the config-driven KV default binding to VINEXT_KV_CACHE (imperative
  deploy/tpr path keeps VINEXT_CACHE — flagged on the thread).

* refactor(cache): align KV binding name to VINEXT_KV_CACHE everywhere

Rename the KV cache binding from VINEXT_CACHE to VINEXT_KV_CACHE across the
whole codebase so the config-driven adapter, the imperative deploy-generated
worker, TPR's wrangler detection, and the apps/web example all agree. The
unrelated X-Vinext-Cache response-header constant (VINEXT_CACHE_HEADER) is
untouched.

* tidy

* .

* .

* .

* .

* .

* Move apps/web cache to plugin config

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
2026-06-04 23:30:33 +00:00