feat: support assetPrefix in next.config#474
feat: support assetPrefix in next.config#474elydelva wants to merge 12 commits intocloudflare:mainfrom
Conversation
Exploration: Next.js reference implementationBefore coding, I read through Key files:
Coverage confirmed by the spec:
if (result.assetPrefix === '') {
result.assetPrefix = result.basePath
}When |
Exploration: vinext codebase analysisExplored Critical finding — silent bug in // shims/font-local.ts ~line 329
if (href && href.startsWith("/") && !ssrFontPreloadHrefs.has(href)) {When
Must guard
Vite's |
Implementation notesAll 8 planned changes are in. Here's a summary of the test results on my end, so reviewers know what to expect from CI. Passing:
Pre-existing failures (not introduced by this PR):
Local environment caveat:
|
… tests - tests/font-asset-prefix.test.ts: covers collectFontPreloads accepting https://, http://, and // protocol-relative URLs — the bug where CDN URLs were silently dropped before the assetPrefix fix - tests/asset-prefix.test.ts: adds basePath+assetPrefix combination describe block asserting both fields simultaneously and verifying no double-prefix (e.g. "https://cdn.example.com/app") is produced
commit: |
Adds a new Pages Router fixture (tests/fixtures/asset-prefix/) configured with assetPrefix: "https://cdn.example.com" and a Playwright project (asset-prefix-prod) that runs a production build + vinext start on port 4180. The E2E spec (tests/e2e/asset-prefix/asset-prefix.spec.ts) asserts: - <script type="module" src="..."> tags begin with the CDN origin - <link rel="modulepreload" href="..."> tags begin with the CDN origin - <link rel="stylesheet" href="..."> tags (if present) begin with the CDN origin - <a href="..."> page navigation links do NOT carry the CDN prefix
…x inherits basePath When assetPrefix is a same-origin path (e.g. "/docs"), Vite already embeds basePath into every emitted asset filename, so prepending assetPrefix again produced /docs/docs/assets/chunk.js. Only use assetPrefix as a href prefix when it is an absolute CDN URL (https://, http://, or protocol-relative //). Otherwise fall back to an empty prefix so the existing "/" + tf produces the correct path.
Our assetPrefix additions used tab indentation while the rest of the codebase uses 2-space indentation. Running `pnpm run fmt` normalises the five affected files (tabs → spaces) so that `pnpm run fmt:check` passes cleanly.
|
/bigbonk review |
|
/bigbonk review |
There was a problem hiding this comment.
Review: assetPrefix support
The approach is well-researched and the PR description is thorough. The core implementation is correct: renderBuiltUrl for client-side builds, collectAssetTags for SSR HTML injection, and the font-local.ts preload fix.
I found one functional issue (unused variable in app-rsc-entry), one correctness concern (redundant renderBuiltUrl activation for basePath-only case), and several style/hygiene items.
Key findings
1. __assetPrefix is declared but never used in app-rsc-entry.ts — it gets serialized into the generated entry code but nothing reads it. This is dead code that should either be wired up (App Router asset tag injection) or removed until it's needed.
2. renderBuiltUrl fires unnecessarily when assetPrefix === basePath — When only basePath is set and no explicit assetPrefix, the inheritance rule sets assetPrefix = basePath. This activates renderBuiltUrl, which then does the exact same thing Vite's built-in base handling already does. It's functionally harmless today, but it's a subtle maintenance trap.
3. Import reordering in index.ts — The ~56-line import rearrangement is pure cosmetic churn that bloats the diff significantly and increases merge conflict risk with other in-flight branches. Consider reverting the import sort or splitting it into a separate commit.
4. Potential dedup mismatch with basePath + CDN assetPrefix — When both are set, clientEntryFile uses normalizeManifestFile (line 3163, strips leading / only → assets/entry.js) while lazyChunksData (line 3170) still uses manifestFilesWithBase (→ app/assets/lazy.js). In collectAssetTags, the seen set would contain assets/entry.js but SSR manifest values resolve to app/assets/entry.js, so seen.has(tf) dedup fails, potentially emitting the entry <script> twice. Worth adding a test case for basePath + CDN assetPrefix together.
5. experimental config clobbering — The { experimental: { renderBuiltUrl } } spread into the config object would silently overwrite any user-defined experimental options in their vite.config.ts. See inline comment.
Summary
The feature implementation is solid. The collectAssetTags CDN-only guard is the right design — it correctly handles the SSR manifest values that already contain the base prefix. The font preload fix is a genuine bug fix. Tests cover the important cases. The issues above are mostly about tightening up the edges.
(Please disregard the earlier "test" reviews — those were testing which diff lines the API accepts.)
| trailingSlash?: boolean, | ||
| config?: AppRouterConfig, | ||
| instrumentationPath?: string | null, | ||
| assetPrefix?: string, |
There was a problem hiding this comment.
assetPrefix is passed in and serialized to __assetPrefix (line 1283), but __assetPrefix is never referenced anywhere in the generated entry code. This is dead code.
If App Router doesn't need it yet (because the RSC plugin handles asset URLs differently), consider removing this parameter until it's actually wired up. Alternatively, if App Router asset tags should use this (analogous to the Pages Router collectAssetTags change), that's a gap worth noting.
packages/vinext/src/index.ts
Outdated
| ...(nextConfig.basePath ? { base: nextConfig.basePath + "/" } : {}), | ||
| // When assetPrefix is set, rewrite built asset/chunk URLs to the CDN origin. | ||
| // SSR environments stay relative so server-side imports resolve correctly. | ||
| ...(nextConfig.assetPrefix |
There was a problem hiding this comment.
When only basePath is set (no explicit assetPrefix), resolveNextConfig sets assetPrefix = basePath. This makes the condition truthy, so renderBuiltUrl activates — but it then produces exactly the same URLs that Vite's built-in base handling would have produced (basePath + "/" + filename), since filename from Vite does NOT include the base prefix.
This is functionally correct today, but it's a subtle redundancy that could become a bug if either path changes independently. Consider guarding this more precisely:
| ...(nextConfig.assetPrefix | |
| ...(nextConfig.assetPrefix && nextConfig.assetPrefix !== nextConfig.basePath |
This makes the intent explicit: only activate renderBuiltUrl when assetPrefix points somewhere different from basePath (i.e., an external CDN).
| // SSR environments stay relative so server-side imports resolve correctly. | ||
| ...(nextConfig.assetPrefix | ||
| ? { | ||
| experimental: { |
There was a problem hiding this comment.
This spreads { experimental: { renderBuiltUrl } } into the returned config object. If the user's vite.config.ts also sets experimental options, the shallow spread will silently overwrite them. Consider merging with the existing config:
experimental: {
...config.experimental,
renderBuiltUrl(...) { ... },
},Or at minimum, document that assetPrefix takes precedence over user-defined renderBuiltUrl.
| // or protocol-relative //). When assetPrefix is a same-origin path like | ||
| // "/docs", Vite already embeds basePath into every emitted asset path, so | ||
| // prepending it again would produce a double-prefix like /docs/docs/assets/. | ||
| var _assetHrefPrefix = (vinextConfig.assetPrefix && |
There was a problem hiding this comment.
Nit: var in new code. The rest of collectAssetTags uses var because it's generated ES5-ish code, so this is consistent with the surrounding style — just noting it.
| @@ -0,0 +1,92 @@ | |||
| /** | |||
There was a problem hiding this comment.
Good test coverage for config resolution. One missing edge case worth adding: assetPrefix set to just "/" (a single slash). After trailing-slash stripping this becomes "", which would trigger basePath inheritance. Is that the intended behavior? Next.js treats assetPrefix: "/" as "no prefix", which seems correct, but it's worth an explicit test to document this.
| import { describe, expect, it } from "vitest"; | ||
| import localFont, { getSSRFontPreloads } from "../packages/vinext/src/shims/font-local.js"; | ||
|
|
||
| describe("collectFontPreloads — CDN URL acceptance", () => { |
There was a problem hiding this comment.
These tests call localFont() without any cleanup between test cases, and getSSRFontPreloads() returns the cumulative global list. Each test asserts toContainEqual which works because unique font names are used, but the tests are order-dependent and would break if run in isolation with it.only. Consider adding a beforeEach that clears the preload set, or noting this limitation.
|
The review has been submitted successfully. Here's a summary of what was posted: Review posted on PR #474The review covers 6 inline comments and a comprehensive summary identifying 5 key findings:
|
|
@james-elicx Bonk workflow was cancelled. View workflow run · To retry, trigger Bonk again. |
|
The bot is going off the rails!! |
Preserve user experimental renderBuiltUrl config without clobbering, remove unused App Router assetPrefix plumbing, and add regression coverage for assetPrefix '/' plus basePath+CDN dedup behavior.
|
Follow-up updates are now in:
Validation run locally:
|
|
/bigbonk re-review |
Resolve conflict in packages/vinext/src/index.ts by keeping upstream changes and reapplying assetPrefix renderBuiltUrl composition, typed callback forwarding, and basePath guard behavior.
There was a problem hiding this comment.
Review: assetPrefix support
The feature implementation is well-researched and the core approach is correct: renderBuiltUrl for client builds, collectAssetTags for SSR HTML injection, and the font-local.ts preload fix. The PR description is thorough and the exploration comments are excellent.
However, there are two blocking issues and several significant concerns.
Blocking
-
Stale snapshots cause 6 test failures. The
entry-templates.test.tssnapshots containconst __assetPrefix = ""in the RSC entry output, butapp-rsc-entry.tsno longer generates this variable. Runningpnpm test tests/entry-templates.test.tsproduces 6 snapshot mismatches. The snapshots need to be regenerated withpnpm test tests/entry-templates.test.ts -- -u. -
Massive formatting churn in
index.ts. The diff shows +3685/-3334 forindex.ts, but the actual feature change is ~30 lines (therenderBuiltUrlblock). The rest is a full-file indent change from 2-space to tabs plus import reordering. This creates unnecessary merge conflicts with other in-flight work and makes the PR nearly impossible to review in GitHub's diff view. The formatting changes should be reverted or split into a separate commit/PR.
Non-blocking concerns
-
Unrelated code removal —
UserResolveConfigWithTsconfigPaths,viteMajorVersion,shouldEnableNativeTsconfigPaths, and the Vite 8tsconfigPathsconditional are all removed. These are functional changes unrelated toassetPrefixand should be in a separate PR. -
Font preload tests lack isolation —
font-local.tsuses module-level mutable state (ssrFontPreloads,ssrFontPreloadHrefs,classCounter) that accumulates across tests with no reset mechanism. The tests work around this with unique filenames andtoContainEqual, but they're order-dependent and would break withit.only. Consider usingvi.resetModules()in abeforeEachto get a fresh module instance. -
E2E test is HTTP-only, not browser-based — The Playwright tests use
request.get()to check HTML output, which is fine for verifying URL generation, but doesn't verify that CDN-prefixed scripts actually hydrate correctly. Given thathttps://cdn.example.comis unreachable, this is understandable, but worth noting the coverage gap.
|
|
||
|
|
||
| const __basePath = ""; | ||
| const __assetPrefix = ""; |
There was a problem hiding this comment.
Bug (blocking): This const __assetPrefix = "" line was generated by an earlier version of the code. The PR author removed __assetPrefix from app-rsc-entry.ts (correctly — it was dead code), but the snapshots weren't regenerated afterward.
Running pnpm test tests/entry-templates.test.ts produces 6 snapshot failures — all 6 RSC entry snapshots contain this line but the generated code no longer emits it.
Fix: run pnpm test tests/entry-templates.test.ts -- -u to update the snapshots.
packages/vinext/src/index.ts
Outdated
| // When assetPrefix is set (and differs from basePath inheritance), | ||
| // rewrite built asset/chunk URLs to the configured prefix. | ||
| // SSR environments stay relative so server-side imports resolve correctly. | ||
| ...(nextConfig.assetPrefix && |
There was a problem hiding this comment.
The renderBuiltUrl implementation is correct. The guard nextConfig.assetPrefix !== nextConfig.basePath correctly avoids redundant activation when assetPrefix was inherited from basePath. The config.experimental spread preserves user config, and the fallback to userRenderBuiltUrl for non-asset/chunk types is a nice touch.
One edge case: for ctx.type === "asset" || ctx.type === "chunk", the function always returns the CDN-prefixed URL without consulting userRenderBuiltUrl. This means a user can't selectively override CDN prefixing for specific assets. This is probably fine for v1 — if someone sets assetPrefix, they want all assets CDN-prefixed — but worth documenting if the option grows more complex.
| // or protocol-relative //). When assetPrefix is a same-origin path like | ||
| // "/docs", Vite already embeds basePath into every emitted asset path, so | ||
| // prepending it again would produce a double-prefix like /docs/docs/assets/. | ||
| var _assetHrefPrefix = (vinextConfig.assetPrefix && |
There was a problem hiding this comment.
The CDN-only guard here is the key design decision and it's correct. When assetPrefix === basePath (same-origin path), Vite's base already embeds the prefix into emitted paths, so prepending it again in collectAssetTags would double-prefix.
The var is consistent with the surrounding generated code style (ES5-ish). No issue here.
| // Vite transform should have already resolved them to absolute URLs. | ||
| if (href && href.startsWith("/") && !ssrFontPreloadHrefs.has(href)) { | ||
| // Accept https://, http://, and protocol-relative // URLs for assetPrefix CDN cases. | ||
| if ( |
There was a problem hiding this comment.
Good fix. This was a genuine silent bug — CDN-prefixed font URLs were being dropped because the guard only accepted href.startsWith("/"). The extended check for https://, http://, and // correctly handles all assetPrefix forms.
Minor: getFontMimeType() (line 306) determines MIME type from the URL suffix. For CDN URLs like https://cdn.example.com/assets/font-abc123.woff2, the .woff2 extension is still at the end, so this works correctly. Just noting it for completeness.
| env: config.env ?? {}, | ||
| basePath: config.basePath ?? "", | ||
| assetPrefix: | ||
| typeof config.assetPrefix === "string" ? config.assetPrefix.replace(/\/$/, "") : "", |
There was a problem hiding this comment.
The regex replace(/\/$/, "") strips a single trailing slash, which matches Next.js behavior. Worth noting that assetPrefix: "https://cdn.example.com//" (double slash) would only strip one, leaving a trailing /. Next.js has the same behavior, so this is consistent.
| import { describe, expect, it } from "vitest"; | ||
| import localFont, { getSSRFontPreloads } from "../packages/vinext/src/shims/font-local.js"; | ||
|
|
||
| describe("collectFontPreloads — CDN URL acceptance", () => { |
There was a problem hiding this comment.
These tests have a global state accumulation issue. localFont() pushes to ssrFontPreloads and ssrFontPreloadHrefs (module-level arrays/sets in font-local.ts) that are never cleared between tests. The unique filenames and toContainEqual assertions work around this, but:
- Tests are order-dependent
it.onlyon any single test would still pass, butgetSSRFontPreloads()returns the cumulative list- If another test file imports
font-local.tsfirst, the state leaks across files
Consider wrapping each test with vi.resetModules() + fresh dynamic import, or exporting a clearSSRFontPreloads() for testing.
|
|
||
| expect(new Set(moduleScriptSrcs).size).toBe(moduleScriptSrcs.length); | ||
| expect(new Set(modulePreloadHrefs).size).toBe(modulePreloadHrefs.length); | ||
| expect(moduleScriptSrcs.some((src) => src.includes("/docs/assets/"))).toBe(true); |
There was a problem hiding this comment.
Good regression test. This assertion moduleScriptSrcs.some((src) => src.includes("/docs/assets/")) verifies that the basePath path segment appears inside the CDN URL (e.g., https://cdn.example.com/docs/assets/entry-xxx.js), proving both basePath and assetPrefix are applied without double-prefixing.
The dedup assertions on lines 1561-1562 (Set.size === array.length) directly address the concern about duplicate <script> tags — good coverage.
|
The review has been submitted successfully. Here's a summary of what was posted: Review posted on PR #474Verdict: Changes Requested — 2 blocking issues found, 3 non-blocking concerns. Blocking issues:
Non-blocking concerns:
7 inline comments were posted on specific lines covering the |
- Remove __assetPrefix from RSC entry snapshots (variable no longer generated) - Regenerate pnpm-lock.yaml to fix lightningcss 1.31.1 vs 1.32.0 mismatch
|
Thanks for the thorough review. I've addressed the two blocking issues: Blocking (fixed):
Non-blocking (acknowledged):
Lockfile — Regenerated to fix the Ready for re-review. |
|
/bigbonk review |
There was a problem hiding this comment.
Review: assetPrefix support
Well-researched PR with thorough exploration of the Next.js reference implementation. The core design is sound: renderBuiltUrl for Vite-produced client assets, _assetHrefPrefix in collectAssetTags for SSR-injected HTML, and the font-local.ts preload guard fix. The previous review's blocking issues (stale snapshots, formatting churn, dead __assetPrefix in app-rsc-entry) have been addressed.
I have one correctness concern, one coverage gap observation, and a few minor items.
1. _assetHrefPrefix guard doesn't cover protocol-relative assetPrefix set via config
The _assetHrefPrefix logic in collectAssetTags only activates CDN prefixing for URLs starting with https://, http://, or //. But assetPrefix can legitimately be set to //cdn.example.com (protocol-relative) in next.config. This case is correctly handled — good.
However, there's no test covering assetPrefix: "//cdn.example.com" end-to-end. The asset-prefix.test.ts config tests only check https:// variants. Consider adding a protocol-relative config test to asset-prefix.test.ts to ensure the _assetHrefPrefix guard and renderBuiltUrl both handle it.
2. App Router asset prefix coverage gap
The PR correctly removed the dead __assetPrefix from app-rsc-entry.ts, but this means App Router SSR HTML has no assetPrefix injection path for collectAssetTags-equivalent logic. The App Router relies on the RSC plugin's loadBootstrapScriptContent() for the client entry, and CSS <link> tags are injected by the RSC rendering pipeline, not by hand-written collectAssetTags.
This means:
- JS chunks: Covered by
renderBuiltUrl(Vite rewrites the URLs at build time) - CSS: Covered by
renderBuiltUrl - Bootstrap script in HTML: Depends on how
@vitejs/plugin-rscgenerates the bootstrap<script>tag — if it uses the built URL from Vite's pipeline,renderBuiltUrlcovers it. If it constructs it manually, there's a gap.
This isn't necessarily a bug in this PR (the PR description says App Router coverage comes from renderBuiltUrl), but it's worth verifying with an App Router fixture test in a follow-up. The coverage matrix in the PR description should note this distinction.
3. Font preload test isolation (non-blocking, acknowledged)
The author acknowledged this in the thread. The font-local.ts module uses global mutable state (ssrFontPreloads, ssrFontPreloadHrefs) that accumulates across tests. The current workaround (unique filenames + toContainEqual) is fragile.
Summary
The implementation is correct for the Pages Router path and the Vite build pipeline. The basePath inheritance rule, the renderBuiltUrl guard for assetPrefix !== basePath, the CDN-only check in collectAssetTags, and the font preload fix are all well-reasoned. The regression test for basePath + absolute assetPrefix dedup (lines 1506-1598 in pages-router.test.ts) directly addresses the most dangerous edge case.
Good to merge once CI passes, with the protocol-relative test gap as a minor follow-up item.
| renderBuiltUrl(filename: string, ctx: { type: string; ssr: boolean }) { | ||
| if (ctx.ssr) return { relative: true }; | ||
| if (ctx.type === "asset" || ctx.type === "chunk") { | ||
| return nextConfig.assetPrefix + "/" + filename; |
There was a problem hiding this comment.
Subtle detail worth calling out: filename here does NOT include the base prefix when renderBuiltUrl is called by Vite. So when basePath: "/docs" and assetPrefix: "https://cdn.example.com", this produces https://cdn.example.com/assets/chunk-xxx.js — no /docs/ segment.
But in collectAssetTags, the SSR manifest values DO include the base prefix (docs/assets/chunk-xxx.js), so the CDN URL becomes https://cdn.example.com/docs/assets/chunk-xxx.js.
This means JS chunks loaded via <script> tags (from collectAssetTags) will have /docs/ in the path, but JS chunks loaded via native import() (from renderBuiltUrl) will NOT. Both resolve correctly because they point to different file references, but it's a subtlety worth a comment for future maintainers.
| // or protocol-relative //). When assetPrefix is a same-origin path like | ||
| // "/docs", Vite already embeds basePath into every emitted asset path, so | ||
| // prepending it again would produce a double-prefix like /docs/docs/assets/. | ||
| var _assetHrefPrefix = (vinextConfig.assetPrefix && |
There was a problem hiding this comment.
The CDN-only guard is the right design decision. One observation: if someone sets assetPrefix: "/cdn-path" (a same-origin absolute path that is NOT the same as basePath), this guard will treat it as non-CDN and skip prefixing, even though renderBuiltUrl WILL prefix it (since assetPrefix !== basePath). This creates an inconsistency where Vite-emitted asset URLs in JS/CSS use the prefix but SSR-injected <script>/<link> tags don't.
In practice this is unlikely — assetPrefix as a same-origin path different from basePath is an unusual config — but it's a spec divergence from Next.js, which applies assetPrefix unconditionally. Worth documenting as a known limitation or handling it.
| env: config.env ?? {}, | ||
| basePath: config.basePath ?? "", | ||
| assetPrefix: | ||
| typeof config.assetPrefix === "string" ? config.assetPrefix.replace(/\/$/, "") : "", |
There was a problem hiding this comment.
The replace(/\/$/, "") only strips a single trailing slash, which is correct and matches Next.js. Just noting: typeof config.assetPrefix === "string" correctly handles undefined, null, and other non-string values, falling back to "". Good defensive check.
| expect(resolved.assetPrefix).toBe("https://cdn.example.com/subdir"); | ||
| }); | ||
|
|
||
| it('treats assetPrefix "/" as empty and falls back to basePath inheritance', async () => { |
There was a problem hiding this comment.
Good edge case test — this was called out in the first review and added. The behavior ("/" strips to "", then inherits basePath) matches Next.js.
| }); | ||
| }); | ||
|
|
||
| it("does not collect bare relative font paths (no leading slash or scheme)", () => { |
There was a problem hiding this comment.
This negative test is important — it verifies that the expanded guard doesn't accidentally accept ALL strings. One additional case worth testing: "data:font/woff2;base64,..." — data URIs shouldn't be collected as preload hints since they're already inline.
| href && | ||
| (href.startsWith("/") || | ||
| href.startsWith("https://") || | ||
| href.startsWith("http://") || |
There was a problem hiding this comment.
Consider using a small helper or URL.canParse() check instead of manual prefix matching. The current approach is correct for the known cases, but a data: URI or a blob: URL would pass through if the guard were further relaxed in the future.
Not blocking — the current implementation is correct for the assetPrefix use case.
|
|
||
| // Extract config values (embedded at build time in the server entry) | ||
| const basePath: string = vinextConfig?.basePath ?? ""; | ||
| // assetBase is used only for internal manifest path lookups and dedup |
There was a problem hiding this comment.
Good clarifying comment. This was a source of confusion in the first review (potential dedup mismatch between assetBase and CDN assetPrefix). The comment makes it clear that assetBase is for manifest key lookups, not HTML href construction.
|
Review posted successfully. Here's a summary of what was submitted: Review posted on PR #474Verdict: COMMENT (no blocking issues remaining) The review covers 7 inline comments and identifies 3 key findings:
The previous review's blocking issues (stale snapshots, formatting churn, dead |
Closes #472
Summary
assetPrefixfromnext.configis currently silently ignored in vinext. This PR implements full support conformant with the Next.jsassetPrefixspec.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 withrun_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) —assetPrefixflows through:webpack output.publicPath→ all JS/CSS chunksnext-font-loaderoutputPath → font files baked at build time<script src>,<link href>) inapp-render/client/asset-prefix.ts)public/files or/_next/image(by design)assetPrefixis empty andbasePathis set →assetPrefixinheritsbasePathvinext —
assetPrefixrequires 8 changes across 5 files. Critical gap found:shims/font-local.tsline 329 gates<link rel="preload">emission onhref.startsWith("/"), which silently breaks whenassetPrefixis an absolute CDN URL.Changes
1.
src/config/next-config.ts— config types + resolutionassetPrefix?: stringtoNextConfiginterfaceassetPrefix: stringtoResolvedNextConfiginterface""in null-config fallbackbasePathinheritance rule: ifassetPrefixis empty andbasePathis set →assetPrefix = basePath2.
src/index.ts— Viteexperimental.renderBuiltUrlassetPrefixis set, add Vite'sexperimental.renderBuiltUrlhooktype === "asset"(fonts, images) andtype === "chunk"(JS) URLs withassetPrefixssr: trueso server-side bundles keep relative pathsbase(driven bybasePath, separate concern)3.
src/shims/font-local.ts— font preload hintscollectFontPreloads()line ~329: extend URL guard fromhref.startsWith("/")to also accepthttps://,http://, and//prefixes<link rel="preload">tags are silently dropped whenassetPrefixis an absolute URL4.
src/entries/pages-server-entry.ts— Pages Router HTML injectionassetPrefixin the serialisedvinextConfigJsonpassed to the generated entrycollectAssetTagsgenerated code, replace hardcoded/href prefix withvinextConfig.assetPrefix + "/"5.
src/entries/app-rsc-entry.ts— App Router RSC serialisationassetPrefixin App Router RSC config alongsidebasePath6.
src/server/prod-server.ts— lazy chunks dedup7.
src/index.ts(Cloudflare build plugin) — worker entryassetPrefixto__VINEXT_CLIENT_ENTRY__global for Cloudflare Worker injection8. Tests
tests/asset-prefix.test.tsCoverage matrix (after this PR)
renderBuiltUrlrenderBuiltUrlnext/font/local)renderBuiltUrl+font-local.tsfix<script>/<link>in SSR HTMLvinextConfig.assetPrefixincollectAssetTags<link rel="preload">font-local.tsguard fixpublic/folder files/_next/imageWhat
assetPrefixdoes NOT cover (per spec)This is intentional and matches the Next.js documentation.