fix(metadata): omit unused parent arg for cached generateMetadata - #1719
Conversation
A module-level `'use cache'` directive wraps `generateMetadata` in the cache runtime. vinext always called it as `generateMetadata(props, parent)`, where `parent` resolves to the merged ancestor metadata. When an ancestor sets `metadataBase: new URL(...)`, the cache-key encoder (`encodeReply`) awaited the parent promise and tried to serialize the `URL` instance, logging React's "Only plain objects can be passed to Server Functions from the Client. URL objects are not supported." warning. Match Next.js, which omits the `parent` argument for `generateMetadata` functions that don't use it (resolve-metadata.ts `getResult` / `useCacheFunctionInfo.usedArgs[1]`): - `resolveModuleMetadata` now only passes `parent` when `generateMetadata` declares it (`fn.length >= 2`). - `registerCachedFunction` preserves the original function's arity on the wrapper so the arity check works for cached functions. Adds regression tests for the arity gating, metadataBase preservation, and arity-preserving wrapper.
|
/bigbonk review |
commit: |
|
There was a problem hiding this comment.
Review: fix(metadata): omit unused parent arg for cached generateMetadata
Solid, well-targeted fix. The two changes are coherent and reinforce each other: resolveModuleMetadata gates the parent arg on arity (fn.length >= 2), and registerCachedFunction preserves the wrapped function's arity so the gate works for 'use cache' exports too. I confirmed the end-to-end path — the Vite transform (index.ts:3939) replaces mod.generateMetadata with the registerCachedFunction wrapper, so without the Object.defineProperty arity restore the wrapper would report length === 0 and parent would always be omitted. The two pieces are necessary together.
Verified:
tests/app-page-head.test.ts+tests/shims.test.tspass (1032 tests).- Ancestor metadata accumulation is unaffected:
accumulatedMetadatainserver/app-page-head.ts:308-314merges frommetadataResults, independent of whether a child consumesparent. So omittingparentfor an arity-0 child does not break inheritedmetadataBase(the test confirmsmetadataBaseURL still flows through). generateViewportdoes not pass aparentarg (metadata.tsx:44), so it cannot hit the sameURLserialization bug — no parity gap introduced there.
Minor caveat (non-blocking):
The fn.length >= 2 heuristic approximates Next.js's static usage analysis (usedArgs[1]), and JS arity has edge cases where it diverges:
generateMetadata(props, parent = fallback)reportslength === 1→ vinext would skipparenteven though it's declared.generateMetadata(props = {}, parent)reportslength === 0.
These are uncommon (a default value on the parent param of generateMetadata is unusual), and the PR comment already notes Next.js uses usedArgs[1] rather than arity. The approximation is reasonable; just worth a one-line note in the code comment that arity is an approximation of usage, in case someone later hits a default-param case.
Nit:
The try/catch around Object.defineProperty for the non-configurable length case is defensive but effectively dead — function length is always configurable: true per spec, and the wrapper is a fresh closure. Harmless, but it could be a plain Object.defineProperty call. Not worth changing.
Overall this matches Next.js behavior, has good regression coverage, and the dev/prod parity concern (the cached-wrapper path) is handled. LGTM.
|
Review posted to PR #1719. SummaryThe PR is a clean, well-targeted fix. I verified the full mechanism end-to-end: The fix works as designed:
Tests pass: 1032 tests across the two changed test files. Minor caveat I flagged (non-blocking): Nit: the Overall: matches Next.js behavior, good regression coverage, dev/prod parity handled. LGTM. |
…atch Address /bigbonk review feedback: - Document that fn.length approximates Next.js's usedArgs[1] analysis and diverges on default-parameter signatures. - Remove the dead try/catch around Object.defineProperty (function length is always configurable per spec).
|
/bigbonk re-review for issue and parity |
|
/bigbonk re-review for issues and parity |
|
@james-elicx Bonk workflow was cancelled. View workflow run · To retry, trigger Bonk again. |
There was a problem hiding this comment.
Re-review: fix(metadata): omit unused parent arg for cached generateMetadata
Re-reviewed after the two prior-review nits were addressed (arity-approximation note added to the code comment, dead try/catch dropped). The PR is in good shape — verified end-to-end and confirmed no parity gaps were introduced.
Verified
- Both changes are coherent and jointly necessary.
resolveModuleMetadatagatesparentonmod.generateMetadata.length >= 2(metadata.tsx:560-561), andregisterCachedFunctionrestores the wrapped function's arity (cache-runtime.ts:582) so the gate works for'use cache'exports. Without the arity restore the(...args)wrapper reportslength === 0andparentwould always be omitted. - Mechanism confirmed. The cache wrapper feeds args through
encodeReplyfor key generation (cache-runtime.ts:447). Passing the resolvedparentmetadata (which can carry aURLmetadataBase) is what produced the "URL objects are not supported" log. Omittingparentfor arity-0/1 functions avoids that path entirely. - Ancestor accumulation is unaffected.
accumulatedMetadatainserver/app-page-head.ts:236-244/308-314merges frommetadataResultsindependently of whether a child consumesparent, so inheritedmetadataBasestill flows through. The newapp-page-head.test.tstest confirmsmetadataBaseURL is preserved through an arity-0 child. - No parallel parity bug.
generateViewport(metadata.tsx:44) andgenerateImageMetadata(server/file-based-metadata.ts:505,server/metadata-route-response.ts:285) only ever receive a single{ params }arg — none passparent, so none can hit the same serialization issue. - Narrow blast radius. The wrapper
lengthoverride is consumed only byresolveModuleMetadata;hasGenerateMetadata(app-page-head.ts:143) checks existence, not arity, and no other code reads cached-function arity. - Tests pass: 1032 tests across
tests/app-page-head.test.ts+tests/shims.test.ts.vp checkclean on all four changed files.
Non-blocking
- The
fn.length >= 2heuristic approximates Next.js's static usage analysis (usedArgs[1]), diverging only on default-param signatures like(props, parent = x). This is now documented in the code comment (metadata.tsx:556-559) and is acceptable.
Matches Next.js behavior, good regression coverage, dev/prod parity handled (the cached-wrapper path is the shared mechanism). LGTM.
|
Review posted to PR #1719. SummaryRe-reviewed PR #1719 after the prior nits were addressed (arity-approximation note added, dead The fix is correct and complete:
No parity gaps introduced: Tests: 1032 pass across the two changed test files; Non-blocking: The LGTM. |
Problem
On routes where a parent layout uses a module-level
'use cache'directive and exports agenerateMetadata()that takes no arguments, vinext logged:(Reproduced on
app-router-playgroundatGET /layouts/clothing.)Root cause
'use cache'wrapsgenerateMetadatain the cache runtime. vinext always invoked it asgenerateMetadata(props, parent), whereparentresolves to the merged ancestor metadata. When an ancestor setsmetadataBase: new URL(...), the cache-key encoder (encodeReply) awaited theparentpromise and tried to serialize theURLinstance — which React rejects.Fix (matches Next.js)
Next.js omits the
parentargument forgenerateMetadatafunctions that don't use it (resolve-metadata.tsgetResult/useCacheFunctionInfo.usedArgs[1]), precisely so non-serializable parent values never reach the cache encoder.resolveModuleMetadatanow only passesparentwhengenerateMetadatadeclares it (fn.length >= 2).registerCachedFunctionpreserves the original function's arity on the wrapper (the wrapper is(...args)/ arity 0, which otherwise hides the signature) so the arity check works for cached functions too.Verification
metadataBaseURL resolution still works (og:image→ absolute URL).tests/app-page-head.test.ts(parent omitted for arity-0, still passed for arity-2,metadataBaseURL preserved) andtests/shims.test.ts(arity-preserving wrapper).features,file-based-metadata,nextjs-compat/metadata, andshimssuites pass (1395 tests);vp checkis clean.