* fix: pass NextFetchEvent to middleware and bubble up waitUntil promises to Cloudflare Worker ctx
- Instantiates `NextFetchEvent` with `waitUntil` shim and passes to `middlewareFn`
- Extracts `_waitUntilPromises` from the middleware result and attaches them to the `Response` object via `__vinextWaitUntil`
- Updates the generated Cloudflare Worker `fetch` signature to include `ctx: ExecutionContext`
- Iterates over `__vinextWaitUntil` on the final response and delegates to Cloudflare's native `ctx.waitUntil(promise)`
- Enables background tasks (like telemetry and session sync in `@clerk/nextjs`) to survive the worker response lifecycle
- Updates `vinext check` to explicitly report `@clerk/nextjs` as supported
- Adds `app-router.test.ts` assertion to verify `waitUntil` injection
* fix: address review feedback for waitUntil/NextFetchEvent support
- Fix lost waitUntil promises on redirect/custom response early returns
- Settle waitUntil promises in prod-server (Node.js) instead of dropping
- Mark @clerk/nextjs as "partial" until ESM fix lands (clerk/javascript#7954)
- Align __vinextWaitUntil guard in deploy.ts with app-router-entry.ts
- Use const for non-reassigned _waitUntilPromises array
- Add comment explaining waitUntil check ordering in app-router-entry
- Add unit test verifying runMiddleware bubbles up waitUntil promises
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: move waitUntil settlement before early returns in prod-server
Promise.allSettled(waitUntilPromises) was placed after the !result.continue
check, so redirect/custom-response paths silently dropped promises (e.g.
Clerk session sync on auth redirects). Moved it before the check to match
the pattern in deploy.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(middleware): remove double drainWaitUntil, include waitUntilPromises on error path
* fix: remove dead __vinextWaitUntil read code with no corresponding setter
* fix: remove no-op .catch on Promise.allSettled, strengthen waitUntil redirect test
* test: add waitUntil continue:true path coverage
* fix: settle waitUntil promises in Pages Router dev server (index.ts)
* fix: drain waitUntil promises in generated entry error paths
* regen snaps
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
* fix(google-fonts): use closure variables instead of this for plugin state
Vite does not bind `this` to the plugin object when calling hooks like
configResolved and transform.handler. Using `this._isBuild` etc. results
in a TypeError at startup ('Cannot set properties of undefined').
Replace the property-based state with closure variables so the plugin
works correctly regardless of how Vite invokes the hooks.
* fix(google-fonts): fix indentation of plugin object properties inside IIFE return
* fix: update tests to use closure-compatible plugin initialization
Tests were reaching into plugin internals (_isBuild, _fontCache,
_cacheDir) which are now closure variables after the IIFE refactor.
Updated to drive state through configResolved hook and mock fetch
for cache tests.
* fix(google-fonts): apply closure-variable fix on top of main's refactored plugin
main rewrote the plugin to add virtual module import rewriting but kept
the broken this-binding pattern. Apply the PR's fix: wrap in an IIFE so
state is held in closure vars (isBuild, fontCache, cacheDir) instead of
(this as any)._isBuild etc.
* fmt
* fix tests: replace plugin._isBuild direct access with initPlugin() helper
The google-fonts plugin closure fix removes the _isBuild/_fontCache/_cacheDir
properties from the returned Plugin object. Three tests from main's refactor
still used plugin._isBuild = false directly; replace with initPlugin(plugin,
{ command: 'serve' }) to simulate dev mode via the configResolved hook.
* fix: silence no-unused-vars lint warnings in font-google tests
Remove unused cacheDir variable and prefix unused fetch mock parameters
with underscore (_input, _init) per oxlint convention.
---------
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
Co-authored-by: James <james@eli.cx>
* fix: RSC compatibility for dynamic() and layout segment context
Three related fixes for React Server Component environments:
1. **dynamic.ts: Remove "use client" and add RSC async path**
The `"use client"` directive forced `next/dynamic` into a client
component boundary, but `dynamic()` should work in server components
too. In the RSC environment, `React.lazy` is not available (the
`react-server` condition exports a stripped-down React). Added a
runtime check: when `React.lazy` is not a function, use an async
server component pattern instead (the RSC renderer natively supports
async components).
Also switched from destructured imports (`lazy`, `Suspense`,
`useState`, `useEffect`) to `React.lazy`, `React.Suspense`, etc.
to avoid importing names that don't exist under the `react-server`
condition.
2. **layout-segment-context.tsx: Remove "use client"**
This module is imported directly by the RSC entry. The `"use client"`
directive created a client component boundary that breaks the RSC
rendering pipeline. `getLayoutSegmentContext()` already returns
`null` when `React.createContext` is unavailable (RSC), and the
`LayoutSegmentProvider` gracefully falls back to passing children
through unchanged.
3. **app-rsc-entry.ts: Wrap route handler params with makeThenableParams**
Next.js 15+ changed route handler params to be async (Promises).
Route handlers that `await params` crash when params is a plain
object. `makeThenableParams()` wraps the object so it's both a
Promise and has synchronous property access.
* chore: fix formatting and update entry-templates snapshots after merge with main
* fix: restore 'use client' in layout-segment-context to fix useSelectedLayoutSegment(s)
Removing 'use client' caused LayoutSegmentProvider to run in the RSC
environment where React.createContext is undefined. getLayoutSegmentContext()
returned null, the provider became a no-op, and useSelectedLayoutSegments
always returned [] instead of the actual segments.
The 'use client' boundary is required so the RSC bundler renders this
component in the SSR/browser environment where createContext works.
* test: add regression tests for RSC dynamic() and route handler await params
Add two regression tests for the fixes in PR #466:
1. Route handler await params (Next.js 15 async params pattern):
Tests /api/catch-all/[...slugs] which uses `await params` in the
handler — verifies that makeThenableParams() correctly wraps params
so both await and direct property access work.
2. dynamic() in RSC (async component path):
Adds a pure server component fixture that uses dynamic() without
"use client". In the RSC environment React.lazy is unavailable, so
dynamic() must fall back to the async component pattern. Tests that
the dynamically-loaded component renders in the HTML output.
* fix: address bonk review comments on dynamic.ts RSC async path
- Add comments explaining LoadingComponent is intentionally ignored in
the RSC async fallback path (parent Suspense boundaries handle loading
states; error handling defers to nearest error boundary)
- Add comment explaining the 'as unknown as ComponentType<P>' cast is
safe because the RSC renderer natively supports async components, but
TypeScript's ComponentType<P> doesn't account for async return types
- Add unit tests for the AsyncServerDynamic path by stubbing React.lazy
to undefined (simulating a react-server condition without lazy).
Verifies: displayName, async resolution, bare component exports,
and that LoadingComponent is correctly ignored.
Note: React 19.x exports React.lazy from the react-server condition,
so typeof React.lazy !== 'function' does not trigger in current React.
The AsyncServerDynamic path is defensive forward-compatibility code.
The unit tests stub React.lazy to exercise it directly.
* chore: remove unused vi import from dynamic.test.ts
* docs: correct inaccurate React.lazy/RSC comments throughout
React 19.x exports React.lazy from the react-server condition, so the
typeof React.lazy !== 'function' guard does not trigger in current
React. The AsyncServerDynamic path is defensive forward-compatibility
code only — it does not execute today.
Update all comments that incorrectly claimed React.lazy is unavailable
in the react-server environment:
- dynamic.ts: top-level doc comment and guard comment
- dynamic-rsc.tsx fixture: comment explaining the RSC path
- rsc-dynamic/page.tsx fixture: explain which path actually executes
- nextjs-compat/dynamic.test.ts: clarify that the integration test
exercises LazyServer + Suspense, not AsyncServerDynamic
---------
Co-authored-by: James <james@eli.cx>
* refactor: extract pages api route runtime
* refactor: share Pages ISR cache runtime (#684)
* fix: address Pages API route review feedback
- Replace fragile error.message string matches with typed PagesBodyParseError
(statusCode: 413) thrown from pages-node-compat.ts and api-handler.ts; the
existing instanceof catch in handlePagesApiRoute handles both 400 and 413
- Fix res.setHeader('set-cookie') to replace instead of append on repeated
calls, matching Node.js ServerResponse semantics
- Extract shared getMediaType/isJsonMediaType helpers and the body-parse error
class into pages-media-type.ts to eliminate drift between api-handler.ts and
pages-node-compat.ts
- Add unit tests for 404 (null match), missing default export (500), redirect
(1-arg and 2-arg), writeHead header lowercasing/array joining, setHeader/
getHeader round-trip, and set-cookie replace-not-append parity
---------
Co-authored-by: James <james@eli.cx>
* test: add static export E2E tests (#564)
Add Playwright E2E tests for `output: "export"` static builds covering
both App Router and Pages Router. Includes a lightweight static file
server with path traversal protection, proper error handling, and
startup validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update tests/e2e/static-export/serve-static.mjs
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* feat: add cacheForRequest() — per-request factory cache
Implements the cacheForRequest API discussed in #623.
API: import { cacheForRequest } from 'vinext/cache'
- Factory function reference is the cache key (no string collision)
- Async factories cache the Promise; rejected Promises clear their entry
so subsequent calls within the same request can retry
- Outside request scope: factory runs every time (no caching)
- Public barrel at src/cache.ts, separate from the next/cache shim
- Vite dev alias registered in index.ts
- 6 unit tests covering sync, async, isolation, nested scopes, reject+retry
* fix: address review feedback for cacheForRequest
- Switch test imports from vitest to vite-plus/test (repo standard)
- Use runWithUnifiedStateMutation in nested scope test (real code path)
- Add requestCache assertion to createRequestContext defaults test
- Fix JSDoc @param accuracy for async factory retry behavior
* fix: don't await createFromReadableStream before hydrateRoot (#695)
createFromReadableStream returns a React thenable, not a resolved tree.
Awaiting it blocked hydrateRoot until the entire RSC stream was consumed,
which prevented useEffect callbacks from ever firing in production on
Cloudflare Workers where RSC chunks arrive progressively via script tags.
Pass the thenable directly to hydrateRoot so React resolves components
lazily as stream chunks arrive -- matching the RSC contract and the
pattern already used in the SSR entry.
Adds a regression test fixture and e2e test for the Workers hydration
project that verifies useEffect fires after RSC hydration.
* fix: move effect-test page to app-router-cloudflare example
The cloudflare-workers e2e project builds examples/app-router-cloudflare,
not tests/fixtures/cf-app-basic. Move the regression test fixture page to
the correct location so the /effect-test route is actually served.
The App Router to Pages Router fallback path used
decodeURIComponent(url.pathname), which decoded %2F to /, changing
the path structure. This allowed middleware bypass: a request to
/security/admin%2Fpanel would skip the middleware matcher for
/security/admin/:path* but the Pages fallback would decode it to
/security/admin/panel and serve the protected page.
Replace with segment-wise decoding (decodePathParams) that splits by
/, decodes each segment individually, then re-encodes path delimiters
(/, #, ?) via escapePathDelimiters. This preserves the path structure
while still decoding non-ASCII characters within segments.
Both utilities are ported from Next.js:
- escapePathDelimiters: packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts
https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts
- decodePathParams: packages/next/src/server/lib/router-utils/decode-path-params.ts
https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/decode-path-params.ts
Adds unit tests for escapePathDelimiters (4 tests) and
decodePathParams (8 tests) covering non-ASCII decoding, delimiter
preservation, malformed encoding, and mixed paths.
Config rule has/missing conditions used unanchored regex matching,
so a condition like has:[cookie:role=admin] would match cookie values
containing 'admin' as a substring (e.g. 'not-admin', 'admin-temp').
Anchor the regex with ^ and $ to match the full value, matching
Next.js behavior (prepare-destination.ts uses new RegExp(`^${value}$`)).
Adds tests verifying exact match, substring rejection, superstring
rejection, and regex pattern compatibility with anchoring.
* fix: usePathname() returns "/" during SSR of "use client" page components (#688)
The navigation shim uses a registration pattern where navigation-state.ts
calls _registerStateAccessors() to upgrade navigation.ts from module-level
state to ALS-backed state. This only updates the specific module instance
that imported navigation-state.ts.
In Vite multi-environment dev mode, "use client" components can end up with
a separate module instance of navigation.ts (due to pre-bundling or different
resolver chains). That instance never receives the ALS-backed accessors and
falls back to _serverContext (null), causing usePathname() to return "/".
Fix: navigation-state.ts now also stores the accessor functions on globalThis
via Symbol.for("vinext.navigation.globalAccessors"). The default accessors in
navigation.ts check this global before falling back to module-level state.
This ensures all module instances can reach the ALS-backed state regardless
of which instance _registerStateAccessors was called on.
On the browser (where navigation-state.ts is never imported), the global key
does not exist, so the fallback to module-level _serverContext is preserved —
which is the correct behavior for client-side hydration.
Closes#688
* refactor: deduplicate global accessor key and tighten types
- Export GLOBAL_ACCESSORS_KEY from navigation.ts and import in
navigation-state.ts to prevent silent drift if the Symbol string
ever changes
- Add satisfies constraint on the accessor object so shape mismatches
are caught at compile time
- Assert warm-up fetch status in test to surface fixture setup failures
Headers.set() requires ByteString values (chars <= U+00FF). When dynamic
route params contain non-ASCII characters (Korean, Japanese, etc.),
JSON.stringify preserves them verbatim which causes a TypeError on the
Headers.set() call in buildAppPageRscResponse.
Fix: encodeURIComponent the JSON on the server side before setting the
header, and decodeURIComponent on the client side when reading it back
during initial hydration and client-side navigation.
Closes#676
* feat: seed memory cache from pre-rendered routes at startup
Read vinext-prerender.json and pre-rendered HTML/RSC files at production
server startup to populate the MemoryCacheHandler. This ensures the first
request to any pre-rendered page is a cache HIT instead of triggering a
full re-render.
- Add seedMemoryCacheFromPrerender() in server/seed-cache.ts
- Add buildId, router, and trailingSlash to the prerender manifest
- Call seeding during App Router production server startup
- 11 unit tests covering ISR routes, static routes, dynamic routes,
trailingSlash, graceful degradation, and missing files
Closes#561
* fix: address review feedback on cache seeding PR
- Warn on corrupt manifest JSON instead of silent swallow
- Always serialize trailingSlash (typeof boolean, not truthiness)
- Populate setRevalidateDuration() for seeded entries so
Cache-Control headers are correct on first HIT
- Return seeded route count and log it at startup
- Add tests: corrupt JSON, missing buildId, FNV hash path,
revalidate duration tracking, return value assertions
Addresses review comments on #645.
* fix: use dynamic WASM imports in og-font-patch for Node.js compat
Replace static `import X from "./X.wasm?module"` (workerd-only) with
dynamic `import()` + catch fallback so the same build output runs on
both Cloudflare Workers and Node.js (`vinext start`).
- Yoga WASM (~70KB): dynamic import with inline base64 fallback
- Resvg WASM (~1.3MB): dynamic import with disk-read fallback via
new URL() + readFileSync + WebAssembly.compile
Fixes#642
* fix: move resvg new URL() inside catch handler for workerd compat
In workerd, import.meta.url is "worker" (not a valid URL base), so
new URL("./resvg.wasm", import.meta.url) at the top level throws
TypeError at module load time. Move it inside the .catch() handler
which only runs on Node.js where import.meta.url is a file:// URL.
* refactor: address review — remove dead atob branch, async readFile, dedup tests
- Remove unreachable atob/Uint8Array fallback in yoga Node.js path
(mod === null only on Node.js where Buffer always exists)
- Use fs.promises.readFile instead of readFileSync for resvg disk
fallback (non-blocking, idiomatic in async chain)
- Deduplicate test transforms with shared beforeAll
- Remove unnecessary IIFE in test setup
- Add assertions for Buffer.from and async readFile
* fix: defer clearRequestContext() until HTML stream is fully consumed
headers()/cookies() throw on warm requests because clearRequestContext()
was called synchronously after receiving the RSC/SSR stream handle, before
any Server Components had actually executed. The RSC/SSR pipeline is lazy —
components run while the response body is pulled by the HTTP layer.
Fix: pipe the HTML stream through a TransformStream whose flush() callback
calls clearRequestContext(), deferring cleanup until the last byte is
consumed. Eagerly-read values (draftCookie, consumeDynamicUsage, etc.) are
still captured synchronously before the stream is returned.
Two sites fixed:
- server/app-page-render.ts (renderAppPageLifecycle, covers both the plain
and ISR-cacheable HTML paths)
- server/app-page-stream.ts (renderAppPageHtmlResponse)
Regression tests added for both sites.
Fixes#660
* fix: guard against stream-cancelled crashes in tick-buffered transform and early-disconnect cleanup
- createTickBufferedTransform: wrap the setTimeout callback in try/catch so
that a stream cancellation between scheduling and firing (e.g. a health-check
that calls res.body.cancel()) does not throw ERR_INVALID_STATE and crash the
Vite dev server process.
- deferUntilStreamConsumed: add a cancel() handler so clearRequestContext() is
still called when the HTTP consumer drops the connection before the stream is
fully drained, preventing per-request context leaks on early disconnect.
* feat: support inline next config
* Apply suggestion from @james-elicx
Co-authored-by: James Anderson <james@eli.cx>
* tweaks
---------
Co-authored-by: James Anderson <james@eli.cx>
* fix(image): strip priority prop before forwarding to UnpicImage to prevent DOM leak
The `priority` prop is a Next.js-specific concept that must never reach the
DOM as an attribute. On the two UnpicImage render paths (remote URL with fill
and remote URL with width+height), `priority={true}` was forwarded directly
to `@unpic/react`'s Image component which did not reliably strip it before
rendering the DOM `<img>`, triggering:
Received `true` for a non-boolean attribute `priority`.
Fix: replace `priority={priority}` with the equivalent HTML semantics —
`loading={priority ? 'eager' : loading ?? 'lazy'}` and
`fetchPriority={priority ? 'high' : undefined}` — matching what the local
image and custom-loader paths already do correctly.
Adds 10 reproduction tests covering both affected render paths.
* test: add 50ms slack to compressed streaming timing assertion to fix CI flakiness
Reworks the font-catalog removal from #262 using per-import virtual modules so named imports keep normal ESM semantics, aliased/default calls still self-host in build mode, and dependency imports are transformed too.
Credit to @yunus25jmi1 for the original PR and direction in #262.
* fix: prevent useActionState state becoming undefined when redirect() is called
Fixes issue #589 where useActionState receives undefined state when a Server Action
calls redirect() from next/navigation.
Root Cause:
When a server action calls redirect(), the server sends redirect headers with empty body.
RSC navigation expects a valid RSC payload but receives empty body, causing navigation to fail.
Solution:
Use hard redirect (location.assign/replace) for all action redirects. This matches
Next.js's completeHardNavigation behavior and ensures navigation always completes.
Changes:
- packages/vinext/src/server/app-browser-entry.ts: Use location.assign/replace for action redirects
- tests/e2e/app-router/server-actions.spec.ts: Add waitForHydration to test
- tests/fixtures/app-basic/app/actions/actions.ts: Add redirectWithActionState test action
- tests/fixtures/app-basic/app/action-state-redirect/page.tsx: Add test page
Note: This is a known parity gap - Next.js pre-renders the redirect target's RSC payload
for soft navigation, while vinext currently sends empty body requiring hard redirect.
Signed-off-by: Md Yunus <admin@yunuscollege.eu.org>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* Apply suggestion from @ask-bonk[bot]
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: James Anderson <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* feat: add vinext:optimize-imports plugin for barrel import rewriting
Barrel imports like `import { Slot } from "radix-ui"` cause RSC crashes
because Vite eagerly evaluates all re-exported sub-packages, some of which
call React.createContext() — unavailable in the react-server condition.
This plugin rewrites barrel imports into direct sub-module imports on the
server, matching Next.js's optimizePackageImports behavior:
- Parses barrel entry files to build export maps (namespace, named,
default re-exports, and import-then-export patterns)
- resolveId hook handles pnpm strict hoisting via Vite's own resolver
- Only runs on server environments; client uses Vite's dep optimizer
- Lazy-initialized Set<string> for O(1) package lookups
- Respects experimental.optimizePackageImports from next.config
- Includes Next.js default package list plus radix-ui
Closescloudflare/vinext#100Closescloudflare/vinext#137
* fix: add missing packages to DEFAULT_OPTIMIZE_PACKAGES
Add the full effect ecosystem (17 packages) and all react-icons/*
sub-packages (32 packages) to match Next.js's built-in defaults.
* fix: address bonk review — entryPathCache, buildStart init, Windows path fix, trailing semicolons, fixture-based transform tests
* fix: correct bySource grouping for mixed namespace+named imports, clear barrelCaches on rebuild
- Key bySource map on `${source}::${isNamespace}` instead of just source
so that a namespace re-export and a named re-export from the same sub-module
produce two separate import statements rather than one corrupted one.
Caught by the new 'produces separate statements for namespace and named
imports from the same source' test.
- Clear barrelCaches.exportMapCache and barrelCaches.subpkgOrigin in buildStart
alongside entryPathCache so stale barrel analysis doesn't survive rebuilds.
- Add regression test for the mixed namespace+named grouping bug.
* fix: resolve relative barrel re-export paths to absolute, emit default imports for default re-exports
Barrels like lodash-es and lucide-react use relative re-export paths
(e.g. `export { chunk } from './chunk.js'`). Writing those verbatim caused
Vite to resolve './chunk.js' against the importing user file rather than
the barrel package directory.
Fix: resolve any relative source path against the barrel entry's directory
before emitting the replacement import statement, producing an absolute
filesystem path that Vite resolves correctly regardless of where the
importing file lives.
Also fix: `export { default as X }` re-exports now produce `import X from`
(a default import) instead of `import { default as X }` (invalid syntax
that only works via ASI / bundler leniency).
Tests updated to assert absolute paths and added a dedicated test for
default re-export handling.
* refactor: extract barrel optimization to plugins/optimize-imports.ts, add Windows path fix and react-server TODO
- Move all barrel types, helpers, DEFAULT_OPTIMIZE_PACKAGES, and the
vinext:optimize-imports IIFE from index.ts into a new
packages/vinext/src/plugins/optimize-imports.ts file with a factory
function createOptimizeImportsPlugin(getNextConfig, getRoot)
- Forward-slash normalize resolved absolute paths so emitted import
statements use '/' separators on Windows (path.sep join)
- Add TODO comment in resolveExportsValue noting that the 'react-server'
condition should be added in a future pass
- Add test for ImportDefaultSpecifier: import MyFoo from 'pkg' rewrites
to import MyFoo from '<abs>/sub' (22 tests total, all passing)
* fix(optimize-imports): address sixth-pass review comments
- Fix astName() to throw on unexpected nodes instead of String(value)
- Normalize all resolvePackageEntry return values to forward slashes (Windows fix)
- Fix originalName falsy guard: && → !== undefined
- Pre-build quotedPackages in buildStart to avoid per-file template literal allocations
- Add first-wins comment on subpkgOrigin registration
- Add clarifying comment to lucide-react test fixture
* fix(optimize-imports): remove EnvironmentPluginContext cast, expand subpkgOrigin comment
Vite augments rolldown's MinimalPluginContext with `environment: Environment`
via declaration merging, so both resolveId and transform have `this.environment`
typed without any cast. Drop the EnvironmentPluginContext interface and the
`as unknown as EnvironmentPluginContext` cast in the transform handler, using
direct `this.environment` access consistently with resolveId.
Also expand the subpkgOrigin first-wins comment to make explicit why first-wins
is safe: the sub-package specifier resolves to the same path regardless of which
barrel's importer context is used.
* fix(optimize-imports): address seventh-pass review nits
- Fix test imports: vitest → vite-plus/test, Plugin from vite → vite-plus,
following the convention used by 82 of 85 test files in this repo
- Add TODO comment for extensionless absolute paths produced when barrel
sources lack file extensions (Vite resolves these correctly in practice)
- Expand subpkgOrigin first-wins comment to explicitly note the nested
node_modules edge case and why it is out of scope for the default list
* fix(optimize-imports): address james-elicx review comments
- Support wildcard re-exports (export * from './sub') by recursively
parsing sub-modules and merging their exports into the barrel map
- Add circular-reference guard to prevent infinite loops in recursive
export map building
- Refactor buildBarrelExportMap into buildExportMapFromFile helper
with shared recursive logic
- Use switch statements for spec.type handling instead of if-else chains
- Remove non-null assertions and type assertions; use optional chaining
and typeof guards throughout
- Extract resolvePackageInfo helper to separate concerns in
resolvePackageEntry
- Tests now import directly from optimize-imports.ts instead of via
the full vinext() plugin array
- Remove _buildBarrelExportMap re-export from index.ts (no longer needed
since the plugin has its own file)
- Fix type assertion for optimizePackageImports in next-config.ts using
a proper type-guard filter
- Apply README nit: 'is always optimized' → 'are always optimized'
- Add tests for wildcard export resolution, no-overwrite semantics,
and circular re-export safety
* fix(optimize-imports): address ask-bonk review comments
- Resolve relative sources to absolute paths inside buildExportMapFromFile
so nested wildcard re-exports (e.g. antd-style components/) resolve against
the correct sub-module directory instead of the barrel root
- Add react-server condition to resolveExportsValue (before node/import) for
RSC-compatible entry points in packages like react and react-dom
- Add NOTE comment in resolvePackageEntry documenting that only exports["."]
is checked, not subpath exports
- Eliminate double readFile call in buildBarrelExportMap by passing the
already-read content to buildExportMapFromFile via new initialContent param
- Update subpkgOrigin guard to check for absolute paths (not startsWith ".")
since relative sources are now stored as absolute paths in the export map
- Add test for nested subdirectory wildcard path resolution correctness
- Update existing unit tests to expect absolute paths in export map entries
* fix(optimize-imports): only prefer react-server condition in RSC environment
SSR renders with the full React runtime and must not resolve react-server
export condition entries. Thread preferReactServer (env.name === 'rsc')
through resolveExportsValue and resolvePackageEntry, and key entryPathCache
by environment prefix to keep RSC and SSR barrel entries separate.
* fix(optimize-imports): address review nits — TODO comment, type annotation, test names, wildcard dir test
- Add TODO comment on allResolved bail-out suggesting debug logging
- Remove redundant ': string[]' annotation on optimizePackageImports (inferred)
- Rename namespace re-export test to use generic X/barrel names instead of Slot/lucide-react
- Update syntax-error test to match new empty-map-on-parse-error behavior (not null)
- Add test for directory-style 'export * from "./components"' where components/index.js exists
* fix(optimize-imports): address review nits — wildcard extensions, double-read, readFile hoisting, subpkg registration guard
- Add /index.mjs, /index.tsx, and .tsx to wildcard candidate list for ESM-first and TypeScript-first packages
- Pass candidateContent to buildExportMapFromFile in wildcard loop to avoid reading each sub-module file twice
- Add comment clarifying the no-op resolveEntry callback pattern
- Hoist readFileSafe closure outside the import-declaration for-loop (one allocation per transform, not per barrel import)
- Gate subpkgOrigin registration loop with registeredBarrels Set so 50 files importing the same barrel don't each iterate the full export map
* fix(optimize-imports): remove redundant double-cache, extract PluginCtx type alias
- Remove the redundant exportMapCache.set() in buildBarrelExportMap — buildExportMapFromFile
already stores the result under the same key; the extra set was a no-op but misleading
- Extract PluginCtx type alias for the environment cast so all three hook sites share one
definition and the verbose inline cast doesn't repeat across resolveId and transform
* fix(optimize-imports): hoist readFileSafe to module level, add namespace import test
- Move readFileSafe from handler scope to module level since it captures
nothing from the per-file closure; avoids a per-call function allocation
- Add test verifying import * as Pkg from 'barrel' is left unchanged
(ImportNamespaceSpecifier sets allResolved = false — can't optimize a
full namespace import to a sub-module)
* fix(optimize-imports): make barrel resolution async, add debug logging, update tests
* fix(optimize-imports): address review feedback — extensions, env-keyed subpkgOrigin, debug gate, graceful astName
* fix: scope registeredBarrels key to env to prevent RSC registration blocking SSR
When RSC and SSR resolve the same barrel entry path (common — most packages
have no react-server export condition), the bare barrelEntry key in
registeredBarrels caused RSC's registration to suppress SSR's inner loop
entirely. SSR's subpkgOrigin map was left empty, making resolveId depend
solely on the cross-env fallback — fragile coupling that could silently break.
Change the registeredBarrels key from barrelEntry to `${envKey}:barrelEntry`
so each environment runs the inner loop and populates its own subpkgOrigin
map independently. Matches the existing entryPathCache key pattern.
Add a regression test covering the shared-barrel-entry scenario.
* fix: handle inline export declarations in barrel sub-modules (date-fns)
date-fns barrels use `export * from "./formatDistanceToNow.js"` where the
sub-module directly declares `export function formatDistanceToNow(...) {}`.
buildExportMapFromFile only handled ExportNamedDeclaration with a source
(re-exports) or specifiers (local re-bindings) — it silently skipped inline
declarations (export function/class/const/let/var), leaving the name absent
from the export map and causing the plugin to log "skipping: could not
resolve specifier" and leave the barrel import unchanged.
Handle the declaration branch: FunctionDeclaration and ClassDeclaration
contribute their id.name; VariableDeclaration iterates all declarators.
The source for inline declaration entries is the sub-module file itself.
Also extend AstBodyNode.declaration typing from `unknown` to the minimal
shape covering all three declaration kinds.
Add three unit tests covering export function, export const (multiple
declarators), and export class; all 38 tests pass.
* refactor: remove DEBUG-gated logging from optimize-imports plugin
* refactor: always emit skip log without DEBUG env gating
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
The head.ts SSR serializer was interpolating attribute names (the key
variable) directly into HTML without validation. Only attribute values
were escaped via escapeAttr(). This could produce malformed HTML when
attribute names contain characters like quotes, angle brackets, or
slashes.
Add isSafeAttrName() that validates attribute names against a strict
regex (alphanumeric, hyphens, colons, dots) and blocks inline event
handler attributes (on*). Applied in both SSR and client-side
syncClientHead() paths.
Also adds comprehensive unit tests for the validation function.
When storing fetch responses in the cache, all response headers were
included without filtering. This meant Set-Cookie headers from the
original response would be replayed to subsequent requests served from
cache, which is incorrect since Set-Cookie is per-response and should
not persist across different requests.
Both the primary cache write path and the stale-while-revalidate
background refresh path now skip Set-Cookie when collecting response
headers for the cache entry.
Adds a test verifying that the original response retains Set-Cookie
but the cached response does not.