[PR #474] feat: support assetPrefix in next.config #600

Open
opened 2026-05-06 13:09:00 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/474
Author: @elydelva
Created: 3/11/2026
Status: 🔄 Open

Base: mainHead: feat/asset-prefix


📝 Commits (10+)

  • 00c8711 feat: add assetPrefix support in next.config
  • 8b5071a feat: support assetPrefix in next.config
  • d99cc97 test: update entry-template snapshots for assetPrefix
  • 04e09e1 test: add font-local CDN preload and basePath+assetPrefix combination tests
  • 6949427 test: add E2E test verifying CDN-prefixed asset URLs in rendered HTML
  • 290a2aa style: fix oxfmt formatting (semicolons, line length)
  • ef6d83a fix: avoid double basePath prefix in collectAssetTags when assetPrefix inherits basePath
  • eca4295 chore: update pnpm lockfile for asset-prefix fixture
  • dd8364e style: run oxfmt on all modified files
  • 6150056 fix: harden assetPrefix edge cases and config composition

📊 Changes

23 files changed (+622 additions, -31 deletions)

View changed files

📝 packages/vinext/src/config/next-config.ts (+19 -4)
📝 packages/vinext/src/entries/app-rsc-entry.ts (+11 -7)
📝 packages/vinext/src/entries/pages-server-entry.ts (+17 -5)
📝 packages/vinext/src/index.ts (+24 -0)
📝 packages/vinext/src/server/prod-server.ts (+4 -0)
📝 packages/vinext/src/shims/font-local.ts (+10 -2)
📝 playwright.config.ts (+11 -0)
📝 pnpm-lock.yaml (+16 -0)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+17 -6)
tests/asset-prefix.test.ts (+102 -0)
tests/e2e/asset-prefix/asset-prefix.spec.ts (+128 -0)
tests/fixtures/asset-prefix/next-shims.d.ts (+24 -0)
tests/fixtures/asset-prefix/next.config.mjs (+6 -0)
tests/fixtures/asset-prefix/package.json (+13 -0)
tests/fixtures/asset-prefix/pages/_app.tsx (+6 -0)
tests/fixtures/asset-prefix/pages/about.tsx (+10 -0)
tests/fixtures/asset-prefix/pages/index.tsx (+11 -0)
tests/fixtures/asset-prefix/pages/styles.css (+4 -0)
tests/fixtures/asset-prefix/tsconfig.json (+11 -0)
tests/fixtures/asset-prefix/vite.config.ts (+6 -0)

...and 3 more files

📄 Description

Closes #472

Summary

assetPrefix from next.config is currently silently ignored in vinext. This PR implements full support conformant with the Next.js assetPrefix spec.

Motivation

When deploying to Cloudflare Workers behind a gateway Worker (routing pattern for auth, multi-tenant apps, etc.), every request — including static fonts, JS chunks, and CSS — goes through the gateway and incurs a Worker invocation. With assetPrefix, static assets can be served from a dedicated subdomain (e.g. assets-app.example.com) backed by a Cloudflare Workers Assets-only deployment with run_worker_first: false, resulting in zero Worker invocations for static assets.

Exploration

Before writing code, the Next.js reference implementation and the vinext codebase were fully analysed:

Next.js (vercel/next.js)assetPrefix flows through:

  • webpack output.publicPath → all JS/CSS chunks
  • next-font-loader outputPath → font files baked at build time
  • Manual SSR HTML injection (<script src>, <link href>) in app-render/
  • Client-side runtime detection via bootstrap script URL (client/asset-prefix.ts)
  • Does not cover public/ files or /_next/image (by design)
  • Key rule: when assetPrefix is empty and basePath is set → assetPrefix inherits basePath

vinextassetPrefix requires 8 changes across 5 files. Critical gap found: shims/font-local.ts line 329 gates <link rel="preload"> emission on href.startsWith("/"), which silently breaks when assetPrefix is an absolute CDN URL.

Changes

1. src/config/next-config.ts — config types + resolution

  • Add assetPrefix?: string to NextConfig interface
  • Add assetPrefix: string to ResolvedNextConfig interface
  • Default to "" in null-config fallback
  • Resolve from user config with trailing slash strip
  • Apply basePath inheritance rule: if assetPrefix is empty and basePath is set → assetPrefix = basePath

2. src/index.ts — Vite experimental.renderBuiltUrl

  • When assetPrefix is set, add Vite's experimental.renderBuiltUrl hook
  • Prefix type === "asset" (fonts, images) and type === "chunk" (JS) URLs with assetPrefix
  • Guard ssr: true so server-side bundles keep relative paths
  • Does not touch Vite's base (driven by basePath, separate concern)

3. src/shims/font-local.ts — font preload hints

  • collectFontPreloads() line ~329: extend URL guard from href.startsWith("/") to also accept https://, http://, and // prefixes
  • Without this fix, font <link rel="preload"> tags are silently dropped when assetPrefix is an absolute URL

