UNPKG

next

Version:

The React Framework

341 lines (340 loc) • 16.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "getCachedImageResponseBody", { enumerable: true, get: function() { return getCachedImageResponseBody; } }); const _nodestream = require("node:stream"); const _nodecrypto = require("node:crypto"); const _invarianterror = require("../../shared/lib/invariant-error"); const _workasyncstorageexternal = require("../app-render/work-async-storage.external"); const _workunitasyncstorageexternal = require("../app-render/work-unit-async-storage.external"); const _dynamicrendering = require("../app-render/dynamic-rendering"); const _dynamicrenderingutils = require("../dynamic-rendering-utils"); const _manifestssingleton = require("../app-render/manifests-singleton"); const _static = require("react-server-dom-webpack/static"); const _client = require("react-server-dom-webpack/client"); function importOgModule() { // Cache Components is Node-only (rejected for the edge runtime at compile // time), so we always load the Node build. Loading it dynamically keeps the // heavy `@vercel/og` renderer (satori + WASM) off the module-load path, so // it's pulled in only when an image is actually rendered. return import('next/dist/compiled/@vercel/og/index.node.js'); } function getCachedImageResponseBody(args) { return new ReadableStream({ async start (controller) { const arrayBuffer = await getCachedImageResponseArrayBuffer(args); if (arrayBuffer.byteLength > 0) { controller.enqueue(new Uint8Array(arrayBuffer)); } controller.close(); } }); } async function getCachedImageResponseArrayBuffer(args) { const workUnitStore = _workunitasyncstorageexternal.workUnitAsyncStorage.getStore(); switch(workUnitStore == null ? void 0 : workUnitStore.type){ case 'prerender': break; case undefined: case 'request': case 'cache': case 'private-cache': case 'unstable-cache': case 'prerender-runtime': case 'prerender-client': case 'validation-client': case 'prerender-ppr': case 'prerender-legacy': case 'generate-static-params': return renderImageResponseArrayBuffer(args); default: return workUnitStore; } const { cacheSignal, resumeDataCache, renderSignal } = workUnitStore; if (!resumeDataCache) { return renderImageResponseArrayBuffer(args); } const workStore = _workasyncstorageexternal.workAsyncStorage.getStore(); if (!workStore) { throw Object.defineProperty(new _invarianterror.InvariantError('Expected a work store while caching an `ImageResponse` during prerendering.'), "__NEXT_ERROR_CODE", { value: "E1382", enumerable: false, configurable: true }); } const [element, options] = args; // `createHangingInputAbortSignal` aborts once the prerender's cache-sourced // input is ready, so anything the serialization below is still awaiting past // that point can be treated as dynamic (non-cache) input. In the prospective // pass it aborts when `cacheSignal.inputReady()` resolves (no cache reads // in progress); in the final pass the caches are already filled, so it just // aborts on the next tick. const hangingInputAbortSignal = (0, _dynamicrendering.createHangingInputAbortSignal)(workUnitStore); // We open the cache read lazily, once we know the serialization completed and // we're about to render and store the image. Opening it before serializing // would keep `cacheSignal.inputReady()` from resolving and thus prevent the // abort signal from ever firing, deadlocking the prospective prerender. let readState = 'ready'; function beginReadOnce() { if (readState === 'ready') { readState = 'pending'; cacheSignal == null ? void 0 : cacheSignal.beginRead(); } } function endReadIfStarted() { if (readState === 'pending') { cacheSignal == null ? void 0 : cacheSignal.endRead(); } readState = 'done'; } // We serialize the element tree with `prerenderToNodeStream` rather than // `renderToPipeableStream`. It's the right fit for prerendering, and it // schedules work deferred for size (`deferTask`) on microtasks, so a fully // static tree finishes flushing before the abort signal fires; a tree still // pending at abort time is then genuinely waiting on dynamic input rather // than just deferred. // // `renderToPipeableStream` would schedule that deferred work on // `setImmediate` instead, which isn't necessarily a deal-breaker: the // sequential-task scheme page rendering uses (`runInSequentialTasks`) drains // pending immediates at each task boundary, so deferred work still runs in // time. But route handler prerendering doesn't use that scheme, so here the // deferred immediates would race the abort. // // The prerender halts silently on abort, leaving unfulfilled references in // place rather than reporting through `onError`. So to tell a halt (the tree // needed dynamic input) apart from a normal completion, we record whether the // abort fired before the serialization finished. `abort()` runs this listener // synchronously, well before we read `resultIsPartial` below. let prerenderCompleted = false; let resultIsPartial = false; let serializationError; hangingInputAbortSignal.addEventListener('abort', ()=>{ if (!prerenderCompleted) { resultIsPartial = true; } }, { once: true }); const { clientModules, rscModuleMapping } = (0, _manifestssingleton.getClientReferenceManifest)(); try { // We serialize only the `element`. It's the part that needs Flight, to run // its async Server Components once and to surface any dynamic input. The // `options` are already-resolved plain data; they're folded into the cache // key directly and passed to satori as-is below. const { prelude } = await (0, _static.prerenderToNodeStream)(element, clientModules, { signal: hangingInputAbortSignal, filterStackFrame: undefined, onError (error) { // A halt (our deliberate abort) emits nothing, so this is only called // for genuine serialization errors. We surface the first one. if (serializationError === undefined && !resultIsPartial) { serializationError = error; } } }); prerenderCompleted = true; if (serializationError !== undefined) { throw serializationError; } if (resultIsPartial) { // The element tree needed dynamic input (e.g. `cookies()` or an uncached // `fetch`), so the image can't be produced statically. Return a hanging // promise: the body never resolves, and the final prerender's macrotask // budget then classifies the route as dynamic. // Whatever dynamic input made the element partial already classified // itself when it created its own hanging promise (cookies() creates a // runtime hanging promise, an uncached fetch creates a dynamic one, // ...), so this wrapper adds no new information and can use the // non-recording dynamic variant. return (0, _dynamicrenderingutils.makeDynamicHangingPromise)(renderSignal, workStore.route, 'dynamic `ImageResponse`'); } // The serialization finished before any dynamic input was needed, so we // will render and cache the image. Hold the cache read now, before the // stream is buffered and deserialized below, so that the prospective // prerender's `cacheReady()` waits for the image to be stored. beginReadOnce(); const chunks = []; for await (const chunk of prelude){ chunks.push(chunk); } const elementBuffer = Buffer.concat(chunks); // Derive a stable cache key from the serialized element plus the options. // We hash rather than reuse the raw serialized bytes so the key stays // compact even for large inputs (e.g. embedded fonts), and we fold the // options in by content so two images that differ only in their options // (size, fonts, ...) don't collide. The options are hashed directly here, // never serialized through Flight, which would both bloat the key and apply // `Buffer.prototype .toJSON` to font data. const hash = (0, _nodecrypto.createHash)('sha256'); hash.update(elementBuffer); updateHashWithOptions(hash, options); const cacheKey = hash.digest('base64'); const cached = resumeDataCache.imageResponses.get(cacheKey); if (cached) { return await cached; } // Deserialize the element and hand it to satori. Because the user's // components already ran during serialization, satori only walks resolved // host elements and never re-runs them, confining user-space I/O to the // in-store serialization above. const deserializedElement = await (0, _client.createFromNodeStream)(_nodestream.Readable.from([ elementBuffer ]), { // We don't want to trigger preloads of client references here. moduleLoading: null, moduleMap: rscModuleMapping, serverModuleMap: (0, _manifestssingleton.getServerModuleMap)() }, { findSourceMapURL: undefined }); // The Flight client hands back the output of an async Server Component as // a `React.lazy` (sync components and plain host elements are inlined). // satori can't unwrap lazies, so we resolve them into plain elements first. // We only reach here once the serialization completed, so every lazy is // already resolved and `_init` returns synchronously. const resolvedElement = resolveFlightLazies(deserializedElement); // Pair the resolved element with the original, in-memory `options`, which // never went through Flight. This keeps the font `Buffer` intact: had it // been serialized, Flight would apply the `toJSON` method that Node's // `Buffer` carries, turning it into a `{ type: 'Buffer', data: [...] }` // object that satori's font parser rejects (it needs an `ArrayBuffer` or a // typed array). const resolvedArgs = [ resolvedElement, options ]; // Render satori outside the prerender work-unit store. It does uncached // `fetch` calls (e.g. loading a font), and inside a Cache Components // prerender an uncached `fetch` outside a cache scope becomes a hanging // promise. Those are framework fetches, not user I/O, so we let them // resolve normally with no store. const arrayBufferPromise = _workunitasyncstorageexternal.workUnitAsyncStorage.exit(()=>renderImageResponseArrayBuffer(resolvedArgs)); if (resumeDataCache.mutable) { resumeDataCache.imageResponses.set(cacheKey, arrayBufferPromise); } return await arrayBufferPromise; } finally{ endReadIfStarted(); } } /** * Updates a hash with a stable encoding of the `ImageResponse` options so they * can participate in the cache key without being serialized through Flight. * Binary values (font `Buffer`s, `ArrayBuffer`s, typed arrays) are hashed by * their raw bytes; objects are walked in sorted-key order. * * `ImageResponse` options are plain data: numbers, strings, booleans, nested * plain objects/arrays, and binary font data. Exotic objects such as `Map` or * `Date` keep their state outside their enumerable own keys, so the key walk * below would hash them incorrectly. Options never contain these, but we warn * if one ever shows up so a mis-keyed cache can be reported. * * The encoding is self-delimiting: every node starts with a type tag, and * variable-length parts (byte runs, primitives, keys) are length-prefixed, * while arrays and objects are count-prefixed. This makes it injective, so no * concatenation of values can be mistaken for a differently shaped input. */ function updateHashWithOptions(hash, value) { if (value === undefined) { hash.update('u'); return; } if (value === null) { hash.update('n'); return; } const type = typeof value; if (type !== 'object') { // Tag with the primitive type so e.g. the number `1` and the string `'1'` // don't hash the same. updateHashWithBytes(hash, 'p', Buffer.from(`${type}:${String(value)}`)); return; } if (value instanceof ArrayBuffer) { updateHashWithBytes(hash, 'a', new Uint8Array(value)); return; } if (ArrayBuffer.isView(value)) { updateHashWithBytes(hash, 'v', new Uint8Array(value.buffer, value.byteOffset, value.byteLength)); return; } if (Array.isArray(value)) { hash.update(`[${value.length},`); for (const item of value){ updateHashWithOptions(hash, item); } return; } // The key walk below captures a plain object faithfully, but an exotic object // keeps its state elsewhere (a `Map`'s/`Set`'s entries, a `Date`'s time), so // two different values would hash the same and could return the wrong cached // image. This shouldn't happen for `ImageResponse` options, so we warn rather // than fail, then hash best-effort, so it can be reported. Not gated on // `NODE_ENV`: this runs during the production `next build` prerender, where // the warning is most useful. const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) { var _value_constructor; const typeName = ((_value_constructor = value.constructor) == null ? void 0 : _value_constructor.name) ?? 'object'; console.warn(`Cannot reliably include an \`ImageResponse\` option of type ` + `\`${typeName}\` in the cache key, so different images may collide and ` + `return an incorrect cached result. Please report this to the Next.js ` + `team.`); } const keys = Object.keys(value).sort(); hash.update(`{${keys.length},`); for (const key of keys){ updateHashWithBytes(hash, 'k', Buffer.from(key)); updateHashWithOptions(hash, value[key]); } } /** * Hashes a length-prefixed, tagged byte run: `<tag><byteLength>:<bytes>`. The * length prefix keeps the run self-delimiting so it can't blend into adjacent * nodes. */ function updateHashWithBytes(hash, tag, bytes) { hash.update(`${tag}${bytes.byteLength}:`); hash.update(bytes); } async function renderImageResponseArrayBuffer(args) { const OGImageResponse = (await importOgModule()).ImageResponse; const imageResponse = new OGImageResponse(...args); if (!imageResponse.body) { return new ArrayBuffer(0); } return imageResponse.arrayBuffer(); } const REACT_LAZY_TYPE = Symbol.for('react.lazy'); /** * Recursively replaces the `React.lazy` references that Flight emits for * resolved async Server Components with the elements they resolve to, so that * satori (which doesn't understand lazy nodes) can walk the tree. This must * only be called on a fully resolved (completed) Flight result, where each * lazy's `_init` returns synchronously rather than suspending. */ function resolveFlightLazies(node) { if (node === null || typeof node !== 'object') { return node; } if (node.$$typeof === REACT_LAZY_TYPE) { const lazy = node; return resolveFlightLazies(lazy._init(lazy._payload)); } if (Array.isArray(node)) { return node.map(resolveFlightLazies); } const element = node; if (element.props && 'children' in element.props) { return { ...element, props: { ...element.props, children: resolveFlightLazies(element.props.children) } }; } return node; } //# sourceMappingURL=cache-image-response.js.map