4. src/entries/pages-server-entry.ts — Pages Router HTML injection

  • Embed assetPrefix in the serialised vinextConfigJson passed to the generated entry
  • In collectAssetTags generated code, replace hardcoded / href prefix with vinextConfig.assetPrefix + "/"

5. src/entries/app-rsc-entry.ts — App Router RSC serialisation

  • Embed assetPrefix in App Router RSC config alongside basePath

6. src/server/prod-server.ts — lazy chunks dedup

  • Verify lazy chunk set comparison uses relative paths (no CDN prefix leak into the dedup set)

7. src/index.ts (Cloudflare build plugin) — worker entry

  • Apply assetPrefix to __VINEXT_CLIENT_ENTRY__ global for Cloudflare Worker injection

8. Tests

  • Unit tests in tests/asset-prefix.test.ts
  • E2E test verifying CDN-prefixed asset URLs appear in rendered HTML

Coverage matrix (after this PR)

Asset type Covered
JS chunks via renderBuiltUrl
CSS chunks via renderBuiltUrl
Font files (next/font/local) via renderBuiltUrl + font-local.ts fix
<script> / <link> in SSR HTML via vinextConfig.assetPrefix in collectAssetTags
Font <link rel="preload"> via font-local.ts guard fix
public/ folder files not covered (matches Next.js spec)
/_next/image not covered (matches Next.js spec)

What assetPrefix does NOT cover (per spec)

Files in the public/ folder — if you want to serve those assets over a CDN, you'll have to introduce the prefix yourself.

This is intentional and matches the Next.js documentation.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/474 **Author:** [@elydelva](https://github.com/elydelva) **Created:** 3/11/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `feat/asset-prefix` --- ### 📝 Commits (10+) - [`00c8711`](https://github.com/cloudflare/vinext/commit/00c87115ab2047bfcea80be44820c659740ea04a) feat: add assetPrefix support in next.config - [`8b5071a`](https://github.com/cloudflare/vinext/commit/8b5071aec79aee20f813ec31b1e1ad4ad7166c4b) feat: support assetPrefix in next.config - [`d99cc97`](https://github.com/cloudflare/vinext/commit/d99cc979da694a9a40a04a944d1c601ff253d3f5) test: update entry-template snapshots for assetPrefix - [`04e09e1`](https://github.com/cloudflare/vinext/commit/04e09e153a721456086f2758722f5299841ce70f) test: add font-local CDN preload and basePath+assetPrefix combination tests - [`6949427`](https://github.com/cloudflare/vinext/commit/69494278a749400090470949dc29a717f37b08b0) test: add E2E test verifying CDN-prefixed asset URLs in rendered HTML - [`290a2aa`](https://github.com/cloudflare/vinext/commit/290a2aa9da75f8f52dc91e527e17a6d7e9d472b9) style: fix oxfmt formatting (semicolons, line length) - [`ef6d83a`](https://github.com/cloudflare/vinext/commit/ef6d83a9b4564a81f61772ec5abeae1986e80438) fix: avoid double basePath prefix in collectAssetTags when assetPrefix inherits basePath - [`eca4295`](https://github.com/cloudflare/vinext/commit/eca42954a4c2d4290894033bc91a318f9135568a) chore: update pnpm lockfile for asset-prefix fixture - [`dd8364e`](https://github.com/cloudflare/vinext/commit/dd8364e013af7d1bde0677b365cbe9310cdfc919) style: run oxfmt on all modified files - [`6150056`](https://github.com/cloudflare/vinext/commit/615005617aaa52a8c969a72deba2d1c31d45cd38) fix: harden assetPrefix edge cases and config composition ### 📊 Changes **23 files changed** (+622 additions, -31 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/config/next-config.ts` (+19 -4) 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+11 -7) 📝 `packages/vinext/src/entries/pages-server-entry.ts` (+17 -5) 📝 `packages/vinext/src/index.ts` (+24 -0) 📝 `packages/vinext/src/server/prod-server.ts` (+4 -0) 📝 `packages/vinext/src/shims/font-local.ts` (+10 -2) 📝 `playwright.config.ts` (+11 -0) 📝 `pnpm-lock.yaml` (+16 -0) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+17 -6) ➕ `tests/asset-prefix.test.ts` (+102 -0) ➕ `tests/e2e/asset-prefix/asset-prefix.spec.ts` (+128 -0) ➕ `tests/fixtures/asset-prefix/next-shims.d.ts` (+24 -0) ➕ `tests/fixtures/asset-prefix/next.config.mjs` (+6 -0) ➕ `tests/fixtures/asset-prefix/package.json` (+13 -0) ➕ `tests/fixtures/asset-prefix/pages/_app.tsx` (+6 -0) ➕ `tests/fixtures/asset-prefix/pages/about.tsx` (+10 -0) ➕ `tests/fixtures/asset-prefix/pages/index.tsx` (+11 -0) ➕ `tests/fixtures/asset-prefix/pages/styles.css` (+4 -0) ➕ `tests/fixtures/asset-prefix/tsconfig.json` (+11 -0) ➕ `tests/fixtures/asset-prefix/vite.config.ts` (+6 -0) _...and 3 more files_ </details> ### 📄 Description Closes #472 ## Summary `assetPrefix` from `next.config` is currently silently ignored in vinext. This PR implements full support conformant with the [Next.js `assetPrefix` spec](https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix). ## Motivation When deploying to Cloudflare Workers behind a gateway Worker (routing pattern for auth, multi-tenant apps, etc.), every request — including static fonts, JS chunks, and CSS — goes through the gateway and incurs a Worker invocation. With `assetPrefix`, static assets can be served from a dedicated subdomain (e.g. `assets-app.example.com`) backed by a Cloudflare Workers Assets-only deployment with `run_worker_first: false`, resulting in **zero Worker invocations** for static assets. ## Exploration Before writing code, the Next.js reference implementation and the vinext codebase were fully analysed: **Next.js (`vercel/next.js`)** — `assetPrefix` flows through: - `webpack output.publicPath` → all JS/CSS chunks - `next-font-loader` outputPath → font files baked at build time - Manual SSR HTML injection (`<script src>`, `<link href>`) in `app-render/` - Client-side runtime detection via bootstrap script URL (`client/asset-prefix.ts`) - Does **not** cover `public/` files or `/_next/image` (by design) - Key rule: when `assetPrefix` is empty and `basePath` is set → `assetPrefix` inherits `basePath` **vinext** — `assetPrefix` requires 8 changes across 5 files. Critical gap found: `shims/font-local.ts` line 329 gates `<link rel="preload">` emission on `href.startsWith("/")`, which silently breaks when `assetPrefix` is an absolute CDN URL. ## Changes ### 1. `src/config/next-config.ts` — config types + resolution - Add `assetPrefix?: string` to `NextConfig` interface - Add `assetPrefix: string` to `ResolvedNextConfig` interface - Default to `""` in null-config fallback - Resolve from user config with trailing slash strip - Apply `basePath` inheritance rule: if `assetPrefix` is empty and `basePath` is set → `assetPrefix = basePath` ### 2. `src/index.ts` — Vite `experimental.renderBuiltUrl` - When `assetPrefix` is set, add Vite's `experimental.renderBuiltUrl` hook - Prefix `type === "asset"` (fonts, images) and `type === "chunk"` (JS) URLs with `assetPrefix` - Guard `ssr: true` so server-side bundles keep relative paths - Does **not** touch Vite's `base` (driven by `basePath`, separate concern) ### 3. `src/shims/font-local.ts` — font preload hints - `collectFontPreloads()` line ~329: extend URL guard from `href.startsWith("/")` to also accept `https://`, `http://`, and `//` prefixes - Without this fix, font `<link rel="preload">` tags are silently dropped when `assetPrefix` is an absolute URL ### 4. `src/entries/pages-server-entry.ts` — Pages Router HTML injection - Embed `assetPrefix` in the serialised `vinextConfigJson` passed to the generated entry - In `collectAssetTags` generated code, replace hardcoded `/` href prefix with `vinextConfig.assetPrefix + "/"` ### 5. `src/entries/app-rsc-entry.ts` — App Router RSC serialisation - Embed `assetPrefix` in App Router RSC config alongside `basePath` ### 6. `src/server/prod-server.ts` — lazy chunks dedup - Verify lazy chunk set comparison uses relative paths (no CDN prefix leak into the dedup set) ### 7. `src/index.ts` (Cloudflare build plugin) — worker entry - Apply `assetPrefix` to `__VINEXT_CLIENT_ENTRY__` global for Cloudflare Worker injection ### 8. Tests - Unit tests in `tests/asset-prefix.test.ts` - E2E test verifying CDN-prefixed asset URLs appear in rendered HTML ## Coverage matrix (after this PR) | Asset type | Covered | |---|---| | JS chunks | ✅ via `renderBuiltUrl` | | CSS chunks | ✅ via `renderBuiltUrl` | | Font files (`next/font/local`) | ✅ via `renderBuiltUrl` + `font-local.ts` fix | | `<script>` / `<link>` in SSR HTML | ✅ via `vinextConfig.assetPrefix` in `collectAssetTags` | | Font `<link rel="preload">` | ✅ via `font-local.ts` guard fix | | `public/` folder files | ❌ not covered (matches Next.js spec) | | `/_next/image` | ❌ not covered (matches Next.js spec) | ## What `assetPrefix` does NOT cover (per spec) > Files in the `public/` folder — if you want to serve those assets over a CDN, you'll have to introduce the prefix yourself. This is intentional and matches the [Next.js documentation](https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix). --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#600
No description provided.