Recipe-app main
This commit is contained in:
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { createAsyncLocalStorage } from './async-local-storage';
|
||||
export const actionAsyncStorageInstance = createAsyncLocalStorage();
|
||||
|
||||
//# sourceMappingURL=action-async-storage-instance.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/action-async-storage-instance.ts"],"sourcesContent":["import type { ActionAsyncStorage } from './action-async-storage.external'\nimport { createAsyncLocalStorage } from './async-local-storage'\n\nexport const actionAsyncStorageInstance: ActionAsyncStorage =\n createAsyncLocalStorage()\n"],"names":["createAsyncLocalStorage","actionAsyncStorageInstance"],"mappings":"AACA,SAASA,uBAAuB,QAAQ,wBAAuB;AAE/D,OAAO,MAAMC,6BACXD,0BAAyB","ignoreList":[0]}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
// Share the instance module in the next-shared layer
|
||||
import { actionAsyncStorageInstance } from './action-async-storage-instance' with {
|
||||
'turbopack-transition': 'next-shared'
|
||||
};
|
||||
export { actionAsyncStorageInstance as actionAsyncStorage };
|
||||
|
||||
//# sourceMappingURL=action-async-storage.external.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/action-async-storage.external.ts"],"sourcesContent":["import type { AsyncLocalStorage } from 'async_hooks'\n\n// Share the instance module in the next-shared layer\nimport { actionAsyncStorageInstance } from './action-async-storage-instance' with { 'turbopack-transition': 'next-shared' }\nexport interface ActionStore {\n readonly isAction?: boolean\n readonly isAppRoute?: boolean\n}\n\nexport type ActionAsyncStorage = AsyncLocalStorage<ActionStore>\n\nexport { actionAsyncStorageInstance as actionAsyncStorage }\n"],"names":["actionAsyncStorageInstance","actionAsyncStorage"],"mappings":"AAEA,qDAAqD;AACrD,SAASA,0BAA0B,QAAQ,uCAAuC;IAAE,wBAAwB;AAAc,EAAC;AAQ3H,SAASA,8BAA8BC,kBAAkB,GAAE","ignoreList":[0]}
|
||||
+993
@@ -0,0 +1,993 @@
|
||||
import { RSC_HEADER, RSC_CONTENT_TYPE_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, ACTION_HEADER, NEXT_ACTION_NOT_FOUND_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_URL, NEXT_ACTION_REVALIDATED_HEADER } from '../../client/components/app-router-headers';
|
||||
import { getAccessFallbackHTTPStatus, isHTTPAccessFallbackError } from '../../client/components/http-access-fallback/http-access-fallback';
|
||||
import { getRedirectTypeFromError, getURLFromRedirectError } from '../../client/components/redirect';
|
||||
import { isRedirectError } from '../../client/components/redirect-error';
|
||||
import RenderResult from '../render-result';
|
||||
import { FlightRenderResult } from './flight-render-result';
|
||||
import { filterReqHeaders, actionsForbiddenHeaders } from '../lib/server-ipc/utils';
|
||||
import { getModifiedCookieValues } from '../web/spec-extension/adapters/request-cookies';
|
||||
import { JSON_CONTENT_TYPE_HEADER, NEXT_CACHE_REVALIDATED_TAGS_HEADER, NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER } from '../../lib/constants';
|
||||
import { getServerActionRequestMetadata } from '../lib/server-action-request-meta';
|
||||
import { isCsrfOriginAllowed } from './csrf-protection';
|
||||
import { warn } from '../../build/output/log';
|
||||
import { RequestCookies, ResponseCookies } from '../web/spec-extension/cookies';
|
||||
import { HeadersAdapter } from '../web/spec-extension/adapters/headers';
|
||||
import { fromNodeOutgoingHttpHeaders } from '../web/utils';
|
||||
import { selectWorkerForForwarding, getServerActionsManifest, getServerModuleMap } from './manifests-singleton';
|
||||
import { isNodeNextRequest, isWebNextRequest } from '../base-http/helpers';
|
||||
import { normalizeFilePath } from './segment-explorer-path';
|
||||
import { extractInfoFromServerReferenceId } from '../../shared/lib/server-reference-info';
|
||||
import { RedirectStatusCode } from '../../client/components/redirect-status-code';
|
||||
import { synchronizeMutableCookies } from '../async-storage/request-store';
|
||||
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external';
|
||||
import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
import { executeRevalidates } from '../revalidation-utils';
|
||||
import { addRequestMeta, getRequestMeta } from '../request-meta';
|
||||
import { setCacheBustingSearchParam } from '../../client/components/router-reducer/set-cache-busting-search-param';
|
||||
import { ActionDidNotRevalidate, ActionDidRevalidateStaticAndDynamic } from '../../shared/lib/action-revalidation-kind';
|
||||
const INLINE_ACTION_PREFIX = '$$RSC_SERVER_ACTION_';
|
||||
/**
|
||||
* Checks if the app has any server actions defined in any runtime.
|
||||
*/ function hasServerActions() {
|
||||
const serverActionsManifest = getServerActionsManifest();
|
||||
return Object.keys(serverActionsManifest.node).length > 0 || Object.keys(serverActionsManifest.edge).length > 0;
|
||||
}
|
||||
function nodeHeadersToRecord(headers) {
|
||||
const record = {};
|
||||
for (const [key, value] of Object.entries(headers)){
|
||||
if (value !== undefined) {
|
||||
record[key] = Array.isArray(value) ? value.join(', ') : `${value}`;
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
function getForwardedHeaders(req, res) {
|
||||
// Get request headers and cookies
|
||||
const requestHeaders = req.headers;
|
||||
const requestCookies = new RequestCookies(HeadersAdapter.from(requestHeaders));
|
||||
// Get response headers and cookies
|
||||
const responseHeaders = res.getHeaders();
|
||||
const responseCookies = new ResponseCookies(fromNodeOutgoingHttpHeaders(responseHeaders));
|
||||
// Merge request and response headers
|
||||
const mergedHeaders = filterReqHeaders({
|
||||
...nodeHeadersToRecord(requestHeaders),
|
||||
...nodeHeadersToRecord(responseHeaders)
|
||||
}, actionsForbiddenHeaders);
|
||||
// Merge cookies into requestCookies, so responseCookies always take precedence
|
||||
// and overwrite/delete those from requestCookies.
|
||||
responseCookies.getAll().forEach((cookie)=>{
|
||||
if (typeof cookie.value === 'undefined') {
|
||||
requestCookies.delete(cookie.name);
|
||||
} else {
|
||||
requestCookies.set(cookie);
|
||||
}
|
||||
});
|
||||
// Update the 'cookie' header with the merged cookies
|
||||
mergedHeaders['cookie'] = requestCookies.toString();
|
||||
// Remove headers that should not be forwarded
|
||||
delete mergedHeaders['transfer-encoding'];
|
||||
return new Headers(mergedHeaders);
|
||||
}
|
||||
function addRevalidationHeader(res, { workStore, requestStore }) {
|
||||
var _workStore_pendingRevalidatedTags;
|
||||
// If a tag was revalidated, the client router needs to invalidate all the
|
||||
// client router cache as they may be stale. And if a path was revalidated, the
|
||||
// client needs to invalidate all subtrees below that path.
|
||||
// TODO: Currently we don't send the specific tags or paths to the client,
|
||||
// we just send a flag indicating that all the static data on the client
|
||||
// should be invalidated. In the future, this will likely be a Bloom filter
|
||||
// or bitmask of some kind.
|
||||
// TODO-APP: Currently the prefetch cache doesn't have subtree information,
|
||||
// so we need to invalidate the entire cache if a path was revalidated.
|
||||
// TODO-APP: Currently paths are treated as tags, so the second element of the tuple
|
||||
// is always empty.
|
||||
// Only count tags without a profile (updateTag) as requiring client cache invalidation
|
||||
// Tags with a profile (revalidateTag) use stale-while-revalidate and shouldn't
|
||||
// trigger immediate client-side cache invalidation
|
||||
const isTagRevalidated = ((_workStore_pendingRevalidatedTags = workStore.pendingRevalidatedTags) == null ? void 0 : _workStore_pendingRevalidatedTags.some((item)=>item.profile === undefined)) ? 1 : 0;
|
||||
const isCookieRevalidated = getModifiedCookieValues(requestStore.mutableCookies).length ? 1 : 0;
|
||||
// First check if a tag, cookie, or path was revalidated.
|
||||
if (isTagRevalidated || isCookieRevalidated) {
|
||||
res.setHeader(NEXT_ACTION_REVALIDATED_HEADER, JSON.stringify(ActionDidRevalidateStaticAndDynamic));
|
||||
} else if (// Check for refresh() actions. This will invalidate only the dynamic data.
|
||||
workStore.pathWasRevalidated !== undefined && workStore.pathWasRevalidated !== ActionDidNotRevalidate) {
|
||||
res.setHeader(NEXT_ACTION_REVALIDATED_HEADER, JSON.stringify(workStore.pathWasRevalidated));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Forwards a server action request to a separate worker. Used when the requested action is not available in the current worker.
|
||||
*/ async function createForwardedActionResponse(req, res, host, workerPathname, basePath) {
|
||||
var _getRequestMeta;
|
||||
if (!host) {
|
||||
throw Object.defineProperty(new Error('Invariant: Missing `host` header from a forwarded Server Actions request.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E226",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const forwardedHeaders = getForwardedHeaders(req, res);
|
||||
// indicate that this action request was forwarded from another worker
|
||||
// we use this to skip rendering the flight tree so that we don't update the UI
|
||||
// with the response from the forwarded worker
|
||||
forwardedHeaders.set('x-action-forwarded', '1');
|
||||
const proto = ((_getRequestMeta = getRequestMeta(req, 'initProtocol')) == null ? void 0 : _getRequestMeta.replace(/:+$/, '')) || 'https';
|
||||
// For standalone or the serverful mode, use the internal origin directly
|
||||
// other than the host headers from the request.
|
||||
const origin = process.env.__NEXT_PRIVATE_ORIGIN || `${proto}://${host.value}`;
|
||||
const fetchUrl = new URL(`${origin}${basePath}${workerPathname}`);
|
||||
try {
|
||||
var _response_headers_get;
|
||||
let body;
|
||||
if (// The type check here ensures that `req` is correctly typed, and the
|
||||
// environment variable check provides dead code elimination.
|
||||
process.env.NEXT_RUNTIME === 'edge' && isWebNextRequest(req)) {
|
||||
if (!req.body) {
|
||||
throw Object.defineProperty(new Error('Invariant: missing request body.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E333",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
body = req.body;
|
||||
} else if (// The type check here ensures that `req` is correctly typed, and the
|
||||
// environment variable check provides dead code elimination.
|
||||
process.env.NEXT_RUNTIME !== 'edge' && isNodeNextRequest(req)) {
|
||||
body = req.stream();
|
||||
} else {
|
||||
throw Object.defineProperty(new Error('Invariant: Unknown request type.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E114",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// Forward the request to the new worker
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'POST',
|
||||
body,
|
||||
duplex: 'half',
|
||||
headers: forwardedHeaders,
|
||||
redirect: 'manual',
|
||||
next: {
|
||||
// @ts-ignore
|
||||
internal: 1
|
||||
}
|
||||
});
|
||||
if ((_response_headers_get = response.headers.get('content-type')) == null ? void 0 : _response_headers_get.startsWith(RSC_CONTENT_TYPE_HEADER)) {
|
||||
// copy the headers from the redirect response to the response we're sending
|
||||
for (const [key, value] of response.headers){
|
||||
if (!actionsForbiddenHeaders.includes(key)) {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
return new FlightRenderResult(response.body);
|
||||
} else {
|
||||
var // Since we aren't consuming the response body, we cancel it to avoid memory leaks
|
||||
_response_body;
|
||||
(_response_body = response.body) == null ? void 0 : _response_body.cancel();
|
||||
}
|
||||
} catch (err) {
|
||||
// we couldn't stream the forwarded response, so we'll just return an empty response
|
||||
console.error(`failed to forward action response`, err);
|
||||
}
|
||||
return RenderResult.fromStatic('{}', JSON_CONTENT_TYPE_HEADER);
|
||||
}
|
||||
/**
|
||||
* Returns the parsed redirect URL if we deem that it is hosted by us.
|
||||
*
|
||||
* We handle both relative and absolute redirect URLs.
|
||||
*
|
||||
* In case the redirect URL is not relative to the application we return `null`.
|
||||
*/ function getAppRelativeRedirectUrl(basePath, host, redirectUrl, currentPathname) {
|
||||
if (redirectUrl.startsWith('/')) {
|
||||
// Absolute path - just add basePath
|
||||
return new URL(`${basePath}${redirectUrl}`, 'http://n');
|
||||
} else if (redirectUrl.startsWith('.')) {
|
||||
// Relative path - resolve relative to current pathname
|
||||
let base = currentPathname || '/';
|
||||
// Ensure the base path ends with a slash so relative resolution works correctly
|
||||
// e.g., "./subpage" from "/subdir" should resolve to "/subdir/subpage"
|
||||
// not "/subpage"
|
||||
if (!base.endsWith('/')) {
|
||||
base = base + '/';
|
||||
}
|
||||
const resolved = new URL(redirectUrl, `http://n${base}`);
|
||||
// Include basePath in the final URL
|
||||
return new URL(`${basePath}${resolved.pathname}${resolved.search}${resolved.hash}`, 'http://n');
|
||||
}
|
||||
const parsedRedirectUrl = new URL(redirectUrl);
|
||||
if ((host == null ? void 0 : host.value) !== parsedRedirectUrl.host) {
|
||||
return null;
|
||||
}
|
||||
// At this point the hosts are the same, just confirm we
|
||||
// are routing to a path underneath the `basePath`
|
||||
return parsedRedirectUrl.pathname.startsWith(basePath) ? parsedRedirectUrl : null;
|
||||
}
|
||||
async function createRedirectRenderResult(req, res, originalHost, redirectUrl, redirectType, basePath, workStore, currentPathname) {
|
||||
res.setHeader('x-action-redirect', `${redirectUrl};${redirectType}`);
|
||||
// If we're redirecting to another route of this Next.js application, we'll
|
||||
// try to stream the response from the other worker path. When that works,
|
||||
// we can save an extra roundtrip and avoid a full page reload.
|
||||
// When the redirect URL starts with a `/` or is to the same host, under the
|
||||
// `basePath` we treat it as an app-relative redirect;
|
||||
const appRelativeRedirectUrl = getAppRelativeRedirectUrl(basePath, originalHost, redirectUrl, currentPathname);
|
||||
if (appRelativeRedirectUrl) {
|
||||
var _getRequestMeta;
|
||||
if (!originalHost) {
|
||||
throw Object.defineProperty(new Error('Invariant: Missing `host` header from a forwarded Server Actions request.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E226",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const forwardedHeaders = getForwardedHeaders(req, res);
|
||||
forwardedHeaders.set(RSC_HEADER, '1');
|
||||
const proto = ((_getRequestMeta = getRequestMeta(req, 'initProtocol')) == null ? void 0 : _getRequestMeta.replace(/:+$/, '')) || 'https';
|
||||
// For standalone or the serverful mode, use the internal origin directly
|
||||
// other than the host headers from the request.
|
||||
const origin = process.env.__NEXT_PRIVATE_ORIGIN || `${proto}://${originalHost.value}`;
|
||||
const fetchUrl = new URL(`${origin}${appRelativeRedirectUrl.pathname}${appRelativeRedirectUrl.search}`);
|
||||
if (workStore.pendingRevalidatedTags) {
|
||||
var _workStore_incrementalCache_prerenderManifest_preview, _workStore_incrementalCache_prerenderManifest, _workStore_incrementalCache;
|
||||
forwardedHeaders.set(NEXT_CACHE_REVALIDATED_TAGS_HEADER, workStore.pendingRevalidatedTags.map((item)=>item.tag).join(','));
|
||||
forwardedHeaders.set(NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER, ((_workStore_incrementalCache = workStore.incrementalCache) == null ? void 0 : (_workStore_incrementalCache_prerenderManifest = _workStore_incrementalCache.prerenderManifest) == null ? void 0 : (_workStore_incrementalCache_prerenderManifest_preview = _workStore_incrementalCache_prerenderManifest.preview) == null ? void 0 : _workStore_incrementalCache_prerenderManifest_preview.previewModeId) || '');
|
||||
}
|
||||
// Ensures that when the path was revalidated we don't return a partial response on redirects
|
||||
forwardedHeaders.delete(NEXT_ROUTER_STATE_TREE_HEADER);
|
||||
// When an action follows a redirect, it's no longer handling an action: it's just a normal RSC request
|
||||
// to the requested URL. We should remove the `next-action` header so that it's not treated as an action
|
||||
forwardedHeaders.delete(ACTION_HEADER);
|
||||
try {
|
||||
var _response_headers_get;
|
||||
setCacheBustingSearchParam(fetchUrl, {
|
||||
[NEXT_ROUTER_PREFETCH_HEADER]: forwardedHeaders.get(NEXT_ROUTER_PREFETCH_HEADER) ? '1' : undefined,
|
||||
[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: forwardedHeaders.get(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER) ?? undefined,
|
||||
[NEXT_ROUTER_STATE_TREE_HEADER]: forwardedHeaders.get(NEXT_ROUTER_STATE_TREE_HEADER) ?? undefined,
|
||||
[NEXT_URL]: forwardedHeaders.get(NEXT_URL) ?? undefined
|
||||
});
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
headers: forwardedHeaders,
|
||||
next: {
|
||||
// @ts-ignore
|
||||
internal: 1
|
||||
}
|
||||
});
|
||||
if ((_response_headers_get = response.headers.get('content-type')) == null ? void 0 : _response_headers_get.startsWith(RSC_CONTENT_TYPE_HEADER)) {
|
||||
// copy the headers from the redirect response to the response we're sending
|
||||
for (const [key, value] of response.headers){
|
||||
if (!actionsForbiddenHeaders.includes(key)) {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
return new FlightRenderResult(response.body);
|
||||
} else {
|
||||
var // Since we aren't consuming the response body, we cancel it to avoid memory leaks
|
||||
_response_body;
|
||||
(_response_body = response.body) == null ? void 0 : _response_body.cancel();
|
||||
}
|
||||
} catch (err) {
|
||||
// we couldn't stream the redirect response, so we'll just do a normal redirect
|
||||
console.error(`failed to get redirect response`, err);
|
||||
}
|
||||
}
|
||||
return RenderResult.EMPTY;
|
||||
}
|
||||
/**
|
||||
* Ensures the value of the header can't create long logs.
|
||||
*/ function limitUntrustedHeaderValueForLogs(value) {
|
||||
return value.length > 100 ? value.slice(0, 100) + '...' : value;
|
||||
}
|
||||
export function parseHostHeader(headers, originDomain) {
|
||||
var _forwardedHostHeader_split_, _forwardedHostHeader_split;
|
||||
const forwardedHostHeader = headers['x-forwarded-host'];
|
||||
const forwardedHostHeaderValue = forwardedHostHeader && Array.isArray(forwardedHostHeader) ? forwardedHostHeader[0] : forwardedHostHeader == null ? void 0 : (_forwardedHostHeader_split = forwardedHostHeader.split(',')) == null ? void 0 : (_forwardedHostHeader_split_ = _forwardedHostHeader_split[0]) == null ? void 0 : _forwardedHostHeader_split_.trim();
|
||||
const hostHeader = headers['host'];
|
||||
if (originDomain) {
|
||||
return forwardedHostHeaderValue === originDomain ? {
|
||||
type: "x-forwarded-host",
|
||||
value: forwardedHostHeaderValue
|
||||
} : hostHeader === originDomain ? {
|
||||
type: "host",
|
||||
value: hostHeader
|
||||
} : undefined;
|
||||
}
|
||||
return forwardedHostHeaderValue ? {
|
||||
type: "x-forwarded-host",
|
||||
value: forwardedHostHeaderValue
|
||||
} : hostHeader ? {
|
||||
type: "host",
|
||||
value: hostHeader
|
||||
} : undefined;
|
||||
}
|
||||
export async function handleAction({ req, res, ComponentMod, generateFlight, workStore, requestStore, serverActions, ctx, metadata }) {
|
||||
const contentType = req.headers['content-type'];
|
||||
const { page } = ctx.renderOpts;
|
||||
const serverModuleMap = getServerModuleMap();
|
||||
const { actionId, isMultipartAction, isFetchAction, isURLEncodedAction, isPossibleServerAction } = getServerActionRequestMetadata(req);
|
||||
const handleUnrecognizedFetchAction = (err)=>{
|
||||
// If the deployment doesn't have skew protection, this is expected to occasionally happen,
|
||||
// so we use a warning instead of an error.
|
||||
console.warn(err);
|
||||
// Return an empty response with a header that the client router will interpret.
|
||||
// We don't need to waste time encoding a flight response, and using a blank body + header
|
||||
// means that unrecognized actions can also be handled at the infra level
|
||||
// (i.e. without needing to invoke a lambda)
|
||||
res.setHeader(NEXT_ACTION_NOT_FOUND_HEADER, '1');
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.statusCode = 404;
|
||||
return {
|
||||
type: 'done',
|
||||
result: RenderResult.fromStatic('Server action not found.', 'text/plain')
|
||||
};
|
||||
};
|
||||
// If it can't be a Server Action, skip handling.
|
||||
// Note that this can be a false positive -- any multipart/urlencoded POST can get us here,
|
||||
// But won't know if it's an MPA action or not until we call `decodeAction` below.
|
||||
if (!isPossibleServerAction) {
|
||||
return null;
|
||||
}
|
||||
// We don't currently support URL encoded actions, so we bail out early.
|
||||
// Depending on if it's a fetch action or an MPA, we return a different response.
|
||||
if (isURLEncodedAction) {
|
||||
if (isFetchAction) {
|
||||
return {
|
||||
type: 'not-found'
|
||||
};
|
||||
} else {
|
||||
// This is an MPA action, so we return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// If the app has no server actions at all, we can 404 early.
|
||||
if (!hasServerActions()) {
|
||||
return handleUnrecognizedFetchAction(getActionNotFoundError(actionId));
|
||||
}
|
||||
if (workStore.isStaticGeneration) {
|
||||
throw Object.defineProperty(new Error("Invariant: server actions can't be handled during static rendering"), "__NEXT_ERROR_CODE", {
|
||||
value: "E359",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
let temporaryReferences;
|
||||
// When running actions the default is no-store, you can still `cache: 'force-cache'`
|
||||
workStore.fetchCache = 'default-no-store';
|
||||
const originHeader = req.headers['origin'];
|
||||
const originHost = typeof originHeader === 'string' ? // However, these contexts can still send along credentials like cookies,
|
||||
// so we need to check if they're allowed cross-origin requests.
|
||||
originHeader === 'null' ? 'null' : new URL(originHeader).host : undefined;
|
||||
const host = parseHostHeader(req.headers);
|
||||
let warning = undefined;
|
||||
function warnBadServerActionRequest() {
|
||||
if (warning) {
|
||||
warn(warning);
|
||||
}
|
||||
}
|
||||
// This is to prevent CSRF attacks. If `x-forwarded-host` is set, we need to
|
||||
// ensure that the request is coming from the same host.
|
||||
if (!originHost) {
|
||||
// This is a handcrafted request without an origin or a request from an unsafe browser.
|
||||
// We'll let this through but log a warning.
|
||||
// We can't guard against unsafe browsers and handcrafted requests can't contain
|
||||
// user credentials that haven't been shared willingly.
|
||||
warning = 'Missing `origin` header from a forwarded Server Actions request.';
|
||||
} else if (!host || originHost !== host.value) {
|
||||
// If the customer sets a list of allowed origins, we'll allow the request.
|
||||
// These are considered safe but might be different from forwarded host set
|
||||
// by the infra (i.e. reverse proxies).
|
||||
if (isCsrfOriginAllowed(originHost, serverActions == null ? void 0 : serverActions.allowedOrigins)) {
|
||||
// Ignore it
|
||||
} else {
|
||||
if (host) {
|
||||
// This seems to be an CSRF attack. We should not proceed the action.
|
||||
console.error(`\`${host.type}\` header with value \`${limitUntrustedHeaderValueForLogs(host.value)}\` does not match \`origin\` header with value \`${limitUntrustedHeaderValueForLogs(originHost)}\` from a forwarded Server Actions request. Aborting the action.`);
|
||||
} else {
|
||||
// This is an attack. We should not proceed the action.
|
||||
console.error(`\`x-forwarded-host\` or \`host\` headers are not provided. One of these is needed to compare the \`origin\` header from a forwarded Server Actions request. Aborting the action.`);
|
||||
}
|
||||
const error = Object.defineProperty(new Error('Invalid Server Actions request.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E80",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
if (isFetchAction) {
|
||||
res.statusCode = 500;
|
||||
metadata.statusCode = 500;
|
||||
const promise = Promise.reject(error);
|
||||
try {
|
||||
// we need to await the promise to trigger the rejection early
|
||||
// so that it's already handled by the time we call
|
||||
// the RSC runtime. Otherwise, it will throw an unhandled
|
||||
// promise rejection error in the renderer.
|
||||
await promise;
|
||||
} catch {
|
||||
// swallow error, it's gonna be handled on the client
|
||||
}
|
||||
return {
|
||||
type: 'done',
|
||||
result: await generateFlight(req, ctx, requestStore, {
|
||||
actionResult: promise,
|
||||
// We didn't execute an action, so no revalidations could have
|
||||
// occurred. We can skip rendering the page.
|
||||
skipPageRendering: true,
|
||||
temporaryReferences
|
||||
})
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ensure we avoid caching server actions unexpectedly
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate');
|
||||
const { actionAsyncStorage } = ComponentMod;
|
||||
const actionWasForwarded = Boolean(req.headers['x-action-forwarded']);
|
||||
if (actionId) {
|
||||
const forwardedWorker = selectWorkerForForwarding(actionId, page);
|
||||
// If forwardedWorker is truthy, it means there isn't a worker for the action
|
||||
// in the current handler, so we forward the request to a worker that has the action.
|
||||
if (forwardedWorker) {
|
||||
return {
|
||||
type: 'done',
|
||||
result: await createForwardedActionResponse(req, res, host, forwardedWorker, ctx.renderOpts.basePath)
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await actionAsyncStorage.run({
|
||||
isAction: true
|
||||
}, async ()=>{
|
||||
// We only use these for fetch actions -- MPA actions handle them inside `decodeAction`.
|
||||
let actionModId;
|
||||
let boundActionArguments = [];
|
||||
if (// The type check here ensures that `req` is correctly typed, and the
|
||||
// environment variable check provides dead code elimination.
|
||||
process.env.NEXT_RUNTIME === 'edge' && isWebNextRequest(req)) {
|
||||
if (!req.body) {
|
||||
throw Object.defineProperty(new Error('invariant: Missing request body.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E364",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// TODO: add body limit
|
||||
// Use react-server-dom-webpack/server
|
||||
const { createTemporaryReferenceSet, decodeReply, decodeAction, decodeFormState } = ComponentMod;
|
||||
temporaryReferences = createTemporaryReferenceSet();
|
||||
if (isMultipartAction) {
|
||||
// TODO-APP: Add streaming support
|
||||
const formData = await req.request.formData();
|
||||
if (isFetchAction) {
|
||||
// A fetch action with a multipart body.
|
||||
try {
|
||||
actionModId = getActionModIdOrError(actionId, serverModuleMap);
|
||||
} catch (err) {
|
||||
return handleUnrecognizedFetchAction(err);
|
||||
}
|
||||
boundActionArguments = await decodeReply(formData, serverModuleMap, {
|
||||
temporaryReferences
|
||||
});
|
||||
} else {
|
||||
// Multipart POST, but not a fetch action.
|
||||
// Potentially an MPA action, we have to try decoding it to check.
|
||||
if (areAllActionIdsValid(formData, serverModuleMap) === false) {
|
||||
// TODO: This can be from skew or manipulated input. We should handle this case
|
||||
// more gracefully but this preserves the prior behavior where decodeAction would throw instead.
|
||||
throw Object.defineProperty(new Error(`Failed to find Server Action. This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", {
|
||||
value: "E975",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const action = await decodeAction(formData, serverModuleMap);
|
||||
if (typeof action === 'function') {
|
||||
// an MPA action.
|
||||
// Only warn if it's a server action, otherwise skip for other post requests
|
||||
warnBadServerActionRequest();
|
||||
const { actionResult } = await executeActionAndPrepareForRender(action, [], workStore, requestStore, actionWasForwarded);
|
||||
const formState = await decodeFormState(actionResult, formData, serverModuleMap);
|
||||
// Skip the fetch path.
|
||||
// We need to render a full HTML version of the page for the response, we'll handle that in app-render.
|
||||
return {
|
||||
type: 'done',
|
||||
result: undefined,
|
||||
formState
|
||||
};
|
||||
} else {
|
||||
// We couldn't decode an action, so this POST request turned out not to be a server action request.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// POST with non-multipart body.
|
||||
// If it's not multipart AND not a fetch action,
|
||||
// then it can't be an action request.
|
||||
if (!isFetchAction) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
actionModId = getActionModIdOrError(actionId, serverModuleMap);
|
||||
} catch (err) {
|
||||
return handleUnrecognizedFetchAction(err);
|
||||
}
|
||||
// A fetch action with a non-multipart body.
|
||||
// In practice, this happens if `encodeReply` returned a string instead of FormData,
|
||||
// which can happen for very simple JSON-like values that don't need multiple flight rows.
|
||||
const chunks = [];
|
||||
const reader = req.body.getReader();
|
||||
while(true){
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
const actionData = Buffer.concat(chunks).toString('utf-8');
|
||||
boundActionArguments = await decodeReply(actionData, serverModuleMap, {
|
||||
temporaryReferences
|
||||
});
|
||||
}
|
||||
} else if (// The type check here ensures that `req` is correctly typed, and the
|
||||
// environment variable check provides dead code elimination.
|
||||
process.env.NEXT_RUNTIME !== 'edge' && isNodeNextRequest(req)) {
|
||||
// Use react-server-dom-webpack/server.node which supports streaming
|
||||
const { createTemporaryReferenceSet, decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState } = require(`./react-server.node`);
|
||||
temporaryReferences = createTemporaryReferenceSet();
|
||||
const { PassThrough, Readable, Transform } = require('node:stream');
|
||||
const { pipeline } = require('node:stream/promises');
|
||||
// If actionBody was stashed in request meta (from parsing the postponed
|
||||
// state prefix in minimal mode), use it instead of req.body
|
||||
const actionBodyFromMeta = getRequestMeta(req, 'actionBody');
|
||||
const body = actionBodyFromMeta ? Readable.from(actionBodyFromMeta) : req.body;
|
||||
const defaultBodySizeLimit = '1 MB';
|
||||
const bodySizeLimit = (serverActions == null ? void 0 : serverActions.bodySizeLimit) ?? defaultBodySizeLimit;
|
||||
const bodySizeLimitBytes = bodySizeLimit !== defaultBodySizeLimit ? require('next/dist/compiled/bytes').parse(bodySizeLimit) : 1024 * 1024 // 1 MB
|
||||
;
|
||||
let size = 0;
|
||||
const sizeLimitTransform = new Transform({
|
||||
transform (chunk, encoding, callback) {
|
||||
size += Buffer.byteLength(chunk, encoding);
|
||||
if (size > bodySizeLimitBytes) {
|
||||
const { ApiError } = require('../api-utils');
|
||||
callback(Object.defineProperty(new ApiError(413, `Body exceeded ${bodySizeLimit} limit.\n` + `To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit`), "__NEXT_ERROR_CODE", {
|
||||
value: "E394",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
callback(null, chunk);
|
||||
}
|
||||
});
|
||||
if (isMultipartAction) {
|
||||
if (isFetchAction) {
|
||||
// A fetch action with a multipart body.
|
||||
try {
|
||||
actionModId = getActionModIdOrError(actionId, serverModuleMap);
|
||||
} catch (err) {
|
||||
return handleUnrecognizedFetchAction(err);
|
||||
}
|
||||
const busboy = require('next/dist/compiled/busboy')({
|
||||
defParamCharset: 'utf8',
|
||||
headers: req.headers,
|
||||
limits: {
|
||||
fieldSize: bodySizeLimitBytes
|
||||
}
|
||||
});
|
||||
const abortController = new AbortController();
|
||||
try {
|
||||
;
|
||||
[, boundActionArguments] = await Promise.all([
|
||||
pipeline(body, sizeLimitTransform, busboy, {
|
||||
signal: abortController.signal
|
||||
}),
|
||||
decodeReplyFromBusboy(busboy, serverModuleMap, {
|
||||
temporaryReferences
|
||||
})
|
||||
]);
|
||||
} catch (err) {
|
||||
abortController.abort();
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
// Multipart POST, but not a fetch action.
|
||||
// Potentially an MPA action, we have to try decoding it to check.
|
||||
const sizeLimitedBody = new PassThrough();
|
||||
// React doesn't yet publish a busboy version of decodeAction
|
||||
// so we polyfill the parsing of FormData.
|
||||
const fakeRequest = new Request('http://localhost', {
|
||||
method: 'POST',
|
||||
// @ts-expect-error
|
||||
headers: {
|
||||
'Content-Type': contentType
|
||||
},
|
||||
body: Readable.toWeb(sizeLimitedBody),
|
||||
duplex: 'half'
|
||||
});
|
||||
let formData;
|
||||
const abortController = new AbortController();
|
||||
try {
|
||||
;
|
||||
[, formData] = await Promise.all([
|
||||
pipeline(body, sizeLimitTransform, sizeLimitedBody, {
|
||||
signal: abortController.signal
|
||||
}),
|
||||
fakeRequest.formData()
|
||||
]);
|
||||
} catch (err) {
|
||||
abortController.abort();
|
||||
throw err;
|
||||
}
|
||||
if (areAllActionIdsValid(formData, serverModuleMap) === false) {
|
||||
// TODO: This can be from skew or manipulated input. We should handle this case
|
||||
// more gracefully but this preserves the prior behavior where decodeAction would throw instead.
|
||||
throw Object.defineProperty(new Error(`Failed to find Server Action. This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", {
|
||||
value: "E975",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// TODO: Refactor so it is harder to accidentally decode an action before you have validated that the
|
||||
// action referred to is available.
|
||||
const action = await decodeAction(formData, serverModuleMap);
|
||||
if (typeof action === 'function') {
|
||||
// an MPA action.
|
||||
// Only warn if it's a server action, otherwise skip for other post requests
|
||||
warnBadServerActionRequest();
|
||||
const { actionResult } = await executeActionAndPrepareForRender(action, [], workStore, requestStore, actionWasForwarded);
|
||||
const formState = await decodeFormState(actionResult, formData, serverModuleMap);
|
||||
// Skip the fetch path.
|
||||
// We need to render a full HTML version of the page for the response, we'll handle that in app-render.
|
||||
return {
|
||||
type: 'done',
|
||||
result: undefined,
|
||||
formState
|
||||
};
|
||||
} else {
|
||||
// We couldn't decode an action, so this POST request turned out not to be a server action request.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// POST with non-multipart body.
|
||||
// If it's not multipart AND not a fetch action,
|
||||
// then it can't be an action request.
|
||||
if (!isFetchAction) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
actionModId = getActionModIdOrError(actionId, serverModuleMap);
|
||||
} catch (err) {
|
||||
return handleUnrecognizedFetchAction(err);
|
||||
}
|
||||
// A fetch action with a non-multipart body.
|
||||
// In practice, this happens if `encodeReply` returned a string instead of FormData,
|
||||
// which can happen for very simple JSON-like values that don't need multiple flight rows.
|
||||
const sizeLimitedBody = new PassThrough();
|
||||
const chunks = [];
|
||||
await Promise.all([
|
||||
pipeline(body, sizeLimitTransform, sizeLimitedBody),
|
||||
(async ()=>{
|
||||
for await (const chunk of sizeLimitedBody){
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
})()
|
||||
]);
|
||||
const actionData = Buffer.concat(chunks).toString('utf-8');
|
||||
boundActionArguments = await decodeReply(actionData, serverModuleMap, {
|
||||
temporaryReferences
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw Object.defineProperty(new Error('Invariant: Unknown request type.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E114",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// actions.js
|
||||
// app/page.js
|
||||
// action worker1
|
||||
// appRender1
|
||||
// app/foo/page.js
|
||||
// action worker2
|
||||
// appRender
|
||||
// / -> fire action -> POST / -> appRender1 -> modId for the action file
|
||||
// /foo -> fire action -> POST /foo -> appRender2 -> modId for the action file
|
||||
const actionMod = await ComponentMod.__next_app__.require(actionModId);
|
||||
const actionHandler = actionMod[// `actionId` must exist if we got here, as otherwise we would have thrown an error above
|
||||
actionId];
|
||||
// Log server action call in development when enabled
|
||||
let logInfo = null;
|
||||
const { type: actionType } = extractInfoFromServerReferenceId(actionId);
|
||||
if (process.env.NODE_ENV === 'development' && ctx.renderOpts.logServerFunctions && // TODO: For now, skip logging for 'use cache' Server Functions as the
|
||||
// output needs more work, or a different approach entirely.
|
||||
actionType !== 'use-cache') {
|
||||
var _serverActionsManifest_runtime;
|
||||
const serverActionsManifest = getServerActionsManifest();
|
||||
const runtime = process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node';
|
||||
const actionInfo = (_serverActionsManifest_runtime = serverActionsManifest[runtime]) == null ? void 0 : _serverActionsManifest_runtime[actionId];
|
||||
if (actionInfo) {
|
||||
var _actionInfo_exportedName;
|
||||
const isInlineAction = (_actionInfo_exportedName = actionInfo.exportedName) == null ? void 0 : _actionInfo_exportedName.startsWith(INLINE_ACTION_PREFIX);
|
||||
const projectDir = ctx.renderOpts.dir || (process.env.NEXT_RUNTIME === 'edge' ? '' : process.cwd());
|
||||
const location = normalizeFilePath(projectDir, actionInfo.filename);
|
||||
// Format function name for display
|
||||
let functionName;
|
||||
if (isInlineAction) {
|
||||
functionName = '<inline action>';
|
||||
} else if (actionInfo.exportedName === 'default') {
|
||||
functionName = 'default';
|
||||
} else {
|
||||
functionName = actionInfo.exportedName || '<action>';
|
||||
}
|
||||
logInfo = {
|
||||
functionName,
|
||||
args: boundActionArguments,
|
||||
location
|
||||
};
|
||||
}
|
||||
}
|
||||
const startTime = performance.now();
|
||||
const { actionResult, skipPageRendering } = await executeActionAndPrepareForRender(actionHandler, boundActionArguments, workStore, requestStore, actionWasForwarded).finally(()=>{
|
||||
addRevalidationHeader(res, {
|
||||
workStore,
|
||||
requestStore
|
||||
});
|
||||
if (logInfo) {
|
||||
// Store server action log info to be logged after the request log
|
||||
const duration = Math.round(performance.now() - startTime);
|
||||
addRequestMeta(req, 'devServerActionLog', {
|
||||
functionName: logInfo.functionName,
|
||||
args: logInfo.args,
|
||||
location: logInfo.location,
|
||||
duration
|
||||
});
|
||||
}
|
||||
});
|
||||
// For form actions, we need to continue rendering the page.
|
||||
if (isFetchAction) {
|
||||
// If we skip page rendering, we need to ensure pending revalidates
|
||||
// are awaited before closing the response. Otherwise, this will be
|
||||
// done after rendering the page.
|
||||
const maybeRevalidatesPromise = skipPageRendering ? executeRevalidates(workStore) : false;
|
||||
return {
|
||||
type: 'done',
|
||||
result: await generateFlight(req, ctx, requestStore, {
|
||||
actionResult: Promise.resolve(actionResult),
|
||||
skipPageRendering,
|
||||
temporaryReferences,
|
||||
waitUntil: maybeRevalidatesPromise === false ? undefined : maybeRevalidatesPromise
|
||||
})
|
||||
};
|
||||
} else {
|
||||
// TODO: this shouldn't be reachable, because all non-fetch codepaths return early.
|
||||
// this will be handled in a follow-up refactor PR.
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
if (isRedirectError(err)) {
|
||||
const redirectUrl = getURLFromRedirectError(err);
|
||||
const redirectType = getRedirectTypeFromError(err);
|
||||
// if it's a fetch action, we'll set the status code for logging/debugging purposes
|
||||
// but we won't set a Location header, as the redirect will be handled by the client router
|
||||
res.statusCode = RedirectStatusCode.SeeOther;
|
||||
metadata.statusCode = RedirectStatusCode.SeeOther;
|
||||
if (isFetchAction) {
|
||||
return {
|
||||
type: 'done',
|
||||
result: await createRedirectRenderResult(req, res, host, redirectUrl, redirectType, ctx.renderOpts.basePath, workStore, requestStore.url.pathname)
|
||||
};
|
||||
}
|
||||
// For an MPA action, the redirect doesn't need a body, just a Location header.
|
||||
res.setHeader('Location', redirectUrl);
|
||||
return {
|
||||
type: 'done',
|
||||
result: RenderResult.EMPTY
|
||||
};
|
||||
} else if (isHTTPAccessFallbackError(err)) {
|
||||
res.statusCode = getAccessFallbackHTTPStatus(err);
|
||||
metadata.statusCode = res.statusCode;
|
||||
if (isFetchAction) {
|
||||
const promise = Promise.reject(err);
|
||||
try {
|
||||
// we need to await the promise to trigger the rejection early
|
||||
// so that it's already handled by the time we call
|
||||
// the RSC runtime. Otherwise, it will throw an unhandled
|
||||
// promise rejection error in the renderer.
|
||||
await promise;
|
||||
} catch {
|
||||
// swallow error, it's gonna be handled on the client
|
||||
}
|
||||
return {
|
||||
type: 'done',
|
||||
result: await generateFlight(req, ctx, requestStore, {
|
||||
skipPageRendering: false,
|
||||
actionResult: promise,
|
||||
temporaryReferences
|
||||
})
|
||||
};
|
||||
}
|
||||
// For an MPA action, we need to render a HTML response. We'll handle that in app-render.
|
||||
return {
|
||||
type: 'not-found'
|
||||
};
|
||||
}
|
||||
// An error that didn't come from `redirect()` or `notFound()`, likely thrown from user code
|
||||
// (but it could also be a bug in our code!)
|
||||
if (isFetchAction) {
|
||||
// TODO: consider checking if the error is an `ApiError` and change status code
|
||||
// so that we can respond with a 413 to requests that break the body size limit
|
||||
// (but if we do that, we also need to make sure that whatever handles the non-fetch error path below does the same)
|
||||
res.statusCode = 500;
|
||||
metadata.statusCode = 500;
|
||||
const promise = Promise.reject(err);
|
||||
try {
|
||||
// we need to await the promise to trigger the rejection early
|
||||
// so that it's already handled by the time we call
|
||||
// the RSC runtime. Otherwise, it will throw an unhandled
|
||||
// promise rejection error in the renderer.
|
||||
await promise;
|
||||
} catch {
|
||||
// swallow error, it's gonna be handled on the client
|
||||
}
|
||||
return {
|
||||
type: 'done',
|
||||
result: await generateFlight(req, ctx, requestStore, {
|
||||
actionResult: promise,
|
||||
// If the page was not revalidated, or if the action was forwarded
|
||||
// from another worker, we can skip rendering the page.
|
||||
skipPageRendering: workStore.pathWasRevalidated === undefined || workStore.pathWasRevalidated === ActionDidNotRevalidate || actionWasForwarded,
|
||||
temporaryReferences
|
||||
})
|
||||
};
|
||||
}
|
||||
// For an MPA action, we need to render a HTML response. We'll rethrow the error and let it be handled above.
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Limit on the number of arguments passed to a server action. This prevents
|
||||
* stack overflow during `action.apply()` from malicious requests.
|
||||
*/ const SERVER_ACTION_ARGS_LIMIT = 1000;
|
||||
async function executeActionAndPrepareForRender(action, args, workStore, requestStore, actionWasForwarded) {
|
||||
requestStore.phase = 'action';
|
||||
let skipPageRendering = actionWasForwarded;
|
||||
if (args.length > SERVER_ACTION_ARGS_LIMIT) {
|
||||
throw Object.defineProperty(new Error(`Server Action arguments list is too long (${args.length}). Maximum allowed is ${SERVER_ACTION_ARGS_LIMIT}.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E986",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
try {
|
||||
const actionResult = await workUnitAsyncStorage.run(requestStore, ()=>action.apply(null, args));
|
||||
// If the page was not revalidated, or if the action was forwarded from
|
||||
// another worker, we can skip rendering the page.
|
||||
skipPageRendering ||= workStore.pathWasRevalidated === undefined || workStore.pathWasRevalidated === ActionDidNotRevalidate;
|
||||
return {
|
||||
actionResult,
|
||||
skipPageRendering
|
||||
};
|
||||
} finally{
|
||||
if (!skipPageRendering) {
|
||||
requestStore.phase = 'render';
|
||||
// When we switch to the render phase, cookies() will return
|
||||
// `workUnitStore.cookies` instead of
|
||||
// `workUnitStore.userspaceMutableCookies`. We want the render to see any
|
||||
// cookie writes that we performed during the action, so we need to update
|
||||
// the immutable cookies to reflect the changes.
|
||||
synchronizeMutableCookies(requestStore);
|
||||
// The server action might have toggled draft mode, so we need to reflect
|
||||
// that in the work store to be up-to-date for subsequent rendering.
|
||||
workStore.isDraftMode = requestStore.draftMode.isEnabled;
|
||||
// If the action called revalidateTag/revalidatePath, then that might
|
||||
// affect data used by the subsequent render, so we need to make sure all
|
||||
// revalidations are applied before that.
|
||||
await executeRevalidates(workStore);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Attempts to find the module ID for the action from the module map. When this fails, it could be a deployment skew where
|
||||
* the action came from a different deployment. It could also simply be an invalid POST request that is not a server action.
|
||||
* In either case, we'll throw an error to be handled by the caller.
|
||||
*/ function getActionModIdOrError(actionId, serverModuleMap) {
|
||||
var _serverModuleMap_actionId;
|
||||
// if we're missing the action ID header, we can't do any further processing
|
||||
if (!actionId) {
|
||||
throw Object.defineProperty(new InvariantError("Missing 'next-action' header."), "__NEXT_ERROR_CODE", {
|
||||
value: "E664",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const actionModId = (_serverModuleMap_actionId = serverModuleMap[actionId]) == null ? void 0 : _serverModuleMap_actionId.id;
|
||||
if (!actionModId) {
|
||||
throw getActionNotFoundError(actionId);
|
||||
}
|
||||
return actionModId;
|
||||
}
|
||||
function getActionNotFoundError(actionId) {
|
||||
return Object.defineProperty(new Error(`Failed to find Server Action${actionId ? ` "${actionId}"` : ''}. This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", {
|
||||
value: "E974",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const $ACTION_ = '$ACTION_';
|
||||
const $ACTION_REF_ = '$ACTION_REF_';
|
||||
const $ACTION_ID_ = '$ACTION_ID_';
|
||||
const ACTION_ID_EXPECTED_LENGTH = 42;
|
||||
/**
|
||||
* This function mirrors logic inside React's decodeAction and should be kept in sync with that.
|
||||
* It pre-parses the FormData to ensure that any action IDs referred to are actual action IDs for
|
||||
* this Next.js application.
|
||||
*/ function areAllActionIdsValid(mpaFormData, serverModuleMap) {
|
||||
let hasAtLeastOneAction = false;
|
||||
// Before we attempt to decode the payload for a possible MPA action, assert that all
|
||||
// action IDs are valid IDs. If not we should disregard the payload
|
||||
for (let key of mpaFormData.keys()){
|
||||
if (!key.startsWith($ACTION_)) {
|
||||
continue;
|
||||
}
|
||||
if (key.startsWith($ACTION_ID_)) {
|
||||
// No Bound args case
|
||||
if (isInvalidActionIdFieldName(key, serverModuleMap)) {
|
||||
return false;
|
||||
}
|
||||
hasAtLeastOneAction = true;
|
||||
} else if (key.startsWith($ACTION_REF_)) {
|
||||
// Bound args case
|
||||
const actionDescriptorField = $ACTION_ + key.slice($ACTION_REF_.length) + ':0';
|
||||
const actionFields = mpaFormData.getAll(actionDescriptorField);
|
||||
if (actionFields.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
const actionField = actionFields[0];
|
||||
if (typeof actionField !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (isInvalidStringActionDescriptor(actionField, serverModuleMap)) {
|
||||
return false;
|
||||
}
|
||||
hasAtLeastOneAction = true;
|
||||
}
|
||||
}
|
||||
return hasAtLeastOneAction;
|
||||
}
|
||||
const ACTION_DESCRIPTOR_ID_PREFIX = '{"id":"';
|
||||
function isInvalidStringActionDescriptor(actionDescriptor, serverModuleMap) {
|
||||
if (actionDescriptor.startsWith(ACTION_DESCRIPTOR_ID_PREFIX) === false) {
|
||||
return true;
|
||||
}
|
||||
const from = ACTION_DESCRIPTOR_ID_PREFIX.length;
|
||||
const to = from + ACTION_ID_EXPECTED_LENGTH;
|
||||
// We expect actionDescriptor to be '{"id":"<actionId>",...}'
|
||||
const actionId = actionDescriptor.slice(from, to);
|
||||
if (actionId.length !== ACTION_ID_EXPECTED_LENGTH || actionDescriptor[to] !== '"') {
|
||||
return true;
|
||||
}
|
||||
const entry = serverModuleMap[actionId];
|
||||
if (entry == null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isInvalidActionIdFieldName(actionIdFieldName, serverModuleMap) {
|
||||
// The field name must always start with $ACTION_ID_ but since it is
|
||||
// the id is extracted from the key of the field we have already validated
|
||||
// this before entering this function
|
||||
if (actionIdFieldName.length !== $ACTION_ID_.length + ACTION_ID_EXPECTED_LENGTH) {
|
||||
// this field name has too few or too many characters
|
||||
return true;
|
||||
}
|
||||
const actionId = actionIdFieldName.slice($ACTION_ID_.length);
|
||||
const entry = serverModuleMap[actionId];
|
||||
if (entry == null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=action-handler.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { createAsyncLocalStorage } from './async-local-storage';
|
||||
export const afterTaskAsyncStorageInstance = createAsyncLocalStorage();
|
||||
|
||||
//# sourceMappingURL=after-task-async-storage-instance.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/after-task-async-storage-instance.ts"],"sourcesContent":["import type { AfterTaskAsyncStorage } from './after-task-async-storage.external'\nimport { createAsyncLocalStorage } from './async-local-storage'\n\nexport const afterTaskAsyncStorageInstance: AfterTaskAsyncStorage =\n createAsyncLocalStorage()\n"],"names":["createAsyncLocalStorage","afterTaskAsyncStorageInstance"],"mappings":"AACA,SAASA,uBAAuB,QAAQ,wBAAuB;AAE/D,OAAO,MAAMC,gCACXD,0BAAyB","ignoreList":[0]}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
// Share the instance module in the next-shared layer
|
||||
import { afterTaskAsyncStorageInstance as afterTaskAsyncStorage } from './after-task-async-storage-instance' with {
|
||||
'turbopack-transition': 'next-shared'
|
||||
};
|
||||
export { afterTaskAsyncStorage };
|
||||
|
||||
//# sourceMappingURL=after-task-async-storage.external.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/after-task-async-storage.external.ts"],"sourcesContent":["import type { AsyncLocalStorage } from 'async_hooks'\n\n// Share the instance module in the next-shared layer\nimport { afterTaskAsyncStorageInstance as afterTaskAsyncStorage } from './after-task-async-storage-instance' with { 'turbopack-transition': 'next-shared' }\nimport type { WorkUnitStore } from './work-unit-async-storage.external'\n\nexport interface AfterTaskStore {\n /** The phase in which the topmost `after` was called.\n *\n * NOTE: Can be undefined when running `generateStaticParams`,\n * where we only have a `workStore`, no `workUnitStore`.\n */\n readonly rootTaskSpawnPhase: WorkUnitStore['phase'] | undefined\n}\n\nexport type AfterTaskAsyncStorage = AsyncLocalStorage<AfterTaskStore>\n\nexport { afterTaskAsyncStorage }\n"],"names":["afterTaskAsyncStorageInstance","afterTaskAsyncStorage"],"mappings":"AAEA,qDAAqD;AACrD,SAASA,iCAAiCC,qBAAqB,QAAQ,2CAA2C;IAAE,wBAAwB;AAAc,EAAC;AAc3J,SAASA,qBAAqB,GAAE","ignoreList":[0]}
|
||||
Generated
Vendored
+137
@@ -0,0 +1,137 @@
|
||||
import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
// React's RSC prerender function will emit an incomplete flight stream when using `prerender`. If the connection
|
||||
// closes then whatever hanging chunks exist will be errored. This is because prerender (an experimental feature)
|
||||
// has not yet implemented a concept of resume. For now we will simulate a paused connection by wrapping the stream
|
||||
// in one that doesn't close even when the underlying is complete.
|
||||
export class ReactServerResult {
|
||||
constructor(stream){
|
||||
this._stream = stream;
|
||||
}
|
||||
tee() {
|
||||
if (this._stream === null) {
|
||||
throw Object.defineProperty(new Error('Cannot tee a ReactServerResult that has already been consumed'), "__NEXT_ERROR_CODE", {
|
||||
value: "E106",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const tee = this._stream.tee();
|
||||
this._stream = tee[0];
|
||||
return tee[1];
|
||||
}
|
||||
consume() {
|
||||
if (this._stream === null) {
|
||||
throw Object.defineProperty(new Error('Cannot consume a ReactServerResult that has already been consumed'), "__NEXT_ERROR_CODE", {
|
||||
value: "E470",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const stream = this._stream;
|
||||
this._stream = null;
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
export async function createReactServerPrerenderResult(underlying) {
|
||||
const chunks = [];
|
||||
const { prelude } = await underlying;
|
||||
const reader = prelude.getReader();
|
||||
while(true){
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
return new ReactServerPrerenderResult(chunks);
|
||||
} else {
|
||||
chunks.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function createReactServerPrerenderResultFromRender(underlying) {
|
||||
const chunks = [];
|
||||
const reader = underlying.getReader();
|
||||
while(true){
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
} else {
|
||||
chunks.push(value);
|
||||
}
|
||||
}
|
||||
return new ReactServerPrerenderResult(chunks);
|
||||
}
|
||||
export class ReactServerPrerenderResult {
|
||||
assertChunks(expression) {
|
||||
if (this._chunks === null) {
|
||||
throw Object.defineProperty(new InvariantError(`Cannot \`${expression}\` on a ReactServerPrerenderResult that has already been consumed.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E593",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return this._chunks;
|
||||
}
|
||||
consumeChunks(expression) {
|
||||
const chunks = this.assertChunks(expression);
|
||||
this.consume();
|
||||
return chunks;
|
||||
}
|
||||
consume() {
|
||||
this._chunks = null;
|
||||
}
|
||||
constructor(chunks){
|
||||
this._chunks = chunks;
|
||||
}
|
||||
asUnclosingStream() {
|
||||
const chunks = this.assertChunks('asUnclosingStream()');
|
||||
return createUnclosingStream(chunks);
|
||||
}
|
||||
consumeAsUnclosingStream() {
|
||||
const chunks = this.consumeChunks('consumeAsUnclosingStream()');
|
||||
return createUnclosingStream(chunks);
|
||||
}
|
||||
asStream() {
|
||||
const chunks = this.assertChunks('asStream()');
|
||||
return createClosingStream(chunks);
|
||||
}
|
||||
consumeAsStream() {
|
||||
const chunks = this.consumeChunks('consumeAsStream()');
|
||||
return createClosingStream(chunks);
|
||||
}
|
||||
}
|
||||
function createUnclosingStream(chunks) {
|
||||
let i = 0;
|
||||
return new ReadableStream({
|
||||
async pull (controller) {
|
||||
if (i < chunks.length) {
|
||||
controller.enqueue(chunks[i++]);
|
||||
}
|
||||
// we intentionally keep the stream open. The consumer will clear
|
||||
// out chunks once finished and the remaining memory will be GC'd
|
||||
// when this object goes out of scope
|
||||
}
|
||||
});
|
||||
}
|
||||
function createClosingStream(chunks) {
|
||||
let i = 0;
|
||||
return new ReadableStream({
|
||||
async pull (controller) {
|
||||
if (i < chunks.length) {
|
||||
controller.enqueue(chunks[i++]);
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
export async function processPrelude(unprocessedPrelude) {
|
||||
const [prelude, peek] = unprocessedPrelude.tee();
|
||||
const reader = peek.getReader();
|
||||
const firstResult = await reader.read();
|
||||
reader.cancel();
|
||||
const preludeIsEmpty = firstResult.done === true;
|
||||
return {
|
||||
prelude,
|
||||
preludeIsEmpty
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=app-render-prerender-utils.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+72
@@ -0,0 +1,72 @@
|
||||
import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
import { createAtomicTimerGroup } from './app-render-scheduling';
|
||||
import { DANGEROUSLY_runPendingImmediatesAfterCurrentTask, expectNoPendingImmediates } from '../node-environment-extensions/fast-set-immediate.external';
|
||||
import { isThenable } from '../../shared/lib/is-thenable';
|
||||
function noop() {}
|
||||
/**
|
||||
* This is a utility function to make scheduling sequential tasks that run back to back easier.
|
||||
* We schedule on the same queue (setTimeout) at the same time to ensure no other events can sneak in between.
|
||||
*
|
||||
* The first function runs in the first task. Each subsequent function runs in its own task.
|
||||
* The returned promise resolves after the last task completes.
|
||||
*/ export function runInSequentialTasks(first, ...rest) {
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
throw Object.defineProperty(new InvariantError('`runInSequentialTasks` should not be called in edge runtime.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1054",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
} else {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const scheduleTimeout = createAtomicTimerGroup();
|
||||
const ids = [];
|
||||
let result;
|
||||
ids.push(scheduleTimeout(()=>{
|
||||
try {
|
||||
DANGEROUSLY_runPendingImmediatesAfterCurrentTask();
|
||||
result = first();
|
||||
// If the first function returns a thenable, suppress unhandled
|
||||
// rejections. A later task in the sequence (e.g. an abort) may
|
||||
// cause the promise to reject, and we don't want that to surface
|
||||
// as an unhandled rejection — the caller will observe the
|
||||
// rejection when they await the returned promise.
|
||||
if (isThenable(result)) {
|
||||
result.then(noop, noop);
|
||||
}
|
||||
} catch (err) {
|
||||
for(let i = 1; i < ids.length; i++){
|
||||
clearTimeout(ids[i]);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
}));
|
||||
for(let i = 0; i < rest.length; i++){
|
||||
const fn = rest[i];
|
||||
let index = ids.length;
|
||||
ids.push(scheduleTimeout(()=>{
|
||||
try {
|
||||
DANGEROUSLY_runPendingImmediatesAfterCurrentTask();
|
||||
fn();
|
||||
} catch (err) {
|
||||
// clear remaining timeouts
|
||||
while(++index < ids.length){
|
||||
clearTimeout(ids[index]);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
}));
|
||||
}
|
||||
// We wait a task before resolving
|
||||
ids.push(scheduleTimeout(()=>{
|
||||
try {
|
||||
expectNoPendingImmediates();
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=app-render-render-utils.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/app-render-render-utils.ts"],"sourcesContent":["import { InvariantError } from '../../shared/lib/invariant-error'\nimport { createAtomicTimerGroup } from './app-render-scheduling'\nimport {\n DANGEROUSLY_runPendingImmediatesAfterCurrentTask,\n expectNoPendingImmediates,\n} from '../node-environment-extensions/fast-set-immediate.external'\nimport { isThenable } from '../../shared/lib/is-thenable'\n\nfunction noop() {}\n\n/**\n * This is a utility function to make scheduling sequential tasks that run back to back easier.\n * We schedule on the same queue (setTimeout) at the same time to ensure no other events can sneak in between.\n *\n * The first function runs in the first task. Each subsequent function runs in its own task.\n * The returned promise resolves after the last task completes.\n */\nexport function runInSequentialTasks<R>(\n first: () => R,\n ...rest: Array<() => void>\n): Promise<Awaited<R>> {\n if (process.env.NEXT_RUNTIME === 'edge') {\n throw new InvariantError(\n '`runInSequentialTasks` should not be called in edge runtime.'\n )\n } else {\n return new Promise((resolve, reject) => {\n const scheduleTimeout = createAtomicTimerGroup()\n const ids: ReturnType<typeof scheduleTimeout>[] = []\n\n let result: R\n ids.push(\n scheduleTimeout(() => {\n try {\n DANGEROUSLY_runPendingImmediatesAfterCurrentTask()\n result = first()\n // If the first function returns a thenable, suppress unhandled\n // rejections. A later task in the sequence (e.g. an abort) may\n // cause the promise to reject, and we don't want that to surface\n // as an unhandled rejection — the caller will observe the\n // rejection when they await the returned promise.\n if (isThenable(result)) {\n result.then(noop, noop)\n }\n } catch (err) {\n for (let i = 1; i < ids.length; i++) {\n clearTimeout(ids[i])\n }\n reject(err)\n }\n })\n )\n\n for (let i = 0; i < rest.length; i++) {\n const fn = rest[i]\n let index = ids.length\n\n ids.push(\n scheduleTimeout(() => {\n try {\n DANGEROUSLY_runPendingImmediatesAfterCurrentTask()\n fn()\n } catch (err) {\n // clear remaining timeouts\n while (++index < ids.length) {\n clearTimeout(ids[index])\n }\n reject(err)\n }\n })\n )\n }\n\n // We wait a task before resolving\n ids.push(\n scheduleTimeout(() => {\n try {\n expectNoPendingImmediates()\n resolve(result as Awaited<R>)\n } catch (err) {\n reject(err)\n }\n })\n )\n })\n }\n}\n"],"names":["InvariantError","createAtomicTimerGroup","DANGEROUSLY_runPendingImmediatesAfterCurrentTask","expectNoPendingImmediates","isThenable","noop","runInSequentialTasks","first","rest","process","env","NEXT_RUNTIME","Promise","resolve","reject","scheduleTimeout","ids","result","push","then","err","i","length","clearTimeout","fn","index"],"mappings":"AAAA,SAASA,cAAc,QAAQ,mCAAkC;AACjE,SAASC,sBAAsB,QAAQ,0BAAyB;AAChE,SACEC,gDAAgD,EAChDC,yBAAyB,QACpB,6DAA4D;AACnE,SAASC,UAAU,QAAQ,+BAA8B;AAEzD,SAASC,QAAQ;AAEjB;;;;;;CAMC,GACD,OAAO,SAASC,qBACdC,KAAc,EACd,GAAGC,IAAuB;IAE1B,IAAIC,QAAQC,GAAG,CAACC,YAAY,KAAK,QAAQ;QACvC,MAAM,qBAEL,CAFK,IAAIX,eACR,iEADI,qBAAA;mBAAA;wBAAA;0BAAA;QAEN;IACF,OAAO;QACL,OAAO,IAAIY,QAAQ,CAACC,SAASC;YAC3B,MAAMC,kBAAkBd;YACxB,MAAMe,MAA4C,EAAE;YAEpD,IAAIC;YACJD,IAAIE,IAAI,CACNH,gBAAgB;gBACd,IAAI;oBACFb;oBACAe,SAASV;oBACT,+DAA+D;oBAC/D,+DAA+D;oBAC/D,iEAAiE;oBACjE,0DAA0D;oBAC1D,kDAAkD;oBAClD,IAAIH,WAAWa,SAAS;wBACtBA,OAAOE,IAAI,CAACd,MAAMA;oBACpB;gBACF,EAAE,OAAOe,KAAK;oBACZ,IAAK,IAAIC,IAAI,GAAGA,IAAIL,IAAIM,MAAM,EAAED,IAAK;wBACnCE,aAAaP,GAAG,CAACK,EAAE;oBACrB;oBACAP,OAAOM;gBACT;YACF;YAGF,IAAK,IAAIC,IAAI,GAAGA,IAAIb,KAAKc,MAAM,EAAED,IAAK;gBACpC,MAAMG,KAAKhB,IAAI,CAACa,EAAE;gBAClB,IAAII,QAAQT,IAAIM,MAAM;gBAEtBN,IAAIE,IAAI,CACNH,gBAAgB;oBACd,IAAI;wBACFb;wBACAsB;oBACF,EAAE,OAAOJ,KAAK;wBACZ,2BAA2B;wBAC3B,MAAO,EAAEK,QAAQT,IAAIM,MAAM,CAAE;4BAC3BC,aAAaP,GAAG,CAACS,MAAM;wBACzB;wBACAX,OAAOM;oBACT;gBACF;YAEJ;YAEA,kCAAkC;YAClCJ,IAAIE,IAAI,CACNH,gBAAgB;gBACd,IAAI;oBACFZ;oBACAU,QAAQI;gBACV,EAAE,OAAOG,KAAK;oBACZN,OAAOM;gBACT;YACF;QAEJ;IACF;AACF","ignoreList":[0]}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
import { unpatchedSetImmediate } from '../node-environment-extensions/fast-set-immediate.external';
|
||||
/*
|
||||
==========================
|
||||
| Background |
|
||||
==========================
|
||||
|
||||
Node.js does not guarantee that two timers scheduled back to back will run
|
||||
on the same iteration of the event loop:
|
||||
|
||||
```ts
|
||||
setTimeout(one, 0)
|
||||
setTimeout(two, 0)
|
||||
```
|
||||
|
||||
Internally, each timer is assigned a `_idleStart` property that holds
|
||||
an internal libuv timestamp in millisecond resolution.
|
||||
This will be used to determine if the timer is already "expired" and should be executed.
|
||||
However, even in sync code, it's possible for two timers to get different `_idleStart` values.
|
||||
This can cause one of the timers to be executed, and the other to be delayed until the next timer phase.
|
||||
|
||||
The delaying happens [here](https://github.com/nodejs/node/blob/c208ffc66bb9418ff026c4e3fa82e5b4387bd147/lib/internal/timers.js#L556-L564).
|
||||
and can be debugged by running node with `NODE_DEBUG=timer`.
|
||||
|
||||
The easiest way to observe it is to run this program in a loop until it exits with status 1:
|
||||
|
||||
```
|
||||
// test.js
|
||||
|
||||
let immediateRan = false
|
||||
const t1 = setTimeout(() => {
|
||||
console.log('timeout 1')
|
||||
setImmediate(() => {
|
||||
console.log('immediate 1')
|
||||
immediateRan = true
|
||||
})
|
||||
})
|
||||
|
||||
const t2 = setTimeout(() => {
|
||||
console.log('timeout 2')
|
||||
if (immediateRan) {
|
||||
console.log('immediate ran before the second timeout!')
|
||||
console.log(
|
||||
`t1._idleStart: ${t1._idleStart}, t2_idleStart: ${t2._idleStart}`
|
||||
);
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
i=1;
|
||||
while true; do
|
||||
output="$(NODE_DEBUG=timer node test.js 2>&1)";
|
||||
if [ "$?" -eq 1 ]; then
|
||||
echo "failed after $i iterations";
|
||||
echo "$output";
|
||||
break;
|
||||
fi;
|
||||
i=$((i+1));
|
||||
done
|
||||
```
|
||||
|
||||
If `t2` is deferred to the next iteration of the event loop,
|
||||
then the immediate scheduled from inside `t1` will run first.
|
||||
When this occurs, `_idleStart` is reliably different between `t1` and `t2`.
|
||||
|
||||
==========================
|
||||
| Solution |
|
||||
==========================
|
||||
|
||||
We can guarantee that multiple timers (with the same delay, usually `0`)
|
||||
run together without any delays by making sure that their `_idleStart`s are the same,
|
||||
because that's what's used to determine if a timer should be deferred or not.
|
||||
Luckily, this property is currently exposed to userland and mutable,
|
||||
so we can patch it.
|
||||
|
||||
Another related trick we could potentially apply is making
|
||||
a timer immediately be considered expired by doing `timer._idleStart -= 2`.
|
||||
(the value must be more than `1`, the delay that actually gets set for `setTimeout(cb, 0)`).
|
||||
This makes node view this timer as "a 1ms timer scheduled 2ms ago",
|
||||
meaning that it should definitely run in the next timer phase.
|
||||
However, I'm not confident we know all the side effects of doing this,
|
||||
so for now, simply ensuring coordination is enough.
|
||||
*/ let shouldAttemptPatching = true;
|
||||
function warnAboutTimers() {
|
||||
console.warn("Next.js cannot guarantee that Cache Components will run as expected due to the current runtime's implementation of `setTimeout()`.\nPlease report a github issue here: https://github.com/vercel/next.js/issues/new/");
|
||||
}
|
||||
/**
|
||||
* Allows scheduling multiple timers (equivalent to `setTimeout(cb, delayMs)`)
|
||||
* that are guaranteed to run in the same iteration of the event loop.
|
||||
*
|
||||
* @param delayMs - the delay to pass to `setTimeout`. (default: 0)
|
||||
*
|
||||
* */ export function createAtomicTimerGroup(delayMs = 0) {
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
throw Object.defineProperty(new InvariantError('createAtomicTimerGroup cannot be called in the edge runtime'), "__NEXT_ERROR_CODE", {
|
||||
value: "E934",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
} else {
|
||||
let isFirstCallback = true;
|
||||
let firstTimerIdleStart = null;
|
||||
let didFirstTimerRun = false;
|
||||
// As a sanity check, we schedule an immediate from the first timeout
|
||||
// to check if the execution was interrupted (i.e. if it ran between the timeouts).
|
||||
// Note that we're deliberately bypassing the "fast setImmediate" patch here --
|
||||
// otherwise, this check would always fail, because the immediate
|
||||
// would always run before the second timeout.
|
||||
let didImmediateRun = false;
|
||||
function runFirstCallback(callback) {
|
||||
didFirstTimerRun = true;
|
||||
if (shouldAttemptPatching) {
|
||||
unpatchedSetImmediate(()=>{
|
||||
didImmediateRun = true;
|
||||
});
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
function runSubsequentCallback(callback) {
|
||||
if (shouldAttemptPatching) {
|
||||
if (didImmediateRun) {
|
||||
// If the immediate managed to run between the timers, then we're not
|
||||
// able to provide the guarantees that we're supposed to
|
||||
shouldAttemptPatching = false;
|
||||
warnAboutTimers();
|
||||
}
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
return function scheduleTimeout(callback) {
|
||||
if (didFirstTimerRun) {
|
||||
throw Object.defineProperty(new InvariantError('Cannot schedule more timers into a group that already executed'), "__NEXT_ERROR_CODE", {
|
||||
value: "E935",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const timer = setTimeout(isFirstCallback ? runFirstCallback : runSubsequentCallback, delayMs, callback);
|
||||
isFirstCallback = false;
|
||||
if (!shouldAttemptPatching) {
|
||||
// We already tried patching some timers, and it didn't work.
|
||||
// No point trying again.
|
||||
return timer;
|
||||
}
|
||||
// NodeJS timers have a `_idleStart` property, but it doesn't exist e.g. in Bun.
|
||||
// If it's not present, we'll warn and try to continue.
|
||||
try {
|
||||
if ('_idleStart' in timer && typeof timer._idleStart === 'number') {
|
||||
// If this is the first timer that was scheduled, save its `_idleStart`.
|
||||
// We'll copy it onto subsequent timers to guarantee that they'll all be
|
||||
// considered expired in the same iteration of the event loop
|
||||
// and thus will all be executed in the same timer phase.
|
||||
if (firstTimerIdleStart === null) {
|
||||
firstTimerIdleStart = timer._idleStart;
|
||||
} else {
|
||||
timer._idleStart = firstTimerIdleStart;
|
||||
}
|
||||
} else {
|
||||
shouldAttemptPatching = false;
|
||||
warnAboutTimers();
|
||||
}
|
||||
} catch (err) {
|
||||
// This should never fail in current Node, but it might start failing in the future.
|
||||
// We might be okay even without tweaking the timers, so warn and try to continue.
|
||||
console.error(Object.defineProperty(new InvariantError('An unexpected error occurred while adjusting `_idleStart` on an atomic timer', {
|
||||
cause: err
|
||||
}), "__NEXT_ERROR_CODE", {
|
||||
value: "E933",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}));
|
||||
shouldAttemptPatching = false;
|
||||
warnAboutTimers();
|
||||
}
|
||||
return timer;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=app-render-scheduling.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+4414
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+50
@@ -0,0 +1,50 @@
|
||||
const sharedAsyncLocalStorageNotAvailableError = Object.defineProperty(new Error('Invariant: AsyncLocalStorage accessed in runtime where it is not available'), "__NEXT_ERROR_CODE", {
|
||||
value: "E504",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
class FakeAsyncLocalStorage {
|
||||
disable() {
|
||||
throw sharedAsyncLocalStorageNotAvailableError;
|
||||
}
|
||||
getStore() {
|
||||
// This fake implementation of AsyncLocalStorage always returns `undefined`.
|
||||
return undefined;
|
||||
}
|
||||
run() {
|
||||
throw sharedAsyncLocalStorageNotAvailableError;
|
||||
}
|
||||
exit() {
|
||||
throw sharedAsyncLocalStorageNotAvailableError;
|
||||
}
|
||||
enterWith() {
|
||||
throw sharedAsyncLocalStorageNotAvailableError;
|
||||
}
|
||||
static bind(fn) {
|
||||
return fn;
|
||||
}
|
||||
}
|
||||
const maybeGlobalAsyncLocalStorage = typeof globalThis !== 'undefined' && globalThis.AsyncLocalStorage;
|
||||
export function createAsyncLocalStorage() {
|
||||
if (maybeGlobalAsyncLocalStorage) {
|
||||
return new maybeGlobalAsyncLocalStorage();
|
||||
}
|
||||
return new FakeAsyncLocalStorage();
|
||||
}
|
||||
export function bindSnapshot(// WARNING: Don't pass a named function to this argument! See: https://github.com/facebook/react/pull/34911
|
||||
fn) {
|
||||
if (maybeGlobalAsyncLocalStorage) {
|
||||
return maybeGlobalAsyncLocalStorage.bind(fn);
|
||||
}
|
||||
return FakeAsyncLocalStorage.bind(fn);
|
||||
}
|
||||
export function createSnapshot() {
|
||||
if (maybeGlobalAsyncLocalStorage) {
|
||||
return maybeGlobalAsyncLocalStorage.snapshot();
|
||||
}
|
||||
return function(fn, ...args) {
|
||||
return fn(...args);
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=async-local-storage.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/async-local-storage.ts"],"sourcesContent":["import type { AsyncLocalStorage } from 'async_hooks'\n\nconst sharedAsyncLocalStorageNotAvailableError = new Error(\n 'Invariant: AsyncLocalStorage accessed in runtime where it is not available'\n)\n\nclass FakeAsyncLocalStorage<Store extends {}>\n implements AsyncLocalStorage<Store>\n{\n disable(): void {\n throw sharedAsyncLocalStorageNotAvailableError\n }\n\n getStore(): Store | undefined {\n // This fake implementation of AsyncLocalStorage always returns `undefined`.\n return undefined\n }\n\n run<R>(): R {\n throw sharedAsyncLocalStorageNotAvailableError\n }\n\n exit<R>(): R {\n throw sharedAsyncLocalStorageNotAvailableError\n }\n\n enterWith(): void {\n throw sharedAsyncLocalStorageNotAvailableError\n }\n\n static bind<T>(fn: T): T {\n return fn\n }\n}\n\nconst maybeGlobalAsyncLocalStorage =\n typeof globalThis !== 'undefined' && (globalThis as any).AsyncLocalStorage\n\nexport function createAsyncLocalStorage<\n Store extends {},\n>(): AsyncLocalStorage<Store> {\n if (maybeGlobalAsyncLocalStorage) {\n return new maybeGlobalAsyncLocalStorage()\n }\n return new FakeAsyncLocalStorage()\n}\n\nexport function bindSnapshot<T>(\n // WARNING: Don't pass a named function to this argument! See: https://github.com/facebook/react/pull/34911\n fn: T\n): T {\n if (maybeGlobalAsyncLocalStorage) {\n return maybeGlobalAsyncLocalStorage.bind(fn)\n }\n return FakeAsyncLocalStorage.bind(fn)\n}\n\nexport function createSnapshot(): <R, TArgs extends any[]>(\n fn: (...args: TArgs) => R,\n ...args: TArgs\n) => R {\n if (maybeGlobalAsyncLocalStorage) {\n return maybeGlobalAsyncLocalStorage.snapshot()\n }\n return function (fn: any, ...args: any[]) {\n return fn(...args)\n }\n}\n"],"names":["sharedAsyncLocalStorageNotAvailableError","Error","FakeAsyncLocalStorage","disable","getStore","undefined","run","exit","enterWith","bind","fn","maybeGlobalAsyncLocalStorage","globalThis","AsyncLocalStorage","createAsyncLocalStorage","bindSnapshot","createSnapshot","snapshot","args"],"mappings":"AAEA,MAAMA,2CAA2C,qBAEhD,CAFgD,IAAIC,MACnD,+EAD+C,qBAAA;WAAA;gBAAA;kBAAA;AAEjD;AAEA,MAAMC;IAGJC,UAAgB;QACd,MAAMH;IACR;IAEAI,WAA8B;QAC5B,4EAA4E;QAC5E,OAAOC;IACT;IAEAC,MAAY;QACV,MAAMN;IACR;IAEAO,OAAa;QACX,MAAMP;IACR;IAEAQ,YAAkB;QAChB,MAAMR;IACR;IAEA,OAAOS,KAAQC,EAAK,EAAK;QACvB,OAAOA;IACT;AACF;AAEA,MAAMC,+BACJ,OAAOC,eAAe,eAAe,AAACA,WAAmBC,iBAAiB;AAE5E,OAAO,SAASC;IAGd,IAAIH,8BAA8B;QAChC,OAAO,IAAIA;IACb;IACA,OAAO,IAAIT;AACb;AAEA,OAAO,SAASa,aACd,2GAA2G;AAC3GL,EAAK;IAEL,IAAIC,8BAA8B;QAChC,OAAOA,6BAA6BF,IAAI,CAACC;IAC3C;IACA,OAAOR,sBAAsBO,IAAI,CAACC;AACpC;AAEA,OAAO,SAASM;IAId,IAAIL,8BAA8B;QAChC,OAAOA,6BAA6BM,QAAQ;IAC9C;IACA,OAAO,SAAUP,EAAO,EAAE,GAAGQ,IAAW;QACtC,OAAOR,MAAMQ;IACf;AACF","ignoreList":[0]}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* This class is used to detect when all cache reads for a given render are settled.
|
||||
* We do this to allow for cache warming the prerender without having to continue rendering
|
||||
* the remainder of the page. This feature is really only useful when the cacheComponents flag is on
|
||||
* and should only be used in codepaths gated with this feature.
|
||||
*/ import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
export class CacheSignal {
|
||||
constructor(){
|
||||
this.count = 0;
|
||||
this.earlyListeners = [];
|
||||
this.listeners = [];
|
||||
this.tickPending = false;
|
||||
this.pendingTimeoutCleanup = null;
|
||||
this.subscribedSignals = null;
|
||||
this.invokeListenersIfNoPendingReads = ()=>{
|
||||
this.pendingTimeoutCleanup = null;
|
||||
if (this.count === 0) {
|
||||
for(let i = 0; i < this.listeners.length; i++){
|
||||
this.listeners[i]();
|
||||
}
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
};
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
// we rely on `process.nextTick`, which is not supported in edge
|
||||
throw Object.defineProperty(new InvariantError('CacheSignal cannot be used in the edge runtime, because `cacheComponents` does not support it.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E728",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
noMorePendingCaches() {
|
||||
if (!this.tickPending) {
|
||||
this.tickPending = true;
|
||||
queueMicrotask(()=>process.nextTick(()=>{
|
||||
this.tickPending = false;
|
||||
if (this.count === 0) {
|
||||
for(let i = 0; i < this.earlyListeners.length; i++){
|
||||
this.earlyListeners[i]();
|
||||
}
|
||||
this.earlyListeners.length = 0;
|
||||
}
|
||||
}));
|
||||
}
|
||||
// After a cache resolves, React will schedule new rendering work:
|
||||
// - in a microtask (when prerendering)
|
||||
// - in setImmediate (when rendering)
|
||||
// To cover both of these, we have to make sure that we let immediates execute at least once after each cache resolved.
|
||||
// We don't know when the pending timeout was scheduled (and if it's about to resolve),
|
||||
// so by scheduling a new one, we can be sure that we'll go around the event loop at least once.
|
||||
if (this.pendingTimeoutCleanup) {
|
||||
// We cancel the timeout in beginRead, so this shouldn't ever be active here,
|
||||
// but we still cancel it defensively.
|
||||
this.pendingTimeoutCleanup();
|
||||
}
|
||||
this.pendingTimeoutCleanup = scheduleImmediateAndTimeoutWithCleanup(this.invokeListenersIfNoPendingReads);
|
||||
}
|
||||
/**
|
||||
* This promise waits until there are no more in progress cache reads but no later.
|
||||
* This allows for adding more cache reads after to delay cacheReady.
|
||||
*/ inputReady() {
|
||||
return new Promise((resolve)=>{
|
||||
this.earlyListeners.push(resolve);
|
||||
if (this.count === 0) {
|
||||
this.noMorePendingCaches();
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* If there are inflight cache reads this Promise can resolve in a microtask however
|
||||
* if there are no inflight cache reads then we wait at least one task to allow initial
|
||||
* cache reads to be initiated.
|
||||
*/ cacheReady() {
|
||||
return new Promise((resolve)=>{
|
||||
this.listeners.push(resolve);
|
||||
if (this.count === 0) {
|
||||
this.noMorePendingCaches();
|
||||
}
|
||||
});
|
||||
}
|
||||
beginRead() {
|
||||
this.count++;
|
||||
// There's a new pending cache, so if there's a `noMorePendingCaches` timeout running,
|
||||
// we should cancel it.
|
||||
if (this.pendingTimeoutCleanup) {
|
||||
this.pendingTimeoutCleanup();
|
||||
this.pendingTimeoutCleanup = null;
|
||||
}
|
||||
if (this.subscribedSignals !== null) {
|
||||
for (const subscriber of this.subscribedSignals){
|
||||
subscriber.beginRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
endRead() {
|
||||
if (this.count === 0) {
|
||||
throw Object.defineProperty(new InvariantError('CacheSignal got more endRead() calls than beginRead() calls'), "__NEXT_ERROR_CODE", {
|
||||
value: "E678",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// If this is the last read we need to wait a task before we can claim the cache is settled.
|
||||
// The cache read will likely ping a Server Component which can read from the cache again and this
|
||||
// will play out in a microtask so we need to only resolve pending listeners if we're still at 0
|
||||
// after at least one task.
|
||||
// We only want one task scheduled at a time so when we hit count 1 we don't decrement the counter immediately.
|
||||
// If intervening reads happen before the scheduled task runs they will never observe count 1 preventing reentrency
|
||||
this.count--;
|
||||
if (this.count === 0) {
|
||||
this.noMorePendingCaches();
|
||||
}
|
||||
if (this.subscribedSignals !== null) {
|
||||
for (const subscriber of this.subscribedSignals){
|
||||
subscriber.endRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
hasPendingReads() {
|
||||
return this.count > 0;
|
||||
}
|
||||
trackRead(promise) {
|
||||
this.beginRead();
|
||||
// `promise.finally()` still rejects, so don't use it here to avoid unhandled rejections
|
||||
const onFinally = this.endRead.bind(this);
|
||||
promise.then(onFinally, onFinally);
|
||||
return promise;
|
||||
}
|
||||
subscribeToReads(subscriber) {
|
||||
if (subscriber === this) {
|
||||
throw Object.defineProperty(new InvariantError('A CacheSignal cannot subscribe to itself'), "__NEXT_ERROR_CODE", {
|
||||
value: "E679",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
if (this.subscribedSignals === null) {
|
||||
this.subscribedSignals = new Set();
|
||||
}
|
||||
this.subscribedSignals.add(subscriber);
|
||||
// we'll notify the subscriber of each endRead() on this signal,
|
||||
// so we need to give it a corresponding beginRead() for each read we have in flight now.
|
||||
for(let i = 0; i < this.count; i++){
|
||||
subscriber.beginRead();
|
||||
}
|
||||
return this.unsubscribeFromReads.bind(this, subscriber);
|
||||
}
|
||||
unsubscribeFromReads(subscriber) {
|
||||
if (!this.subscribedSignals) {
|
||||
return;
|
||||
}
|
||||
this.subscribedSignals.delete(subscriber);
|
||||
// we don't need to set the set back to `null` if it's empty --
|
||||
// if other signals are subscribing to this one, it'll likely get more subscriptions later,
|
||||
// so we'd have to allocate a fresh set again when that happens.
|
||||
}
|
||||
}
|
||||
function scheduleImmediateAndTimeoutWithCleanup(cb) {
|
||||
// If we decide to clean up the timeout, we want to remove
|
||||
// either the immediate or the timeout, whichever is still pending.
|
||||
let clearPending;
|
||||
const immediate = setImmediate(()=>{
|
||||
const timeout = setTimeout(cb, 0);
|
||||
clearPending = clearTimeout.bind(null, timeout);
|
||||
});
|
||||
clearPending = clearImmediate.bind(null, immediate);
|
||||
return ()=>clearPending();
|
||||
}
|
||||
|
||||
//# sourceMappingURL=cache-signal.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+588
@@ -0,0 +1,588 @@
|
||||
/* eslint-disable @next/internal/no-ambiguous-jsx -- Bundled in entry-base so it gets the right JSX runtime. */ import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { PrefetchHint } from '../../shared/lib/app-router-types';
|
||||
import { readVaryParams } from '../../shared/lib/segment-cache/vary-params-decoding';
|
||||
import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createFromReadableStream } from 'react-server-dom-webpack/client';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { prerender } from 'react-server-dom-webpack/static';
|
||||
import { streamFromBuffer, streamToBuffer } from '../stream-utils/node-web-streams-helper';
|
||||
import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler';
|
||||
import { createSegmentRequestKeyPart, appendSegmentRequestKeyPart, ROOT_SEGMENT_REQUEST_KEY, HEAD_REQUEST_KEY } from '../../shared/lib/segment-cache/segment-value-encoding';
|
||||
import { getDigestForWellKnownError } from './create-error-handler';
|
||||
import { Phase, printDebugThrownValueForProspectiveRender } from './prospective-render-utils';
|
||||
import { workAsyncStorage } from './work-async-storage.external';
|
||||
const filterStackFrame = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').filterStackFrameDEV : undefined;
|
||||
const findSourceMapURL = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').findSourceMapURLDEV : undefined;
|
||||
function onSegmentPrerenderError(error) {
|
||||
const digest = getDigestForWellKnownError(error);
|
||||
if (digest) {
|
||||
return digest;
|
||||
}
|
||||
// We don't need to log the errors because we would have already done that
|
||||
// when generating the original Flight stream for the whole page.
|
||||
if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
printDebugThrownValueForProspectiveRender(error, (workStore == null ? void 0 : workStore.route) ?? 'unknown route', Phase.SegmentCollection);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Extract the FlightRouterState, seed data, and head from a prerendered
|
||||
* InitialRSCPayload. Returns null if the payload doesn't match the expected
|
||||
* shape (single path with 3 elements).
|
||||
*/ function extractFlightData(initialRSCPayload) {
|
||||
const flightDataPaths = initialRSCPayload.f;
|
||||
// FlightDataPath is an unsound type, hence the additional checks.
|
||||
if (flightDataPaths.length !== 1 && flightDataPaths[0].length !== 3) {
|
||||
console.error('Internal Next.js error: InitialRSCPayload does not match the expected ' + 'shape for a prerendered page during segment prefetch generation.');
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
buildId: initialRSCPayload.b,
|
||||
flightRouterState: flightDataPaths[0][0],
|
||||
seedData: flightDataPaths[0][1],
|
||||
head: flightDataPaths[0][2]
|
||||
};
|
||||
}
|
||||
export async function collectSegmentData(isCacheComponentsEnabled, fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest, prefetchInlining, hints) {
|
||||
// Traverse the router tree and generate a prefetch response for each segment.
|
||||
// A mutable map to collect the results as we traverse the route tree.
|
||||
const resultMap = new Map();
|
||||
// Before we start, warm up the module cache by decoding the page data once.
|
||||
// Then we can assume that any remaining async tasks that occur the next time
|
||||
// are due to hanging promises caused by dynamic data access. Note we only
|
||||
// have to do this once per page, not per individual segment.
|
||||
//
|
||||
try {
|
||||
await createFromReadableStream(streamFromBuffer(fullPageDataBuffer), {
|
||||
findSourceMapURL,
|
||||
serverConsumerManifest
|
||||
});
|
||||
await waitAtLeastOneReactRenderTask();
|
||||
} catch {}
|
||||
// Create an abort controller that we'll use to stop the stream.
|
||||
const abortController = new AbortController();
|
||||
const onCompletedProcessingRouteTree = async ()=>{
|
||||
// Since all we're doing is decoding and re-encoding a cached prerender, if
|
||||
// serializing the stream takes longer than a microtask, it must because of
|
||||
// hanging promises caused by dynamic data.
|
||||
await waitAtLeastOneReactRenderTask();
|
||||
abortController.abort();
|
||||
};
|
||||
// Generate a stream for the route tree prefetch. While we're walking the
|
||||
// tree, we'll also spawn additional tasks to generate the segment prefetches.
|
||||
// The promises for these tasks are pushed to a mutable array that we will
|
||||
// await once the route tree is fully rendered.
|
||||
const segmentTasks = [];
|
||||
const { prelude: treeStream } = await prerender(// RootTreePrefetch is not a valid return type for a React component, but
|
||||
// we need to use a component so that when we decode the original stream
|
||||
// inside of it, the side effects are transferred to the new stream.
|
||||
// @ts-expect-error
|
||||
/*#__PURE__*/ _jsx(PrefetchTreeData, {
|
||||
isClientParamParsingEnabled: isCacheComponentsEnabled,
|
||||
fullPageDataBuffer: fullPageDataBuffer,
|
||||
serverConsumerManifest: serverConsumerManifest,
|
||||
clientModules: clientModules,
|
||||
staleTime: staleTime,
|
||||
segmentTasks: segmentTasks,
|
||||
onCompletedProcessingRouteTree: onCompletedProcessingRouteTree,
|
||||
prefetchInlining: prefetchInlining,
|
||||
hints: hints
|
||||
}), clientModules, {
|
||||
filterStackFrame,
|
||||
signal: abortController.signal,
|
||||
onError: onSegmentPrerenderError
|
||||
});
|
||||
// Write the route tree to a special `/_tree` segment.
|
||||
const treeBuffer = await streamToBuffer(treeStream);
|
||||
resultMap.set('/_tree', treeBuffer);
|
||||
// Also output the entire full page data response
|
||||
resultMap.set('/_full', fullPageDataBuffer);
|
||||
// Now that we've finished rendering the route tree, all the segment tasks
|
||||
// should have been spawned. Await them in parallel and write the segment
|
||||
// prefetches to the result map.
|
||||
for (const [segmentPath, buffer] of (await Promise.all(segmentTasks))){
|
||||
resultMap.set(segmentPath, buffer);
|
||||
}
|
||||
return resultMap;
|
||||
}
|
||||
/**
|
||||
* Compute prefetch hints for a route by measuring segment sizes and deciding
|
||||
* which segments should be inlined. Only runs at build time. The results are
|
||||
* written to prefetch-hints.json and loaded at server startup.
|
||||
*
|
||||
* This is a separate pass from collectSegmentData so that the inlining
|
||||
* decisions can be fed back into collectSegmentData to control which segments
|
||||
* are output as separate entries vs. inlined into their parent.
|
||||
*/ export async function collectPrefetchHints(fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest, maxSize, maxBundleSize) {
|
||||
// Warm up the module cache, same as collectSegmentData.
|
||||
try {
|
||||
await createFromReadableStream(streamFromBuffer(fullPageDataBuffer), {
|
||||
findSourceMapURL,
|
||||
serverConsumerManifest
|
||||
});
|
||||
await waitAtLeastOneReactRenderTask();
|
||||
} catch {}
|
||||
// Decode the Flight data to walk the route tree.
|
||||
const initialRSCPayload = await createFromReadableStream(createUnclosingPrefetchStream(streamFromBuffer(fullPageDataBuffer)), {
|
||||
findSourceMapURL,
|
||||
serverConsumerManifest
|
||||
});
|
||||
const flightData = extractFlightData(initialRSCPayload);
|
||||
if (flightData === null) {
|
||||
return {
|
||||
hints: 0,
|
||||
slots: null
|
||||
};
|
||||
}
|
||||
const { buildId, flightRouterState, seedData, head } = flightData;
|
||||
// Measure the head (metadata/viewport) gzip size so the main traversal
|
||||
// can decide whether to inline it into a page's bundle.
|
||||
const headVaryParamsThenable = initialRSCPayload.h;
|
||||
const headVaryParams = headVaryParamsThenable !== null ? readVaryParams(headVaryParamsThenable) : null;
|
||||
const [, headBuffer] = await renderSegmentPrefetch(buildId, staleTime, head, HEAD_REQUEST_KEY, headVaryParams, clientModules);
|
||||
const headGzipSize = await getGzipSize(headBuffer);
|
||||
// Mutable accumulator: the first page leaf that can fit the head sets
|
||||
// this to true. Once set, subsequent leaves skip the check.
|
||||
const headInlineState = {
|
||||
inlined: false
|
||||
};
|
||||
// Walk the tree with the parent-first, child-decides algorithm.
|
||||
const { node } = await collectPrefetchHintsImpl(flightRouterState, buildId, staleTime, seedData, clientModules, ROOT_SEGMENT_REQUEST_KEY, null, maxSize, maxBundleSize, headGzipSize, headInlineState);
|
||||
if (!headInlineState.inlined) {
|
||||
// No page could accept the head. Set HeadOutlined on the root so the
|
||||
// client knows to fetch the head separately.
|
||||
node.hints |= PrefetchHint.HeadOutlined;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
// Measure a segment's gzip size and decide whether it should be inlined.
|
||||
//
|
||||
// These hints are computed once during build and never change for the
|
||||
// lifetime of that deployment. The client can assume that hints delivered as
|
||||
// part of one request will be the same during a subsequent request, given
|
||||
// the same build ID. There's no skew to worry about as long as the build
|
||||
// itself is consistent.
|
||||
//
|
||||
// In the Segment Cache, we split page prefetches into multiple requests so
|
||||
// that each one can be cached and deduped independently. However, some
|
||||
// segments are small enough that the potential caching benefits are not worth
|
||||
// the additional network overhead. For these, we inline a parent's data into
|
||||
// one of its children's responses, avoiding a separate request. The parent
|
||||
// is inlined into the child (not the other way around) because the parent's
|
||||
// response is more likely to be shared across multiple pages. The child's
|
||||
// response is already page-specific, so adding the parent's data there
|
||||
// doesn't meaningfully reduce deduplication. It's similar to how JS bundlers
|
||||
// decide whether to inline a module into a chunk.
|
||||
//
|
||||
// The algorithm is parent-first, child-decides: the parent measures itself
|
||||
// and passes its gzip size down. Each child decides whether to accept. A
|
||||
// child rejects if the parent exceeds maxSize or if accepting would push
|
||||
// the cumulative inlined bytes past maxBundleSize. This produces
|
||||
// both ParentInlinedIntoSelf (on the child) and InlinedIntoChild (on the
|
||||
// parent) in a single pass.
|
||||
async function collectPrefetchHintsImpl(route, buildId, staleTime, seedData, clientModules, // TODO: Consider persisting the computed requestKey into the hints output
|
||||
// so it doesn't need to be recomputed during the build. This might also
|
||||
// suggest renaming prefetch-hints.json to something like
|
||||
// segment-manifest.json, since it would contain more than just hints.
|
||||
requestKey, parentGzipSize, maxSize, maxBundleSize, headGzipSize, headInlineState) {
|
||||
// Render current segment and measure its gzip size.
|
||||
let currentGzipSize = null;
|
||||
if (seedData !== null) {
|
||||
const varyParamsThenable = seedData[4];
|
||||
const varyParams = varyParamsThenable !== null ? readVaryParams(varyParamsThenable) : null;
|
||||
const [, buffer] = await renderSegmentPrefetch(buildId, staleTime, seedData[0], requestKey, varyParams, clientModules);
|
||||
currentGzipSize = await getGzipSize(buffer);
|
||||
}
|
||||
// Only offer this segment to its children for inlining if its gzip size
|
||||
// is below maxSize. Segments above this get their own response.
|
||||
const sizeToInline = currentGzipSize !== null && currentGzipSize < maxSize ? currentGzipSize : null;
|
||||
// Process children serially (not in parallel) to ensure deterministic
|
||||
// results. Since this only runs at build time and the rendering is just
|
||||
// re-encoding cached prerenders, this won't impact build times. Each child
|
||||
// receives our gzip size and decides whether to inline us. Once a child
|
||||
// accepts, we stop offering to remaining siblings — the parent is only
|
||||
// inlined into one child. In parallel routes, this avoids duplicating the
|
||||
// parent's data across multiple sibling responses.
|
||||
const children = route[1];
|
||||
const seedDataChildren = seedData !== null ? seedData[1] : null;
|
||||
let slots = null;
|
||||
let didInlineIntoChild = false;
|
||||
let acceptingChildInlinedBytes = 0;
|
||||
// Track the smallest inlinedBytes across all children so we know how much
|
||||
// budget remains along the best path. When our own parent asks whether we
|
||||
// can accept its data, the parent's bytes would flow through to the child
|
||||
// with the most remaining headroom.
|
||||
let smallestChildInlinedBytes = Infinity;
|
||||
let hasChildren = false;
|
||||
for(const parallelRouteKey in children){
|
||||
hasChildren = true;
|
||||
const childRoute = children[parallelRouteKey];
|
||||
const childSegment = childRoute[0];
|
||||
const childSeedData = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null;
|
||||
const childRequestKey = appendSegmentRequestKeyPart(requestKey, parallelRouteKey, createSegmentRequestKeyPart(childSegment));
|
||||
const childResult = await collectPrefetchHintsImpl(childRoute, buildId, staleTime, childSeedData, clientModules, childRequestKey, // Once a child has accepted us, stop offering to remaining siblings.
|
||||
didInlineIntoChild ? null : sizeToInline, maxSize, maxBundleSize, headGzipSize, headInlineState);
|
||||
if (slots === null) {
|
||||
slots = {};
|
||||
}
|
||||
slots[parallelRouteKey] = childResult.node;
|
||||
if (childResult.node.hints & PrefetchHint.ParentInlinedIntoSelf) {
|
||||
// This child accepted our data — it will include our segment's
|
||||
// response in its own. No need to track headroom anymore since
|
||||
// we already know which child we're inlined into.
|
||||
didInlineIntoChild = true;
|
||||
acceptingChildInlinedBytes = childResult.inlinedBytes;
|
||||
} else if (!didInlineIntoChild) {
|
||||
// Track the child with the most remaining headroom. Used below
|
||||
// when deciding whether to accept our own parent's data.
|
||||
if (childResult.inlinedBytes < smallestChildInlinedBytes) {
|
||||
smallestChildInlinedBytes = childResult.inlinedBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Leaf segment: no children have consumed any budget yet.
|
||||
if (!hasChildren) {
|
||||
smallestChildInlinedBytes = 0;
|
||||
}
|
||||
// Mark this segment as InlinedIntoChild if one of its children accepted.
|
||||
// This means this segment doesn't need its own prefetch response — its
|
||||
// data is included in the accepting child's response instead.
|
||||
let hints = 0;
|
||||
if (didInlineIntoChild) {
|
||||
hints |= PrefetchHint.InlinedIntoChild;
|
||||
}
|
||||
// inlinedBytes represents the total gzipped bytes of parent data inlined
|
||||
// into the deepest "inlining target" along this branch. It starts at 0 at
|
||||
// the leaves and grows as parents are inlined going back up the tree. If a
|
||||
// child accepted us, our size is already counted in that child's value.
|
||||
let inlinedBytes = didInlineIntoChild ? acceptingChildInlinedBytes : smallestChildInlinedBytes;
|
||||
// At leaf nodes (pages), try to inline the head (metadata/viewport) into
|
||||
// this page's response. The head is treated like an additional inlined
|
||||
// entry — it counts against the same total budget. Only the first page
|
||||
// that has room gets the head; subsequent pages skip via the shared
|
||||
// headInlineState accumulator.
|
||||
if (!hasChildren && !headInlineState.inlined) {
|
||||
if (inlinedBytes + headGzipSize < maxBundleSize) {
|
||||
hints |= PrefetchHint.HeadInlinedIntoSelf;
|
||||
inlinedBytes += headGzipSize;
|
||||
headInlineState.inlined = true;
|
||||
}
|
||||
}
|
||||
// Decide whether to accept our own parent's data. Two conditions:
|
||||
//
|
||||
// 1. The parent offered us a size (parentGzipSize is not null). It's null
|
||||
// when the parent is too large to inline or when this is the root.
|
||||
//
|
||||
// 2. The total inlined bytes along this branch wouldn't exceed the budget.
|
||||
// Even if each segment is individually small, at some point it no
|
||||
// longer makes sense to keep adding bytes because the combined response
|
||||
// is unique per URL and can't be deduped.
|
||||
//
|
||||
// A node can be both InlinedIntoChild and ParentInlinedIntoSelf. This
|
||||
// happens in multi-level chains: GP → P → C where all are small. C
|
||||
// accepts P (P is InlinedIntoChild), then P also accepts GP (P is
|
||||
// ParentInlinedIntoSelf). The result: C's response includes both P's
|
||||
// and GP's data. The parent's data flows through to the deepest
|
||||
// accepting descendant.
|
||||
if (parentGzipSize !== null) {
|
||||
if (inlinedBytes + parentGzipSize < maxBundleSize) {
|
||||
hints |= PrefetchHint.ParentInlinedIntoSelf;
|
||||
inlinedBytes += parentGzipSize;
|
||||
}
|
||||
}
|
||||
return {
|
||||
node: {
|
||||
hints,
|
||||
slots
|
||||
},
|
||||
inlinedBytes
|
||||
};
|
||||
}
|
||||
// We use gzip size rather than raw size because it better reflects the actual
|
||||
// transfer cost. The inlining trade-off is about whether the overhead of an
|
||||
// additional HTTP request (connection setup, headers, round trip) is worth
|
||||
// the deduplication benefit of keeping a segment separate. Below some
|
||||
// compressed size, the request overhead dominates and inlining is better.
|
||||
// Above it, the deduplication benefit of a cacheable standalone response
|
||||
// wins out.
|
||||
async function getGzipSize(buffer) {
|
||||
const stream = new Blob([
|
||||
new Uint8Array(buffer)
|
||||
]).stream().pipeThrough(new CompressionStream('gzip'));
|
||||
const compressedBlob = await new Response(stream).blob();
|
||||
return compressedBlob.size;
|
||||
}
|
||||
async function PrefetchTreeData({ isClientParamParsingEnabled, fullPageDataBuffer, serverConsumerManifest, clientModules, staleTime, segmentTasks, onCompletedProcessingRouteTree, prefetchInlining, hints }) {
|
||||
// We're currently rendering a Flight response for the route tree prefetch.
|
||||
// Inside this component, decode the Flight stream for the whole page. This is
|
||||
// a hack to transfer the side effects from the original Flight stream (e.g.
|
||||
// Float preloads) onto the Flight stream for the tree prefetch.
|
||||
// TODO: React needs a better way to do this. Needed for Server Actions, too.
|
||||
const initialRSCPayload = await createFromReadableStream(createUnclosingPrefetchStream(streamFromBuffer(fullPageDataBuffer)), {
|
||||
findSourceMapURL,
|
||||
serverConsumerManifest
|
||||
});
|
||||
const flightData = extractFlightData(initialRSCPayload);
|
||||
if (flightData === null) {
|
||||
return null;
|
||||
}
|
||||
const { buildId, flightRouterState, seedData, head } = flightData;
|
||||
// Extract the head vary params from the decoded response.
|
||||
// The head vary params thenable should be fulfilled by now; if not, treat
|
||||
// as unknown (null).
|
||||
const headVaryParamsThenable = initialRSCPayload.h;
|
||||
const headVaryParams = headVaryParamsThenable !== null ? readVaryParams(headVaryParamsThenable) : null;
|
||||
// Compute the route metadata tree by traversing the FlightRouterState. As we
|
||||
// walk the tree, we will also spawn a task to produce a prefetch response for
|
||||
// each segment (unless prefetch inlining is enabled, in which case all
|
||||
// segments are bundled into a single /_inlined response).
|
||||
const tree = collectSegmentDataImpl(isClientParamParsingEnabled, flightRouterState, buildId, staleTime, seedData, clientModules, ROOT_SEGMENT_REQUEST_KEY, segmentTasks, prefetchInlining, hints);
|
||||
if (prefetchInlining) {
|
||||
// When prefetch inlining is enabled, bundle all segment data into a single
|
||||
// /_inlined response instead of individual per-segment responses. The head
|
||||
// is also included in the inlined response.
|
||||
segmentTasks.push(waitAtLeastOneReactRenderTask().then(()=>renderInlinedPrefetchResponse(flightRouterState, buildId, staleTime, seedData, head, headVaryParams, clientModules)));
|
||||
} else {
|
||||
// Also spawn a task to produce a prefetch response for the "head" segment.
|
||||
// The head contains metadata, like the title; it's not really a route
|
||||
// segment, but it contains RSC data, so it's treated like a segment by
|
||||
// the client cache.
|
||||
segmentTasks.push(waitAtLeastOneReactRenderTask().then(()=>renderSegmentPrefetch(buildId, staleTime, head, HEAD_REQUEST_KEY, headVaryParams, clientModules)));
|
||||
}
|
||||
// Notify the abort controller that we're done processing the route tree.
|
||||
// Anything async that happens after this point must be due to hanging
|
||||
// promises in the original stream.
|
||||
onCompletedProcessingRouteTree();
|
||||
// Render the route tree to a special `/_tree` segment.
|
||||
const treePrefetch = {
|
||||
tree,
|
||||
staleTime
|
||||
};
|
||||
if (buildId) {
|
||||
treePrefetch.buildId = buildId;
|
||||
}
|
||||
return treePrefetch;
|
||||
}
|
||||
function collectSegmentDataImpl(isClientParamParsingEnabled, route, buildId, staleTime, seedData, clientModules, requestKey, segmentTasks, prefetchInlining, hintTree) {
|
||||
// Metadata about the segment. Sent as part of the tree prefetch. Null if
|
||||
// there are no children.
|
||||
let slotMetadata = null;
|
||||
const children = route[1];
|
||||
const seedDataChildren = seedData !== null ? seedData[1] : null;
|
||||
for(const parallelRouteKey in children){
|
||||
const childRoute = children[parallelRouteKey];
|
||||
const childSegment = childRoute[0];
|
||||
const childSeedData = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null;
|
||||
const childRequestKey = appendSegmentRequestKeyPart(requestKey, parallelRouteKey, createSegmentRequestKeyPart(childSegment));
|
||||
const childHintTree = hintTree !== null && hintTree.slots !== null ? hintTree.slots[parallelRouteKey] ?? null : null;
|
||||
const childTree = collectSegmentDataImpl(isClientParamParsingEnabled, childRoute, buildId, staleTime, childSeedData, clientModules, childRequestKey, segmentTasks, prefetchInlining, childHintTree);
|
||||
if (slotMetadata === null) {
|
||||
slotMetadata = {};
|
||||
}
|
||||
slotMetadata[parallelRouteKey] = childTree;
|
||||
}
|
||||
// Union the hints already embedded in the FlightRouterState with the
|
||||
// separately-computed build-time hints. During the initial build, the
|
||||
// FlightRouterState was produced before collectPrefetchHints ran, so
|
||||
// inlining hints (ParentInlinedIntoSelf, InlinedIntoChild) won't be in
|
||||
// route[4] yet. On subsequent renders the hints are already in the
|
||||
// FlightRouterState, so the union is idempotent.
|
||||
const prefetchHints = (route[4] ?? 0) | (hintTree !== null ? hintTree.hints : 0);
|
||||
// Determine which params this segment varies on.
|
||||
// Read the vary params thenable directly from the seed data. By the time
|
||||
// collectSegmentData runs, the thenable should be fulfilled. If it's not
|
||||
// fulfilled or null, treat as unknown (null means we can't share cache
|
||||
// entries across param values).
|
||||
const varyParamsThenable = seedData !== null ? seedData[4] : null;
|
||||
const varyParams = varyParamsThenable !== null ? readVaryParams(varyParamsThenable) : null;
|
||||
if (!prefetchInlining) {
|
||||
// When prefetch inlining is disabled, spawn individual segment tasks.
|
||||
// When enabled, segment data is bundled into the /_inlined response
|
||||
// instead, so we skip per-segment tasks here.
|
||||
if (seedData !== null) {
|
||||
// Spawn a task to write the segment data to a new Flight stream.
|
||||
segmentTasks.push(// Since we're already in the middle of a render, wait until after the
|
||||
// current task to escape the current rendering context.
|
||||
waitAtLeastOneReactRenderTask().then(()=>renderSegmentPrefetch(buildId, staleTime, seedData[0], requestKey, varyParams, clientModules)));
|
||||
} else {
|
||||
// This segment does not have any seed data. Skip generating a prefetch
|
||||
// response for it. We'll still include it in the route tree, though.
|
||||
// TODO: We should encode in the route tree whether a segment is missing
|
||||
// so we don't attempt to fetch it for no reason. As of now this shouldn't
|
||||
// ever happen in practice, though.
|
||||
}
|
||||
}
|
||||
const segment = route[0];
|
||||
let name;
|
||||
let param;
|
||||
if (typeof segment === 'string') {
|
||||
name = segment;
|
||||
param = null;
|
||||
} else {
|
||||
name = segment[0];
|
||||
param = {
|
||||
type: segment[2],
|
||||
// This value is omitted from the prefetch response when cacheComponents
|
||||
// is enabled.
|
||||
key: isClientParamParsingEnabled ? null : segment[1],
|
||||
siblings: segment[3]
|
||||
};
|
||||
}
|
||||
// Metadata about the segment. Sent to the client as part of the
|
||||
// tree prefetch.
|
||||
return {
|
||||
name,
|
||||
param,
|
||||
prefetchHints,
|
||||
slots: slotMetadata
|
||||
};
|
||||
}
|
||||
async function renderSegmentPrefetch(buildId, staleTime, rsc, requestKey, varyParams, clientModules) {
|
||||
// Render the segment data to a stream.
|
||||
const segmentPrefetch = {
|
||||
rsc,
|
||||
isPartial: await isPartialRSCData(rsc, clientModules),
|
||||
staleTime,
|
||||
varyParams
|
||||
};
|
||||
if (buildId) {
|
||||
segmentPrefetch.buildId = buildId;
|
||||
}
|
||||
// Since all we're doing is decoding and re-encoding a cached prerender, if
|
||||
// it takes longer than a microtask, it must because of hanging promises
|
||||
// caused by dynamic data. Abort the stream at the end of the current task.
|
||||
const abortController = new AbortController();
|
||||
waitAtLeastOneReactRenderTask().then(()=>abortController.abort());
|
||||
const { prelude: segmentStream } = await prerender(segmentPrefetch, clientModules, {
|
||||
filterStackFrame,
|
||||
signal: abortController.signal,
|
||||
onError: onSegmentPrerenderError
|
||||
});
|
||||
const segmentBuffer = await streamToBuffer(segmentStream);
|
||||
if (requestKey === ROOT_SEGMENT_REQUEST_KEY) {
|
||||
return [
|
||||
'/_index',
|
||||
segmentBuffer
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
requestKey,
|
||||
segmentBuffer
|
||||
];
|
||||
}
|
||||
}
|
||||
async function renderInlinedPrefetchResponse(route, buildId, staleTime, seedData, head, headVaryParams, clientModules) {
|
||||
// Build the inlined tree by walking the route and collecting all segments.
|
||||
const inlinedTree = await buildInlinedSegmentPrefetch(route, buildId, staleTime, seedData, clientModules);
|
||||
// Build the head segment.
|
||||
const headPrefetch = {
|
||||
rsc: head,
|
||||
isPartial: await isPartialRSCData(head, clientModules),
|
||||
staleTime,
|
||||
varyParams: headVaryParams
|
||||
};
|
||||
if (buildId) {
|
||||
headPrefetch.buildId = buildId;
|
||||
}
|
||||
const response = {
|
||||
tree: inlinedTree,
|
||||
head: headPrefetch
|
||||
};
|
||||
// Render as a single Flight response.
|
||||
const abortController = new AbortController();
|
||||
waitAtLeastOneReactRenderTask().then(()=>abortController.abort());
|
||||
const { prelude } = await prerender(response, clientModules, {
|
||||
filterStackFrame,
|
||||
signal: abortController.signal,
|
||||
onError: onSegmentPrerenderError
|
||||
});
|
||||
const buffer = await streamToBuffer(prelude);
|
||||
return [
|
||||
'/' + PAGE_SEGMENT_KEY,
|
||||
buffer
|
||||
];
|
||||
}
|
||||
async function buildInlinedSegmentPrefetch(route, buildId, staleTime, seedData, clientModules) {
|
||||
let slots = null;
|
||||
const children = route[1];
|
||||
const seedDataChildren = seedData !== null ? seedData[1] : null;
|
||||
for(const parallelRouteKey in children){
|
||||
const childRoute = children[parallelRouteKey];
|
||||
const childSeedData = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null;
|
||||
const childPrefetch = await buildInlinedSegmentPrefetch(childRoute, buildId, staleTime, childSeedData, clientModules);
|
||||
if (slots === null) {
|
||||
slots = {};
|
||||
}
|
||||
slots[parallelRouteKey] = childPrefetch;
|
||||
}
|
||||
const rsc = seedData !== null ? seedData[0] : null;
|
||||
const varyParamsThenable = seedData !== null ? seedData[4] : null;
|
||||
const varyParams = varyParamsThenable !== null ? readVaryParams(varyParamsThenable) : null;
|
||||
const segment = {
|
||||
rsc,
|
||||
isPartial: rsc !== null ? await isPartialRSCData(rsc, clientModules) : true,
|
||||
staleTime,
|
||||
varyParams
|
||||
};
|
||||
if (buildId) {
|
||||
segment.buildId = buildId;
|
||||
}
|
||||
return {
|
||||
segment,
|
||||
slots
|
||||
};
|
||||
}
|
||||
async function isPartialRSCData(rsc, clientModules) {
|
||||
// We can determine if a segment contains only partial data if it takes longer
|
||||
// than a task to encode, because dynamic data is encoded as an infinite
|
||||
// promise. We must do this in a separate Flight prerender from the one that
|
||||
// actually generates the prefetch stream because we need to include
|
||||
// `isPartial` in the stream itself.
|
||||
let isPartial = false;
|
||||
const abortController = new AbortController();
|
||||
waitAtLeastOneReactRenderTask().then(()=>{
|
||||
// If we haven't yet finished the outer task, then it must be because we
|
||||
// accessed dynamic data.
|
||||
isPartial = true;
|
||||
abortController.abort();
|
||||
});
|
||||
await prerender(rsc, clientModules, {
|
||||
filterStackFrame,
|
||||
signal: abortController.signal,
|
||||
onError () {}
|
||||
});
|
||||
return isPartial;
|
||||
}
|
||||
function createUnclosingPrefetchStream(originalFlightStream) {
|
||||
// When PPR is enabled, prefetch streams may contain references that never
|
||||
// resolve, because that's how we encode dynamic data access. In the decoded
|
||||
// object returned by the Flight client, these are reified into hanging
|
||||
// promises that suspend during render, which is effectively what we want.
|
||||
// The UI resolves when it switches to the dynamic data stream
|
||||
// (via useDeferredValue(dynamic, static)).
|
||||
//
|
||||
// However, the Flight implementation currently errors if the server closes
|
||||
// the response before all the references are resolved. As a cheat to work
|
||||
// around this, we wrap the original stream in a new stream that never closes,
|
||||
// and therefore doesn't error.
|
||||
const reader = originalFlightStream.getReader();
|
||||
return new ReadableStream({
|
||||
async pull (controller) {
|
||||
while(true){
|
||||
const { done, value } = await reader.read();
|
||||
if (!done) {
|
||||
// Pass to the target stream and keep consuming the Flight response
|
||||
// from the server.
|
||||
controller.enqueue(value);
|
||||
continue;
|
||||
}
|
||||
// The server stream has closed. Exit, but intentionally do not close
|
||||
// the target stream.
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=collect-segment-data.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { createAsyncLocalStorage } from './async-local-storage';
|
||||
export const consoleAsyncStorageInstance = createAsyncLocalStorage();
|
||||
|
||||
//# sourceMappingURL=console-async-storage-instance.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/console-async-storage-instance.ts"],"sourcesContent":["import { createAsyncLocalStorage } from './async-local-storage'\nimport type { ConsoleAsyncStorage } from './console-async-storage.external'\n\nexport const consoleAsyncStorageInstance: ConsoleAsyncStorage =\n createAsyncLocalStorage()\n"],"names":["createAsyncLocalStorage","consoleAsyncStorageInstance"],"mappings":"AAAA,SAASA,uBAAuB,QAAQ,wBAAuB;AAG/D,OAAO,MAAMC,8BACXD,0BAAyB","ignoreList":[0]}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
// Share the instance module in the next-shared layer
|
||||
import { consoleAsyncStorageInstance } from './console-async-storage-instance' with {
|
||||
'turbopack-transition': 'next-shared'
|
||||
};
|
||||
export { consoleAsyncStorageInstance as consoleAsyncStorage };
|
||||
|
||||
//# sourceMappingURL=console-async-storage.external.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/console-async-storage.external.ts"],"sourcesContent":["import type { AsyncLocalStorage } from 'async_hooks'\n\n// Share the instance module in the next-shared layer\nimport { consoleAsyncStorageInstance } from './console-async-storage-instance' with { 'turbopack-transition': 'next-shared' }\n\nexport interface ConsoleStore {\n /**\n * if true the color of logs output will be dimmed to indicate the log is\n * from a repeat or validation render that is not typically relevant to\n * the primary action the server is taking.\n */\n readonly dim: boolean\n}\n\nexport type ConsoleAsyncStorage = AsyncLocalStorage<ConsoleStore>\n\nexport { consoleAsyncStorageInstance as consoleAsyncStorage }\n"],"names":["consoleAsyncStorageInstance","consoleAsyncStorage"],"mappings":"AAEA,qDAAqD;AACrD,SAASA,2BAA2B,QAAQ,wCAAwC;IAAE,wBAAwB;AAAc,EAAC;AAa7H,SAASA,+BAA+BC,mBAAmB,GAAE","ignoreList":[0]}
|
||||
Generated
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
import { interopDefault } from './interop-default';
|
||||
import { getLinkAndScriptTags } from './get-css-inlined-link-tags';
|
||||
import { getAssetQueryString } from './get-asset-query-string';
|
||||
import { encodeURIPath } from '../../shared/lib/encode-uri-path';
|
||||
import { renderCssResource } from './render-css-resource';
|
||||
export async function createComponentStylesAndScripts({ filePath, getComponent, injectedCSS, injectedJS, ctx }) {
|
||||
const { componentMod: { createElement } } = ctx;
|
||||
const { styles: entryCssFiles, scripts: jsHrefs } = getLinkAndScriptTags(filePath, injectedCSS, injectedJS);
|
||||
const styles = renderCssResource(entryCssFiles, ctx);
|
||||
const scripts = jsHrefs ? jsHrefs.map((href, index)=>createElement('script', {
|
||||
src: `${ctx.assetPrefix}/_next/${encodeURIPath(href)}${getAssetQueryString(ctx, true)}`,
|
||||
async: true,
|
||||
key: `script-${index}`
|
||||
})) : null;
|
||||
const Comp = interopDefault(await getComponent());
|
||||
return [
|
||||
Comp,
|
||||
styles,
|
||||
scripts
|
||||
];
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-component-styles-and-scripts.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/create-component-styles-and-scripts.tsx"],"sourcesContent":["import { interopDefault } from './interop-default'\nimport { getLinkAndScriptTags } from './get-css-inlined-link-tags'\nimport type { AppRenderContext } from './app-render'\nimport { getAssetQueryString } from './get-asset-query-string'\nimport { encodeURIPath } from '../../shared/lib/encode-uri-path'\nimport { renderCssResource } from './render-css-resource'\n\nexport async function createComponentStylesAndScripts({\n filePath,\n getComponent,\n injectedCSS,\n injectedJS,\n ctx,\n}: {\n filePath: string\n getComponent: () => any\n injectedCSS: Set<string>\n injectedJS: Set<string>\n ctx: AppRenderContext\n}): Promise<[React.ComponentType<any>, React.ReactNode, React.ReactNode]> {\n const {\n componentMod: { createElement },\n } = ctx\n const { styles: entryCssFiles, scripts: jsHrefs } = getLinkAndScriptTags(\n filePath,\n injectedCSS,\n injectedJS\n )\n\n const styles = renderCssResource(entryCssFiles, ctx)\n\n const scripts = jsHrefs\n ? jsHrefs.map((href, index) =>\n createElement('script', {\n src: `${ctx.assetPrefix}/_next/${encodeURIPath(href)}${getAssetQueryString(ctx, true)}`,\n async: true,\n key: `script-${index}`,\n })\n )\n : null\n\n const Comp = interopDefault(await getComponent())\n\n return [Comp, styles, scripts]\n}\n"],"names":["interopDefault","getLinkAndScriptTags","getAssetQueryString","encodeURIPath","renderCssResource","createComponentStylesAndScripts","filePath","getComponent","injectedCSS","injectedJS","ctx","componentMod","createElement","styles","entryCssFiles","scripts","jsHrefs","map","href","index","src","assetPrefix","async","key","Comp"],"mappings":"AAAA,SAASA,cAAc,QAAQ,oBAAmB;AAClD,SAASC,oBAAoB,QAAQ,8BAA6B;AAElE,SAASC,mBAAmB,QAAQ,2BAA0B;AAC9D,SAASC,aAAa,QAAQ,mCAAkC;AAChE,SAASC,iBAAiB,QAAQ,wBAAuB;AAEzD,OAAO,eAAeC,gCAAgC,EACpDC,QAAQ,EACRC,YAAY,EACZC,WAAW,EACXC,UAAU,EACVC,GAAG,EAOJ;IACC,MAAM,EACJC,cAAc,EAAEC,aAAa,EAAE,EAChC,GAAGF;IACJ,MAAM,EAAEG,QAAQC,aAAa,EAAEC,SAASC,OAAO,EAAE,GAAGf,qBAClDK,UACAE,aACAC;IAGF,MAAMI,SAAST,kBAAkBU,eAAeJ;IAEhD,MAAMK,UAAUC,UACZA,QAAQC,GAAG,CAAC,CAACC,MAAMC,QACjBP,cAAc,UAAU;YACtBQ,KAAK,GAAGV,IAAIW,WAAW,CAAC,OAAO,EAAElB,cAAce,QAAQhB,oBAAoBQ,KAAK,OAAO;YACvFY,OAAO;YACPC,KAAK,CAAC,OAAO,EAAEJ,OAAO;QACxB,MAEF;IAEJ,MAAMK,OAAOxB,eAAe,MAAMO;IAElC,OAAO;QAACiB;QAAMX;QAAQE;KAAQ;AAChC","ignoreList":[0]}
|
||||
+821
@@ -0,0 +1,821 @@
|
||||
import { isClientReference, isUseCacheFunction } from '../../lib/client-and-server-references';
|
||||
import { getLayoutOrPageModule } from '../lib/app-dir-module';
|
||||
import { interopDefault } from './interop-default';
|
||||
import { parseLoaderTree } from '../../shared/lib/router/utils/parse-loader-tree';
|
||||
import { createComponentStylesAndScripts } from './create-component-styles-and-scripts';
|
||||
import { getLayerAssets } from './get-layer-assets';
|
||||
import { hasLoadingComponentInTree } from './has-loading-component-in-tree';
|
||||
import { validateRevalidate } from '../lib/patch-fetch';
|
||||
import { PARALLEL_ROUTE_DEFAULT_PATH } from '../../client/components/builtin/default';
|
||||
import { getTracer } from '../lib/trace/tracer';
|
||||
import { NextNodeServerSpan } from '../lib/trace/constants';
|
||||
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout';
|
||||
import { workUnitAsyncStorage } from './work-unit-async-storage.external';
|
||||
import { createVaryParamsAccumulator, emptyVaryParamsAccumulator, getVaryParamsThenable } from './vary-params';
|
||||
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment';
|
||||
import { BOUNDARY_PREFIX, BOUNDARY_SUFFIX, BUILTIN_PREFIX, getConventionPathByType, isNextjsBuiltinFilePath } from './segment-explorer-path';
|
||||
import { RenderStage } from './staged-rendering';
|
||||
/**
|
||||
* Use the provided loader tree to create the React Component tree.
|
||||
*/ // TODO convert these arguments to non-object form. the entrypoint doesn't need most of them
|
||||
export function createComponentTree(props) {
|
||||
return getTracer().trace(NextNodeServerSpan.createComponentTree, {
|
||||
spanName: 'build component tree'
|
||||
}, ()=>createComponentTreeInternal(props, true));
|
||||
}
|
||||
function errorMissingDefaultExport(pagePath, convention) {
|
||||
const normalizedPagePath = pagePath === '/' ? '' : pagePath;
|
||||
throw Object.defineProperty(new Error(`The default export is not a React Component in "${normalizedPagePath}/${convention}"`), "__NEXT_ERROR_CODE", {
|
||||
value: "E45",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const cacheNodeKey = 'c';
|
||||
async function createComponentTreeInternal({ loaderTree: tree, parentParams, parentOptionalCatchAllParamName, parentRuntimePrefetchable, rootLayoutIncluded, injectedCSS, injectedJS, injectedFontPreloadTags, ctx, missingSlots, preloadCallbacks, authInterrupts, MetadataOutlet }, isRoot) {
|
||||
const { renderOpts: { nextConfigOutput, experimental, cacheComponents }, workStore, componentMod: { createElement, Fragment, SegmentViewNode, HTTPAccessFallbackBoundary, LayoutRouter, RenderFromTemplateContext, ClientPageRoot, ClientSegmentRoot, createServerSearchParamsForServerPage, createPrerenderSearchParamsForClientPage, createServerParamsForServerSegment, createPrerenderParamsForClientSegment, serverHooks: { DynamicServerError }, Postpone }, pagePath, getDynamicParamFromSegment, isPrefetch, query } = ctx;
|
||||
const { page, conventionPath, segment, modules, parallelRoutes } = parseLoaderTree(tree);
|
||||
const { layout, template, error, loading, 'not-found': notFound, forbidden, unauthorized } = modules;
|
||||
const injectedCSSWithCurrentLayout = new Set(injectedCSS);
|
||||
const injectedJSWithCurrentLayout = new Set(injectedJS);
|
||||
const injectedFontPreloadTagsWithCurrentLayout = new Set(injectedFontPreloadTags);
|
||||
const layerAssets = getLayerAssets({
|
||||
preloadCallbacks,
|
||||
ctx,
|
||||
layoutOrPagePath: conventionPath,
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout,
|
||||
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout
|
||||
});
|
||||
const [Template, templateStyles, templateScripts] = template ? await createComponentStylesAndScripts({
|
||||
ctx,
|
||||
filePath: template[1],
|
||||
getComponent: template[0],
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout
|
||||
}) : [
|
||||
Fragment
|
||||
];
|
||||
const [ErrorComponent, errorStyles, errorScripts] = error ? await createComponentStylesAndScripts({
|
||||
ctx,
|
||||
filePath: error[1],
|
||||
getComponent: error[0],
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout
|
||||
}) : [];
|
||||
const [Loading, loadingStyles, loadingScripts] = loading ? await createComponentStylesAndScripts({
|
||||
ctx,
|
||||
filePath: loading[1],
|
||||
getComponent: loading[0],
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout
|
||||
}) : [];
|
||||
const isLayout = typeof layout !== 'undefined';
|
||||
const isPage = typeof page !== 'undefined';
|
||||
const { mod: layoutOrPageMod, modType } = await getTracer().trace(NextNodeServerSpan.getLayoutOrPageModule, {
|
||||
hideSpan: !(isLayout || isPage),
|
||||
spanName: 'resolve segment modules',
|
||||
attributes: {
|
||||
'next.segment': segment
|
||||
}
|
||||
}, ()=>getLayoutOrPageModule(tree));
|
||||
/**
|
||||
* Checks if the current segment is a root layout.
|
||||
*/ const rootLayoutAtThisLevel = isLayout && !rootLayoutIncluded;
|
||||
/**
|
||||
* Checks if the current segment or any level above it has a root layout.
|
||||
*/ const rootLayoutIncludedAtThisLevelOrAbove = rootLayoutIncluded || rootLayoutAtThisLevel;
|
||||
const [NotFound, notFoundStyles] = notFound ? await createComponentStylesAndScripts({
|
||||
ctx,
|
||||
filePath: notFound[1],
|
||||
getComponent: notFound[0],
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout
|
||||
}) : [];
|
||||
const instantConfig = layoutOrPageMod ? layoutOrPageMod.unstable_instant : undefined;
|
||||
const hasRuntimePrefetch = instantConfig && typeof instantConfig === 'object' ? instantConfig.prefetch === 'runtime' : false;
|
||||
const isRuntimePrefetchable = hasRuntimePrefetch || parentRuntimePrefetchable;
|
||||
const [Forbidden, forbiddenStyles] = authInterrupts && forbidden ? await createComponentStylesAndScripts({
|
||||
ctx,
|
||||
filePath: forbidden[1],
|
||||
getComponent: forbidden[0],
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout
|
||||
}) : [];
|
||||
const [Unauthorized, unauthorizedStyles] = authInterrupts && unauthorized ? await createComponentStylesAndScripts({
|
||||
ctx,
|
||||
filePath: unauthorized[1],
|
||||
getComponent: unauthorized[0],
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout
|
||||
}) : [];
|
||||
let dynamic = layoutOrPageMod == null ? void 0 : layoutOrPageMod.dynamic;
|
||||
if (nextConfigOutput === 'export') {
|
||||
if (!dynamic || dynamic === 'auto') {
|
||||
dynamic = 'error';
|
||||
} else if (dynamic === 'force-dynamic') {
|
||||
// force-dynamic is always incompatible with 'export'. We must interrupt the build
|
||||
throw Object.defineProperty(new StaticGenBailoutError(`Page with \`dynamic = "force-dynamic"\` couldn't be exported. \`output: "export"\` requires all pages be renderable statically because there is no runtime server to dynamically render routes in this output format. Learn more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports`), "__NEXT_ERROR_CODE", {
|
||||
value: "E527",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
if (typeof dynamic === 'string') {
|
||||
// the nested most config wins so we only force-static
|
||||
// if it's configured above any parent that configured
|
||||
// otherwise
|
||||
if (dynamic === 'error') {
|
||||
workStore.dynamicShouldError = true;
|
||||
} else if (dynamic === 'force-dynamic') {
|
||||
workStore.forceDynamic = true;
|
||||
// TODO: (PPR) remove this bailout once PPR is the default
|
||||
if (workStore.isStaticGeneration && !experimental.isRoutePPREnabled) {
|
||||
// If the postpone API isn't available, we can't postpone the render and
|
||||
// therefore we can't use the dynamic API.
|
||||
const err = Object.defineProperty(new DynamicServerError(`Page with \`dynamic = "force-dynamic"\` won't be rendered statically.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E585",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
workStore.dynamicUsageDescription = err.message;
|
||||
workStore.dynamicUsageStack = err.stack;
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
workStore.dynamicShouldError = false;
|
||||
workStore.forceStatic = dynamic === 'force-static';
|
||||
}
|
||||
}
|
||||
if (typeof (layoutOrPageMod == null ? void 0 : layoutOrPageMod.fetchCache) === 'string') {
|
||||
workStore.fetchCache = layoutOrPageMod == null ? void 0 : layoutOrPageMod.fetchCache;
|
||||
}
|
||||
if (typeof (layoutOrPageMod == null ? void 0 : layoutOrPageMod.revalidate) !== 'undefined') {
|
||||
validateRevalidate(layoutOrPageMod == null ? void 0 : layoutOrPageMod.revalidate, workStore.route);
|
||||
}
|
||||
if (typeof (layoutOrPageMod == null ? void 0 : layoutOrPageMod.revalidate) === 'number') {
|
||||
const defaultRevalidate = layoutOrPageMod.revalidate;
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'prerender':
|
||||
case 'prerender-runtime':
|
||||
case 'prerender-legacy':
|
||||
case 'prerender-ppr':
|
||||
if (workUnitStore.revalidate > defaultRevalidate) {
|
||||
workUnitStore.revalidate = defaultRevalidate;
|
||||
}
|
||||
break;
|
||||
case 'request':
|
||||
break;
|
||||
// createComponentTree is not called for these stores:
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'prerender-client':
|
||||
case 'validation-client':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
if (!workStore.forceStatic && workStore.isStaticGeneration && defaultRevalidate === 0 && // If the postpone API isn't available, we can't postpone the render and
|
||||
// therefore we can't use the dynamic API.
|
||||
!experimental.isRoutePPREnabled) {
|
||||
const dynamicUsageDescription = `revalidate: 0 configured ${segment}`;
|
||||
workStore.dynamicUsageDescription = dynamicUsageDescription;
|
||||
throw Object.defineProperty(new DynamicServerError(dynamicUsageDescription), "__NEXT_ERROR_CODE", {
|
||||
value: "E1005",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
// Read unstable_dynamicStaleTime from page modules (not layouts) and track it on
|
||||
// the store's stale field. This affects the segment cache stale time via
|
||||
// the StaleTimeIterable.
|
||||
if (isPage && typeof (layoutOrPageMod == null ? void 0 : layoutOrPageMod.unstable_dynamicStaleTime) === 'number') {
|
||||
const pageStaleTime = layoutOrPageMod.unstable_dynamicStaleTime;
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'prerender':
|
||||
case 'prerender-runtime':
|
||||
case 'prerender-legacy':
|
||||
case 'prerender-ppr':
|
||||
if (workUnitStore.stale > pageStaleTime) {
|
||||
workUnitStore.stale = pageStaleTime;
|
||||
}
|
||||
break;
|
||||
case 'request':
|
||||
if (workUnitStore.stale === undefined || workUnitStore.stale > pageStaleTime) {
|
||||
workUnitStore.stale = pageStaleTime;
|
||||
}
|
||||
break;
|
||||
// createComponentTree is not called for these stores:
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'prerender-client':
|
||||
case 'validation-client':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
const isStaticGeneration = workStore.isStaticGeneration;
|
||||
// Assume the segment we're rendering contains only partial data if PPR is
|
||||
// enabled and this is a statically generated response. This is used by the
|
||||
// client Segment Cache after a prefetch to determine if it can skip the
|
||||
// second request to fill in the dynamic data.
|
||||
//
|
||||
// It's OK for this to be `true` when the data is actually fully static, but
|
||||
// it's not OK for this to be `false` when the data possibly contains holes.
|
||||
// Although the value here is overly pessimistic, for prefetches, it will be
|
||||
// replaced by a more specific value when the data is later processed into
|
||||
// per-segment responses (see collect-segment-data.tsx)
|
||||
//
|
||||
// For dynamic requests, this must always be `false` because dynamic responses
|
||||
// are never partial.
|
||||
const isPossiblyPartialResponse = isStaticGeneration && experimental.isRoutePPREnabled === true;
|
||||
const LayoutOrPage = layoutOrPageMod ? interopDefault(layoutOrPageMod) : undefined;
|
||||
/**
|
||||
* The React Component to render.
|
||||
*/ let MaybeComponent = LayoutOrPage;
|
||||
if (process.env.NODE_ENV === 'development' || isStaticGeneration) {
|
||||
const { isValidElementType } = require('next/dist/compiled/react-is');
|
||||
if (typeof MaybeComponent !== 'undefined' && !isValidElementType(MaybeComponent)) {
|
||||
errorMissingDefaultExport(pagePath, modType ?? 'page');
|
||||
}
|
||||
if (typeof ErrorComponent !== 'undefined' && !isValidElementType(ErrorComponent)) {
|
||||
errorMissingDefaultExport(pagePath, 'error');
|
||||
}
|
||||
if (typeof Loading !== 'undefined' && !isValidElementType(Loading)) {
|
||||
errorMissingDefaultExport(pagePath, 'loading');
|
||||
}
|
||||
if (typeof NotFound !== 'undefined' && !isValidElementType(NotFound)) {
|
||||
errorMissingDefaultExport(pagePath, 'not-found');
|
||||
}
|
||||
if (typeof Forbidden !== 'undefined' && !isValidElementType(Forbidden)) {
|
||||
errorMissingDefaultExport(pagePath, 'forbidden');
|
||||
}
|
||||
if (typeof Unauthorized !== 'undefined' && !isValidElementType(Unauthorized)) {
|
||||
errorMissingDefaultExport(pagePath, 'unauthorized');
|
||||
}
|
||||
}
|
||||
// Handle dynamic segment params.
|
||||
const segmentParam = getDynamicParamFromSegment(tree);
|
||||
// Create object holding the parent params and current params
|
||||
let currentParams = parentParams;
|
||||
if (segmentParam && segmentParam.value !== null) {
|
||||
currentParams = {
|
||||
...parentParams,
|
||||
[segmentParam.param]: segmentParam.value
|
||||
};
|
||||
}
|
||||
// Track optional catch-all params with no value (e.g., [[...slug]] at /).
|
||||
// These params won't exist as properties on the params object, so vary
|
||||
// params tracking needs to use a Proxy to detect access. We propagate this
|
||||
// through the tree so that child segments (like __PAGE__) also know about
|
||||
// the missing param. In practice, this only gets passed down one level —
|
||||
// from the optional catch-all layout segment to the page segment — so it's
|
||||
// always very close to the leaf of the tree.
|
||||
const optionalCatchAllParamName = (segmentParam == null ? void 0 : segmentParam.type) === 'oc' && segmentParam.value === null ? segmentParam.param : parentOptionalCatchAllParamName;
|
||||
// Resolve the segment param
|
||||
const isSegmentViewEnabled = !!process.env.__NEXT_DEV_SERVER;
|
||||
const dir = (process.env.NEXT_RUNTIME === 'edge' ? process.env.__NEXT_EDGE_PROJECT_DIR : ctx.renderOpts.dir) || '';
|
||||
const [notFoundElement, notFoundFilePath] = await createBoundaryConventionElement({
|
||||
ctx,
|
||||
conventionName: 'not-found',
|
||||
Component: NotFound,
|
||||
styles: notFoundStyles,
|
||||
tree
|
||||
});
|
||||
const [forbiddenElement] = await createBoundaryConventionElement({
|
||||
ctx,
|
||||
conventionName: 'forbidden',
|
||||
Component: Forbidden,
|
||||
styles: forbiddenStyles,
|
||||
tree
|
||||
});
|
||||
const [unauthorizedElement] = await createBoundaryConventionElement({
|
||||
ctx,
|
||||
conventionName: 'unauthorized',
|
||||
Component: Unauthorized,
|
||||
styles: unauthorizedStyles,
|
||||
tree
|
||||
});
|
||||
// TODO: Combine this `map` traversal with the loop below that turns the array
|
||||
// into an object.
|
||||
const parallelRouteMap = await Promise.all(Object.keys(parallelRoutes).map(async (parallelRouteKey)=>{
|
||||
const isChildrenRouteKey = parallelRouteKey === 'children';
|
||||
const parallelRoute = parallelRoutes[parallelRouteKey];
|
||||
const notFoundComponent = isChildrenRouteKey ? notFoundElement : undefined;
|
||||
const forbiddenComponent = isChildrenRouteKey ? forbiddenElement : undefined;
|
||||
const unauthorizedComponent = isChildrenRouteKey ? unauthorizedElement : undefined;
|
||||
// if we're prefetching and that there's a Loading component, we bail out
|
||||
// otherwise we keep rendering for the prefetch.
|
||||
// We also want to bail out if there's no Loading component in the tree.
|
||||
let childCacheNodeSeedData = null;
|
||||
if (// Before PPR, the way instant navigations work in Next.js is we
|
||||
// prefetch everything up to the first route segment that defines a
|
||||
// loading.tsx boundary. (We do the same if there's no loading
|
||||
// boundary in the entire tree, because we don't want to prefetch too
|
||||
// much) The rest of the tree is deferred until the actual navigation.
|
||||
// It does not take into account whether the data is dynamic — even if
|
||||
// the tree is completely static, it will still defer everything
|
||||
// inside the loading boundary.
|
||||
//
|
||||
// This behavior predates PPR and is only relevant if the
|
||||
// PPR flag is not enabled.
|
||||
isPrefetch && (Loading || !hasLoadingComponentInTree(parallelRoute)) && // The approach with PPR is different — loading.tsx behaves like a
|
||||
// regular Suspense boundary and has no special behavior.
|
||||
//
|
||||
// With PPR, we prefetch as deeply as possible, and only defer when
|
||||
// dynamic data is accessed. If so, we only defer the nearest parent
|
||||
// Suspense boundary of the dynamic data access, regardless of whether
|
||||
// the boundary is defined by loading.tsx or a normal <Suspense>
|
||||
// component in userspace.
|
||||
//
|
||||
// NOTE: In practice this usually means we'll end up prefetching more
|
||||
// than we were before PPR, which may or may not be considered a
|
||||
// performance regression by some apps. The plan is to address this
|
||||
// before General Availability of PPR by introducing granular
|
||||
// per-segment fetching, so we can reuse as much of the tree as
|
||||
// possible during both prefetches and dynamic navigations. But during
|
||||
// the beta period, we should be clear about this trade off in our
|
||||
// communications.
|
||||
!experimental.isRoutePPREnabled) {
|
||||
// Don't prefetch this child. This will trigger a lazy fetch by the
|
||||
// client router.
|
||||
} else {
|
||||
// Create the child component
|
||||
if (process.env.NODE_ENV === 'development' && missingSlots) {
|
||||
var _parsedTree_conventionPath;
|
||||
// When we detect the default fallback (which triggers a 404), we collect the missing slots
|
||||
// to provide more helpful debug information during development mode.
|
||||
const parsedTree = parseLoaderTree(parallelRoute);
|
||||
if ((_parsedTree_conventionPath = parsedTree.conventionPath) == null ? void 0 : _parsedTree_conventionPath.endsWith(PARALLEL_ROUTE_DEFAULT_PATH)) {
|
||||
missingSlots.add(parallelRouteKey);
|
||||
}
|
||||
}
|
||||
const seedData = await createComponentTreeInternal({
|
||||
loaderTree: parallelRoute,
|
||||
parentParams: currentParams,
|
||||
parentOptionalCatchAllParamName: optionalCatchAllParamName,
|
||||
parentRuntimePrefetchable: isRuntimePrefetchable,
|
||||
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
|
||||
injectedCSS: injectedCSSWithCurrentLayout,
|
||||
injectedJS: injectedJSWithCurrentLayout,
|
||||
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
|
||||
ctx,
|
||||
missingSlots,
|
||||
preloadCallbacks,
|
||||
authInterrupts,
|
||||
// `StreamingMetadataOutlet` is used to conditionally throw. In the case of parallel routes we will have more than one page
|
||||
// but we only want to throw on the first one.
|
||||
MetadataOutlet: isChildrenRouteKey ? MetadataOutlet : null
|
||||
}, false);
|
||||
childCacheNodeSeedData = seedData;
|
||||
}
|
||||
const templateNode = createElement(Template, null, createElement(RenderFromTemplateContext, null));
|
||||
const templateFilePath = getConventionPathByType(tree, dir, 'template');
|
||||
const errorFilePath = getConventionPathByType(tree, dir, 'error');
|
||||
const loadingFilePath = getConventionPathByType(tree, dir, 'loading');
|
||||
const globalErrorFilePath = isRoot ? getConventionPathByType(tree, dir, 'global-error') : undefined;
|
||||
const wrappedErrorStyles = isSegmentViewEnabled && errorFilePath ? createElement(SegmentViewNode, {
|
||||
type: 'error',
|
||||
pagePath: errorFilePath
|
||||
}, errorStyles) : errorStyles;
|
||||
// Add a suffix to avoid conflict with the segment view node representing rendered file.
|
||||
// existence: not-found.tsx@boundary
|
||||
// rendered: not-found.tsx
|
||||
const fileNameSuffix = BOUNDARY_SUFFIX;
|
||||
const segmentViewBoundaries = isSegmentViewEnabled ? createElement(Fragment, null, notFoundFilePath && createElement(SegmentViewNode, {
|
||||
type: `${BOUNDARY_PREFIX}not-found`,
|
||||
pagePath: notFoundFilePath + fileNameSuffix
|
||||
}), loadingFilePath && createElement(SegmentViewNode, {
|
||||
type: `${BOUNDARY_PREFIX}loading`,
|
||||
pagePath: loadingFilePath + fileNameSuffix
|
||||
}), errorFilePath && createElement(SegmentViewNode, {
|
||||
type: `${BOUNDARY_PREFIX}error`,
|
||||
pagePath: errorFilePath + fileNameSuffix
|
||||
}), globalErrorFilePath && createElement(SegmentViewNode, {
|
||||
type: `${BOUNDARY_PREFIX}global-error`,
|
||||
pagePath: isNextjsBuiltinFilePath(globalErrorFilePath) ? `${BUILTIN_PREFIX}global-error.js${fileNameSuffix}` : globalErrorFilePath
|
||||
})) : null;
|
||||
return [
|
||||
parallelRouteKey,
|
||||
createElement(LayoutRouter, {
|
||||
parallelRouterKey: parallelRouteKey,
|
||||
error: ErrorComponent,
|
||||
errorStyles: wrappedErrorStyles,
|
||||
errorScripts: errorScripts,
|
||||
template: isSegmentViewEnabled && templateFilePath ? createElement(SegmentViewNode, {
|
||||
type: 'template',
|
||||
pagePath: templateFilePath
|
||||
}, templateNode) : templateNode,
|
||||
templateStyles: templateStyles,
|
||||
templateScripts: templateScripts,
|
||||
notFound: notFoundComponent,
|
||||
forbidden: forbiddenComponent,
|
||||
unauthorized: unauthorizedComponent,
|
||||
...isSegmentViewEnabled && {
|
||||
segmentViewBoundaries
|
||||
}
|
||||
}),
|
||||
childCacheNodeSeedData
|
||||
];
|
||||
}));
|
||||
// Convert the parallel route map into an object after all promises have been resolved.
|
||||
let parallelRouteProps = {};
|
||||
let parallelRouteCacheNodeSeedData = {};
|
||||
for (const parallelRoute of parallelRouteMap){
|
||||
const [parallelRouteKey, parallelRouteProp, flightData] = parallelRoute;
|
||||
parallelRouteProps[parallelRouteKey] = parallelRouteProp;
|
||||
parallelRouteCacheNodeSeedData[parallelRouteKey] = flightData;
|
||||
}
|
||||
let loadingElement = Loading ? createElement(Loading, {
|
||||
key: 'l'
|
||||
}) : null;
|
||||
const loadingFilePath = getConventionPathByType(tree, dir, 'loading');
|
||||
if (isSegmentViewEnabled && loadingElement) {
|
||||
if (loadingFilePath) {
|
||||
loadingElement = createElement(SegmentViewNode, {
|
||||
key: cacheNodeKey + '-loading',
|
||||
type: 'loading',
|
||||
pagePath: loadingFilePath
|
||||
}, loadingElement);
|
||||
}
|
||||
}
|
||||
const loadingData = loadingElement ? [
|
||||
loadingElement,
|
||||
loadingStyles,
|
||||
loadingScripts
|
||||
] : null;
|
||||
// When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component
|
||||
if (!MaybeComponent) {
|
||||
return createSeedData(ctx, createElement(Fragment, {
|
||||
key: cacheNodeKey
|
||||
}, layerAssets, parallelRouteProps.children), parallelRouteCacheNodeSeedData, loadingData, isPossiblyPartialResponse, isRuntimePrefetchable, // No user-provided component, so no params will be accessed. Use the
|
||||
// pre-resolved empty tracker.
|
||||
emptyVaryParamsAccumulator);
|
||||
}
|
||||
const Component = MaybeComponent;
|
||||
// If force-dynamic is used and the current render supports postponing, we
|
||||
// replace it with a node that will postpone the render. This ensures that the
|
||||
// postpone is invoked during the react render phase and not during the next
|
||||
// render phase.
|
||||
// @TODO this does not actually do what it seems like it would or should do. The idea is that
|
||||
// if we are rendering in a force-dynamic mode and we can postpone we should only make the segments
|
||||
// that ask for force-dynamic to be dynamic, allowing other segments to still prerender. However
|
||||
// because this comes after the children traversal and the static generation store is mutated every segment
|
||||
// along the parent path of a force-dynamic segment will hit this condition effectively making the entire
|
||||
// render force-dynamic. We should refactor this function so that we can correctly track which segments
|
||||
// need to be dynamic
|
||||
if (workStore.isStaticGeneration && workStore.forceDynamic && experimental.isRoutePPREnabled) {
|
||||
return createSeedData(ctx, createElement(Fragment, {
|
||||
key: cacheNodeKey
|
||||
}, createElement(Postpone, {
|
||||
reason: 'dynamic = "force-dynamic" was used',
|
||||
route: workStore.route
|
||||
}), layerAssets), parallelRouteCacheNodeSeedData, loadingData, true, isRuntimePrefetchable, // force-dynamic postpones without rendering the component, so no params
|
||||
// are accessed. The vary params are empty.
|
||||
emptyVaryParamsAccumulator);
|
||||
}
|
||||
const isClientComponent = isClientReference(layoutOrPageMod);
|
||||
const varyParamsAccumulator = isClientComponent && cacheComponents ? // from the server, so they have an empty vary params set.
|
||||
emptyVaryParamsAccumulator : createVaryParamsAccumulator();
|
||||
if (process.env.NODE_ENV === 'development' && 'params' in parallelRouteProps) {
|
||||
// @TODO consider making this an error and running the check in build as well
|
||||
console.error(`"params" is a reserved prop in Layouts and Pages and cannot be used as the name of a parallel route in ${segment}`);
|
||||
}
|
||||
if (isPage) {
|
||||
const PageComponent = Component;
|
||||
// Assign searchParams to props if this is a page
|
||||
let pageElement;
|
||||
if (isClientComponent) {
|
||||
if (cacheComponents) {
|
||||
// Params are omitted when Cache Components is enabled
|
||||
pageElement = createElement(ClientPageRoot, {
|
||||
Component: PageComponent,
|
||||
serverProvidedParams: null
|
||||
});
|
||||
} else if (isStaticGeneration) {
|
||||
const promiseOfParams = createPrerenderParamsForClientSegment(currentParams);
|
||||
const promiseOfSearchParams = createPrerenderSearchParamsForClientPage();
|
||||
pageElement = createElement(ClientPageRoot, {
|
||||
Component: PageComponent,
|
||||
serverProvidedParams: {
|
||||
searchParams: query,
|
||||
params: currentParams,
|
||||
promises: [
|
||||
promiseOfSearchParams,
|
||||
promiseOfParams
|
||||
]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
pageElement = createElement(ClientPageRoot, {
|
||||
Component: PageComponent,
|
||||
serverProvidedParams: {
|
||||
searchParams: query,
|
||||
params: currentParams,
|
||||
promises: null
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If we are passing params to a server component Page we need to track
|
||||
// their usage in case the current render mode tracks dynamic API usage.
|
||||
const params = createServerParamsForServerSegment(currentParams, optionalCatchAllParamName, varyParamsAccumulator, isRuntimePrefetchable);
|
||||
// If we are passing searchParams to a server component Page we need to
|
||||
// track their usage in case the current render mode tracks dynamic API
|
||||
// usage.
|
||||
let searchParams = createServerSearchParamsForServerPage(query, varyParamsAccumulator, isRuntimePrefetchable);
|
||||
if (isUseCacheFunction(PageComponent)) {
|
||||
const UseCachePageComponent = PageComponent;
|
||||
pageElement = createElement(UseCachePageComponent, {
|
||||
params: params,
|
||||
searchParams: searchParams,
|
||||
$$isPage: true
|
||||
});
|
||||
} else {
|
||||
pageElement = createElement(PageComponent, {
|
||||
params: params,
|
||||
searchParams: searchParams
|
||||
});
|
||||
}
|
||||
}
|
||||
const isDefaultSegment = segment === DEFAULT_SEGMENT_KEY;
|
||||
const pageFilePath = getConventionPathByType(tree, dir, 'page') ?? getConventionPathByType(tree, dir, 'defaultPage');
|
||||
const segmentType = isDefaultSegment ? 'default' : 'page';
|
||||
const wrappedPageElement = isSegmentViewEnabled && pageFilePath ? createElement(SegmentViewNode, {
|
||||
key: cacheNodeKey + '-' + segmentType,
|
||||
type: segmentType,
|
||||
pagePath: pageFilePath
|
||||
}, pageElement) : pageElement;
|
||||
return createSeedData(ctx, createElement(Fragment, {
|
||||
key: cacheNodeKey
|
||||
}, wrappedPageElement, layerAssets, MetadataOutlet ? createElement(MetadataOutlet, null) : null), parallelRouteCacheNodeSeedData, loadingData, isPossiblyPartialResponse, isRuntimePrefetchable, varyParamsAccumulator);
|
||||
} else {
|
||||
const SegmentComponent = Component;
|
||||
const isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot = rootLayoutAtThisLevel && 'children' in parallelRoutes && Object.keys(parallelRoutes).length > 1;
|
||||
let segmentNode;
|
||||
if (isClientComponent) {
|
||||
let clientSegment;
|
||||
if (cacheComponents) {
|
||||
// Params are omitted when Cache Components is enabled
|
||||
clientSegment = createElement(ClientSegmentRoot, {
|
||||
Component: SegmentComponent,
|
||||
slots: parallelRouteProps,
|
||||
serverProvidedParams: null
|
||||
});
|
||||
} else if (isStaticGeneration) {
|
||||
const promiseOfParams = createPrerenderParamsForClientSegment(currentParams);
|
||||
clientSegment = createElement(ClientSegmentRoot, {
|
||||
Component: SegmentComponent,
|
||||
slots: parallelRouteProps,
|
||||
serverProvidedParams: {
|
||||
params: currentParams,
|
||||
promises: [
|
||||
promiseOfParams
|
||||
]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
clientSegment = createElement(ClientSegmentRoot, {
|
||||
Component: SegmentComponent,
|
||||
slots: parallelRouteProps,
|
||||
serverProvidedParams: {
|
||||
params: currentParams,
|
||||
promises: null
|
||||
}
|
||||
});
|
||||
}
|
||||
if (isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot) {
|
||||
let notfoundClientSegment;
|
||||
let forbiddenClientSegment;
|
||||
let unauthorizedClientSegment;
|
||||
// TODO-APP: This is a hack to support unmatched parallel routes, which will throw `notFound()`.
|
||||
// This ensures that a `HTTPAccessFallbackBoundary` is available for when that happens,
|
||||
// but it's not ideal, as it needlessly invokes the `NotFound` component and renders the `RootLayout` twice.
|
||||
// We should instead look into handling the fallback behavior differently in development mode so that it doesn't
|
||||
// rely on the `NotFound` behavior.
|
||||
notfoundClientSegment = createErrorBoundaryClientSegmentRoot({
|
||||
ctx,
|
||||
ErrorBoundaryComponent: NotFound,
|
||||
errorElement: notFoundElement,
|
||||
ClientSegmentRoot,
|
||||
layerAssets,
|
||||
SegmentComponent,
|
||||
currentParams
|
||||
});
|
||||
forbiddenClientSegment = createErrorBoundaryClientSegmentRoot({
|
||||
ctx,
|
||||
ErrorBoundaryComponent: Forbidden,
|
||||
errorElement: forbiddenElement,
|
||||
ClientSegmentRoot,
|
||||
layerAssets,
|
||||
SegmentComponent,
|
||||
currentParams
|
||||
});
|
||||
unauthorizedClientSegment = createErrorBoundaryClientSegmentRoot({
|
||||
ctx,
|
||||
ErrorBoundaryComponent: Unauthorized,
|
||||
errorElement: unauthorizedElement,
|
||||
ClientSegmentRoot,
|
||||
layerAssets,
|
||||
SegmentComponent,
|
||||
currentParams
|
||||
});
|
||||
if (notfoundClientSegment || forbiddenClientSegment || unauthorizedClientSegment) {
|
||||
segmentNode = createElement(HTTPAccessFallbackBoundary, {
|
||||
key: cacheNodeKey,
|
||||
notFound: notfoundClientSegment,
|
||||
forbidden: forbiddenClientSegment,
|
||||
unauthorized: unauthorizedClientSegment
|
||||
}, layerAssets, clientSegment);
|
||||
} else {
|
||||
segmentNode = createElement(Fragment, {
|
||||
key: cacheNodeKey
|
||||
}, layerAssets, clientSegment);
|
||||
}
|
||||
} else {
|
||||
segmentNode = createElement(Fragment, {
|
||||
key: cacheNodeKey
|
||||
}, layerAssets, clientSegment);
|
||||
}
|
||||
} else {
|
||||
const params = createServerParamsForServerSegment(currentParams, optionalCatchAllParamName, varyParamsAccumulator, isRuntimePrefetchable);
|
||||
let serverSegment;
|
||||
if (isUseCacheFunction(SegmentComponent)) {
|
||||
const UseCacheLayoutComponent = SegmentComponent;
|
||||
serverSegment = createElement(UseCacheLayoutComponent, {
|
||||
...parallelRouteProps,
|
||||
params: params,
|
||||
$$isLayout: true
|
||||
}, // Force static children here so that they're validated.
|
||||
// See https://github.com/facebook/react/pull/34846
|
||||
parallelRouteProps.children);
|
||||
} else {
|
||||
serverSegment = createElement(SegmentComponent, {
|
||||
...parallelRouteProps,
|
||||
params: params
|
||||
}, // Force static children here so that they're validated.
|
||||
// See https://github.com/facebook/react/pull/34846
|
||||
parallelRouteProps.children);
|
||||
}
|
||||
if (isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot) {
|
||||
// TODO-APP: This is a hack to support unmatched parallel routes, which will throw `notFound()`.
|
||||
// This ensures that a `HTTPAccessFallbackBoundary` is available for when that happens,
|
||||
// but it's not ideal, as it needlessly invokes the `NotFound` component and renders the `RootLayout` twice.
|
||||
// We should instead look into handling the fallback behavior differently in development mode so that it doesn't
|
||||
// rely on the `NotFound` behavior.
|
||||
segmentNode = createElement(HTTPAccessFallbackBoundary, {
|
||||
key: cacheNodeKey,
|
||||
notFound: notFoundElement ? createElement(Fragment, null, layerAssets, createElement(SegmentComponent, {
|
||||
params: params
|
||||
}, notFoundStyles, notFoundElement)) : undefined
|
||||
}, layerAssets, serverSegment);
|
||||
} else {
|
||||
segmentNode = createElement(Fragment, {
|
||||
key: cacheNodeKey
|
||||
}, layerAssets, serverSegment);
|
||||
}
|
||||
}
|
||||
const layoutFilePath = getConventionPathByType(tree, dir, 'layout');
|
||||
const wrappedSegmentNode = isSegmentViewEnabled && layoutFilePath ? createElement(SegmentViewNode, {
|
||||
key: 'layout',
|
||||
type: 'layout',
|
||||
pagePath: layoutFilePath
|
||||
}, segmentNode) : segmentNode;
|
||||
// For layouts we just render the component
|
||||
return createSeedData(ctx, wrappedSegmentNode, parallelRouteCacheNodeSeedData, loadingData, isPossiblyPartialResponse, isRuntimePrefetchable, varyParamsAccumulator);
|
||||
}
|
||||
}
|
||||
function createErrorBoundaryClientSegmentRoot({ ctx, ErrorBoundaryComponent, errorElement, ClientSegmentRoot, layerAssets, SegmentComponent, currentParams }) {
|
||||
const { componentMod: { createElement, Fragment } } = ctx;
|
||||
if (ErrorBoundaryComponent) {
|
||||
const notFoundParallelRouteProps = {
|
||||
children: errorElement
|
||||
};
|
||||
return createElement(Fragment, null, layerAssets, createElement(ClientSegmentRoot, {
|
||||
Component: SegmentComponent,
|
||||
slots: notFoundParallelRouteProps,
|
||||
params: currentParams
|
||||
}));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export function getRootParams(loaderTree, getDynamicParamFromSegment) {
|
||||
return getRootParamsImpl({}, loaderTree, getDynamicParamFromSegment);
|
||||
}
|
||||
function getRootParamsImpl(parentParams, loaderTree, getDynamicParamFromSegment) {
|
||||
const { modules: { layout }, parallelRoutes } = parseLoaderTree(loaderTree);
|
||||
const segmentParam = getDynamicParamFromSegment(loaderTree);
|
||||
let currentParams = parentParams;
|
||||
if (segmentParam && segmentParam.value !== null) {
|
||||
currentParams = {
|
||||
...parentParams,
|
||||
[segmentParam.param]: segmentParam.value
|
||||
};
|
||||
}
|
||||
const isRootLayout = typeof layout !== 'undefined';
|
||||
if (isRootLayout) {
|
||||
return currentParams;
|
||||
} else if (!parallelRoutes.children) {
|
||||
// This should really be an error but there are bugs in Turbopack that cause
|
||||
// the _not-found LoaderTree to not have any layouts. For rootParams sake
|
||||
// this is somewhat irrelevant when you are not customizing the 404 page.
|
||||
// If you are customizing 404
|
||||
// TODO update rootParams to make all params optional if `/app/not-found.tsx` is defined
|
||||
return currentParams;
|
||||
} else {
|
||||
return getRootParamsImpl(currentParams, // We stop looking for root params as soon as we hit the first layout
|
||||
// and it is not possible to use parallel route children above the root layout
|
||||
// so every parallelRoutes object that this function can visit will necessarily
|
||||
// have a single `children` prop and no others.
|
||||
parallelRoutes.children, getDynamicParamFromSegment);
|
||||
}
|
||||
}
|
||||
async function createBoundaryConventionElement({ ctx, conventionName, Component, styles, tree }) {
|
||||
const { componentMod: { createElement, Fragment } } = ctx;
|
||||
const isSegmentViewEnabled = !!process.env.__NEXT_DEV_SERVER;
|
||||
const dir = (process.env.NEXT_RUNTIME === 'edge' ? process.env.__NEXT_EDGE_PROJECT_DIR : ctx.renderOpts.dir) || '';
|
||||
const { SegmentViewNode } = ctx.componentMod;
|
||||
const element = Component ? createElement(Fragment, null, createElement(Component, null), styles) : undefined;
|
||||
const pagePath = getConventionPathByType(tree, dir, conventionName);
|
||||
const wrappedElement = isSegmentViewEnabled && element ? createElement(SegmentViewNode, {
|
||||
key: cacheNodeKey + '-' + conventionName,
|
||||
type: conventionName,
|
||||
// TODO: Discovered when moving to `createElement`.
|
||||
// `SegmentViewNode` doesn't support undefined `pagePath`
|
||||
pagePath: pagePath
|
||||
}, element) : element;
|
||||
return [
|
||||
wrappedElement,
|
||||
pagePath
|
||||
];
|
||||
}
|
||||
function createSeedData(ctx, rsc, parallelRoutes, loading, isPossiblyPartialResponse, isRuntimePrefetchable, varyParamsAccumulator) {
|
||||
const createElement = ctx.componentMod.createElement;
|
||||
// When this segment is NOT runtime-prefetchable, delay it until the Static
|
||||
// stage by wrapping the node in a promise. This allows runtime-prefetchable
|
||||
// segments (the lower tree) to render first during EarlyStatic, so their
|
||||
// runtime data resolves in EarlyRuntime where sync IO can be checked.
|
||||
// React will suspend on the thenable and resume when the stage advances.
|
||||
if (!isRuntimePrefetchable) {
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workUnitStore) {
|
||||
let stagedRendering;
|
||||
switch(workUnitStore.type){
|
||||
case 'request':
|
||||
case 'prerender-runtime':
|
||||
stagedRendering = workUnitStore.stagedRendering;
|
||||
if (stagedRendering) {
|
||||
const deferredRsc = rsc;
|
||||
rsc = stagedRendering.waitForStage(RenderStage.Static).then(()=>deferredRsc);
|
||||
}
|
||||
break;
|
||||
case 'prerender':
|
||||
case 'prerender-client':
|
||||
case 'validation-client':
|
||||
case 'prerender-ppr':
|
||||
case 'prerender-legacy':
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loading !== null) {
|
||||
// If a loading.tsx boundary is present, wrap the component data in an
|
||||
// additional context provider to pass the loading data to the next
|
||||
// set of children.
|
||||
// NOTE: The reason this is a separate wrapper from LayoutRouter is because
|
||||
// not all segments render a LayoutRouter component, e.g. the root segment.
|
||||
const LoadingBoundaryProvider = ctx.componentMod.LoadingBoundaryProvider;
|
||||
rsc = createElement(LoadingBoundaryProvider, {
|
||||
loading: loading,
|
||||
children: rsc
|
||||
});
|
||||
}
|
||||
return [
|
||||
rsc,
|
||||
parallelRoutes,
|
||||
null,
|
||||
isPossiblyPartialResponse,
|
||||
varyParamsAccumulator ? getVaryParamsThenable(varyParamsAccumulator) : null
|
||||
];
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-component-tree.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+158
@@ -0,0 +1,158 @@
|
||||
import stringHash from 'next/dist/compiled/string-hash';
|
||||
import { formatServerError } from '../../lib/format-server-error';
|
||||
import { SpanStatusCode, getTracer } from '../lib/trace/tracer';
|
||||
import { isAbortError } from '../pipe-readable';
|
||||
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr';
|
||||
import { isDynamicServerError } from '../../client/components/hooks-server-context';
|
||||
import { isNextRouterError } from '../../client/components/is-next-router-error';
|
||||
import { isPrerenderInterruptedError } from './dynamic-rendering';
|
||||
import { getProperError } from '../../lib/is-error';
|
||||
import { createDigestWithErrorCode } from '../../lib/error-telemetry-utils';
|
||||
import { isReactLargeShellError } from './react-large-shell-error';
|
||||
import { isInstantValidationError } from './instant-validation/instant-validation-error';
|
||||
/**
|
||||
* Returns a digest for well-known Next.js errors, otherwise `undefined`. If a
|
||||
* digest is returned this also means that the error does not need to be
|
||||
* reported.
|
||||
*/ export function getDigestForWellKnownError(error) {
|
||||
// If we're bailing out to CSR, we don't need to log the error.
|
||||
if (isBailoutToCSRError(error)) return error.digest;
|
||||
// If this is a navigation error, we don't need to log the error.
|
||||
if (isNextRouterError(error)) return error.digest;
|
||||
// If this error occurs, we know that we should be stopping the static
|
||||
// render. This is only thrown in static generation when PPR is not enabled,
|
||||
// which causes the whole page to be marked as dynamic. We don't need to
|
||||
// tell the user about this error, as it's not actionable.
|
||||
if (isDynamicServerError(error)) return error.digest;
|
||||
// If this is a prerender interrupted error, we don't need to log the error.
|
||||
if (isPrerenderInterruptedError(error)) return error.digest;
|
||||
if (isInstantValidationError(error)) return error.digest;
|
||||
return undefined;
|
||||
}
|
||||
export function createReactServerErrorHandler(shouldFormatError, isBuildTimePrerendering, reactServerErrors, onReactServerRenderError, spanToRecordOn) {
|
||||
return (thrownValue)=>{
|
||||
var _err_message;
|
||||
if (typeof thrownValue === 'string') {
|
||||
// TODO-APP: look at using webcrypto instead. Requires a promise to be awaited.
|
||||
return stringHash(thrownValue).toString();
|
||||
}
|
||||
// If the response was closed, we don't need to log the error.
|
||||
if (isAbortError(thrownValue)) return;
|
||||
const digest = getDigestForWellKnownError(thrownValue);
|
||||
if (digest) {
|
||||
return digest;
|
||||
}
|
||||
if (isReactLargeShellError(thrownValue)) {
|
||||
// TODO: Aggregate
|
||||
console.error(thrownValue);
|
||||
return undefined;
|
||||
}
|
||||
let err = getProperError(thrownValue);
|
||||
let silenceLog = false;
|
||||
// If the error already has a digest, respect the original digest,
|
||||
// so it won't get re-generated into another new error.
|
||||
if (err.digest) {
|
||||
if (process.env.NODE_ENV === 'production' && reactServerErrors.has(err.digest)) {
|
||||
// This error is likely an obfuscated error from another react-server
|
||||
// environment (e.g. 'use cache'). We recover the original error here
|
||||
// for reporting purposes.
|
||||
err = reactServerErrors.get(err.digest);
|
||||
// We don't log it again though, as it was already logged in the
|
||||
// original environment.
|
||||
silenceLog = true;
|
||||
} else {
|
||||
// Either we're in development (where we want to keep the transported
|
||||
// error with environmentName), or the error is not in reactServerErrors
|
||||
// but has a digest from other means. Keep the error as-is.
|
||||
}
|
||||
} else {
|
||||
err.digest = createDigestWithErrorCode(err, // TODO-APP: look at using webcrypto instead. Requires a promise to be awaited.
|
||||
stringHash(err.message + (err.stack || '')).toString());
|
||||
}
|
||||
// @TODO by putting this here and not at the top it is possible that
|
||||
// we don't error the build in places we actually expect to
|
||||
if (!reactServerErrors.has(err.digest)) {
|
||||
reactServerErrors.set(err.digest, err);
|
||||
}
|
||||
// Format server errors in development to add more helpful error messages
|
||||
if (shouldFormatError) {
|
||||
formatServerError(err);
|
||||
}
|
||||
// Don't log the suppressed error during export
|
||||
if (!(isBuildTimePrerendering && (err == null ? void 0 : (_err_message = err.message) == null ? void 0 : _err_message.includes('The specific message is omitted in production builds to avoid leaking sensitive details.')))) {
|
||||
// Record exception on the provided span if available, otherwise try active span.
|
||||
const span = spanToRecordOn ?? getTracer().getActiveScopeSpan();
|
||||
if (span) {
|
||||
span.recordException(err);
|
||||
span.setAttribute('error.type', err.name);
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
onReactServerRenderError(err, silenceLog);
|
||||
}
|
||||
return err.digest;
|
||||
};
|
||||
}
|
||||
export function createHTMLErrorHandler(shouldFormatError, isBuildTimePrerendering, reactServerErrors, allCapturedErrors, onHTMLRenderSSRError, spanToRecordOn) {
|
||||
return (thrownValue, errorInfo)=>{
|
||||
var _err_message;
|
||||
if (isReactLargeShellError(thrownValue)) {
|
||||
// TODO: Aggregate
|
||||
console.error(thrownValue);
|
||||
return undefined;
|
||||
}
|
||||
let isSSRError = true;
|
||||
allCapturedErrors.push(thrownValue);
|
||||
// If the response was closed, we don't need to log the error.
|
||||
if (isAbortError(thrownValue)) return;
|
||||
const digest = getDigestForWellKnownError(thrownValue);
|
||||
if (digest) {
|
||||
return digest;
|
||||
}
|
||||
const err = getProperError(thrownValue);
|
||||
// If the error already has a digest, respect the original digest,
|
||||
// so it won't get re-generated into another new error.
|
||||
if (err.digest) {
|
||||
if (reactServerErrors.has(err.digest)) {
|
||||
// This error is likely an obfuscated error from react-server.
|
||||
// We recover the original error here.
|
||||
thrownValue = reactServerErrors.get(err.digest);
|
||||
isSSRError = false;
|
||||
} else {
|
||||
// The error is not from react-server but has a digest
|
||||
// from other means so we don't need to produce a new one
|
||||
}
|
||||
} else {
|
||||
err.digest = createDigestWithErrorCode(err, stringHash(err.message + ((errorInfo == null ? void 0 : errorInfo.componentStack) || err.stack || '')).toString());
|
||||
}
|
||||
// Format server errors in development to add more helpful error messages
|
||||
if (shouldFormatError) {
|
||||
formatServerError(err);
|
||||
}
|
||||
// Don't log the suppressed error during export
|
||||
if (!(isBuildTimePrerendering && (err == null ? void 0 : (_err_message = err.message) == null ? void 0 : _err_message.includes('The specific message is omitted in production builds to avoid leaking sensitive details.')))) {
|
||||
// HTML errors contain RSC errors as well, filter them out before reporting
|
||||
if (isSSRError) {
|
||||
// Record exception on the provided span if available, otherwise try active span.
|
||||
const span = spanToRecordOn ?? getTracer().getActiveScopeSpan();
|
||||
if (span) {
|
||||
span.recordException(err);
|
||||
span.setAttribute('error.type', err.name);
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
onHTMLRenderSSRError(err, errorInfo);
|
||||
}
|
||||
}
|
||||
return err.digest;
|
||||
};
|
||||
}
|
||||
export function isUserLandError(err) {
|
||||
return !isAbortError(err) && !isBailoutToCSRError(err) && !isNextRouterError(err);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-error-handler.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
frontend/node_modules/next/dist/esm/server/app-render/create-flight-router-state-from-loader-tree.js
Generated
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
import { PrefetchHint } from '../../shared/lib/app-router-types';
|
||||
import { addSearchParamsIfPageSegment } from '../../shared/lib/segment';
|
||||
async function createFlightRouterStateFromLoaderTreeImpl(loaderTree, hintTree, getDynamicParamFromSegment, searchParams, didFindRootLayout) {
|
||||
const [segment, parallelRoutes, { layout, loading, page }] = loaderTree;
|
||||
const dynamicParam = getDynamicParamFromSegment(loaderTree);
|
||||
const treeSegment = dynamicParam ? dynamicParam.treeSegment : segment;
|
||||
const segmentTree = [
|
||||
addSearchParamsIfPageSegment(treeSegment, searchParams),
|
||||
{}
|
||||
];
|
||||
// Load the layout or page module to check for unstable_instant config
|
||||
const mod = layout ? await layout[0]() : page ? await page[0]() : undefined;
|
||||
const instantConfig = mod ? mod.unstable_instant : undefined;
|
||||
let prefetchHints = 0;
|
||||
// Union in the precomputed build-time hints (e.g. segment inlining
|
||||
// decisions) if available. When hints are not available (e.g. dev mode or
|
||||
// if prefetch-hints.json was not generated), we fall through and still
|
||||
// compute the other hints below. In the future this should be a build
|
||||
// error, but for now we gracefully degrade.
|
||||
//
|
||||
// TODO: Move more of the hints computation (IsRootLayout, instant config,
|
||||
// loading boundary detection) into the build-time measurement step in
|
||||
// collectPrefetchHints, so this function only needs to union the
|
||||
// precomputed bitmask rather than re-derive hints on every render.
|
||||
if (hintTree !== null) {
|
||||
prefetchHints |= hintTree.hints;
|
||||
}
|
||||
// Mark the first segment that has a layout as the "root" layout
|
||||
if (!didFindRootLayout && typeof layout !== 'undefined') {
|
||||
didFindRootLayout = true;
|
||||
prefetchHints |= PrefetchHint.IsRootLayout;
|
||||
}
|
||||
if (instantConfig && typeof instantConfig === 'object') {
|
||||
prefetchHints |= PrefetchHint.SubtreeHasInstant;
|
||||
if (instantConfig.prefetch === 'runtime') {
|
||||
prefetchHints |= PrefetchHint.HasRuntimePrefetch;
|
||||
}
|
||||
}
|
||||
// Check if this segment has a loading boundary
|
||||
if (loading) {
|
||||
prefetchHints |= PrefetchHint.SegmentHasLoadingBoundary;
|
||||
}
|
||||
const children = {};
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
var _hintTree_slots;
|
||||
// Look up the child hint node by parallel route key, traversing the
|
||||
// hint tree in parallel with the loader tree.
|
||||
const childHintNode = (hintTree == null ? void 0 : (_hintTree_slots = hintTree.slots) == null ? void 0 : _hintTree_slots[parallelRouteKey]) ?? null;
|
||||
const child = await createFlightRouterStateFromLoaderTreeImpl(parallelRoutes[parallelRouteKey], childHintNode, getDynamicParamFromSegment, searchParams, didFindRootLayout);
|
||||
// Propagate subtree flags from children
|
||||
if (child[4] !== undefined) {
|
||||
prefetchHints |= child[4] & (PrefetchHint.SubtreeHasInstant | PrefetchHint.SubtreeHasLoadingBoundary);
|
||||
// If a child has a loading boundary (either directly or in its subtree),
|
||||
// propagate that as SubtreeHasLoadingBoundary to this segment.
|
||||
if (child[4] & (PrefetchHint.SegmentHasLoadingBoundary | PrefetchHint.SubtreeHasLoadingBoundary)) {
|
||||
prefetchHints |= PrefetchHint.SubtreeHasLoadingBoundary;
|
||||
}
|
||||
}
|
||||
children[parallelRouteKey] = child;
|
||||
}
|
||||
segmentTree[1] = children;
|
||||
if (prefetchHints !== 0) {
|
||||
segmentTree[4] = prefetchHints;
|
||||
}
|
||||
return segmentTree;
|
||||
}
|
||||
export async function createFlightRouterStateFromLoaderTree(loaderTree, hintTree, getDynamicParamFromSegment, searchParams) {
|
||||
const didFindRootLayout = false;
|
||||
return createFlightRouterStateFromLoaderTreeImpl(loaderTree, hintTree, getDynamicParamFromSegment, searchParams, didFindRootLayout);
|
||||
}
|
||||
export async function createRouteTreePrefetch(loaderTree, hintTree, getDynamicParamFromSegment) {
|
||||
// Search params should not be added to page segment's cache key during a
|
||||
// route tree prefetch request, because they do not affect the structure of
|
||||
// the route. The client cache has its own logic to handle search params.
|
||||
const searchParams = {};
|
||||
const didFindRootLayout = false;
|
||||
return createFlightRouterStateFromLoaderTreeImpl(loaderTree, hintTree, getDynamicParamFromSegment, searchParams, didFindRootLayout);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-flight-router-state-from-loader-tree.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+76
@@ -0,0 +1,76 @@
|
||||
// micromatch is only available at node runtime, so it cannot be used here since the code path that calls this function
|
||||
// can be run from edge. This is a simple implementation that safely achieves the required functionality.
|
||||
// the goal is to match the functionality for remotePatterns as defined here -
|
||||
// https://nextjs.org/docs/app/api-reference/components/image#remotepatterns
|
||||
// TODO - retrofit micromatch to work in edge and use that instead
|
||||
function matchWildcardDomain(domain, pattern) {
|
||||
// DNS names are case-insensitive per RFC 1035
|
||||
// Use ASCII-only toLowerCase to avoid unicode issues
|
||||
const normalizedDomain = domain.replace(/[A-Z]/g, (c)=>c.toLowerCase());
|
||||
const normalizedPattern = pattern.replace(/[A-Z]/g, (c)=>c.toLowerCase());
|
||||
const domainParts = normalizedDomain.split('.');
|
||||
const patternParts = normalizedPattern.split('.');
|
||||
if (patternParts.length < 1) {
|
||||
// pattern is empty and therefore invalid to match against
|
||||
return false;
|
||||
}
|
||||
if (domainParts.length < patternParts.length) {
|
||||
// domain has too few segments and thus cannot match
|
||||
return false;
|
||||
}
|
||||
// Prevent wildcards from matching entire domains (e.g. '**' or '*.com')
|
||||
// This ensures wildcards can only match subdomains, not the main domain
|
||||
if (patternParts.length === 1 && (patternParts[0] === '*' || patternParts[0] === '**')) {
|
||||
return false;
|
||||
}
|
||||
while(patternParts.length){
|
||||
const patternPart = patternParts.pop();
|
||||
const domainPart = domainParts.pop();
|
||||
switch(patternPart){
|
||||
case '':
|
||||
{
|
||||
// invalid pattern. pattern segments must be non empty
|
||||
return false;
|
||||
}
|
||||
case '*':
|
||||
{
|
||||
// wildcard matches anything so we continue if the domain part is non-empty
|
||||
if (domainPart) {
|
||||
continue;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case '**':
|
||||
{
|
||||
// if this is not the last item in the pattern the pattern is invalid
|
||||
if (patternParts.length > 0) {
|
||||
return false;
|
||||
}
|
||||
// recursive wildcard matches anything so we terminate here if the domain part is non empty
|
||||
return domainPart !== undefined;
|
||||
}
|
||||
case undefined:
|
||||
default:
|
||||
{
|
||||
if (domainPart !== patternPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We exhausted the pattern. If we also exhausted the domain we have a match
|
||||
return domainParts.length === 0;
|
||||
}
|
||||
export const isCsrfOriginAllowed = (originDomain, allowedOrigins = [])=>{
|
||||
// DNS names are case-insensitive per RFC 1035
|
||||
// Use ASCII-only toLowerCase to avoid unicode issues
|
||||
const normalizedOrigin = originDomain.replace(/[A-Z]/g, (c)=>c.toLowerCase());
|
||||
return allowedOrigins.some((allowedOrigin)=>{
|
||||
if (!allowedOrigin) return false;
|
||||
const normalizedAllowed = allowedOrigin.replace(/[A-Z]/g, (c)=>c.toLowerCase());
|
||||
return normalizedAllowed === normalizedOrigin || matchWildcardDomain(originDomain, allowedOrigin);
|
||||
});
|
||||
};
|
||||
|
||||
//# sourceMappingURL=csrf-protection.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/csrf-protection.ts"],"sourcesContent":["// micromatch is only available at node runtime, so it cannot be used here since the code path that calls this function\n// can be run from edge. This is a simple implementation that safely achieves the required functionality.\n// the goal is to match the functionality for remotePatterns as defined here -\n// https://nextjs.org/docs/app/api-reference/components/image#remotepatterns\n// TODO - retrofit micromatch to work in edge and use that instead\nfunction matchWildcardDomain(domain: string, pattern: string) {\n // DNS names are case-insensitive per RFC 1035\n // Use ASCII-only toLowerCase to avoid unicode issues\n const normalizedDomain = domain.replace(/[A-Z]/g, (c) => c.toLowerCase())\n const normalizedPattern = pattern.replace(/[A-Z]/g, (c) => c.toLowerCase())\n\n const domainParts = normalizedDomain.split('.')\n const patternParts = normalizedPattern.split('.')\n\n if (patternParts.length < 1) {\n // pattern is empty and therefore invalid to match against\n return false\n }\n\n if (domainParts.length < patternParts.length) {\n // domain has too few segments and thus cannot match\n return false\n }\n\n // Prevent wildcards from matching entire domains (e.g. '**' or '*.com')\n // This ensures wildcards can only match subdomains, not the main domain\n if (\n patternParts.length === 1 &&\n (patternParts[0] === '*' || patternParts[0] === '**')\n ) {\n return false\n }\n\n while (patternParts.length) {\n const patternPart = patternParts.pop()\n const domainPart = domainParts.pop()\n\n switch (patternPart) {\n case '': {\n // invalid pattern. pattern segments must be non empty\n return false\n }\n case '*': {\n // wildcard matches anything so we continue if the domain part is non-empty\n if (domainPart) {\n continue\n } else {\n return false\n }\n }\n case '**': {\n // if this is not the last item in the pattern the pattern is invalid\n if (patternParts.length > 0) {\n return false\n }\n // recursive wildcard matches anything so we terminate here if the domain part is non empty\n return domainPart !== undefined\n }\n case undefined:\n default: {\n if (domainPart !== patternPart) {\n return false\n }\n }\n }\n }\n\n // We exhausted the pattern. If we also exhausted the domain we have a match\n return domainParts.length === 0\n}\n\nexport const isCsrfOriginAllowed = (\n originDomain: string,\n allowedOrigins: string[] = []\n): boolean => {\n // DNS names are case-insensitive per RFC 1035\n // Use ASCII-only toLowerCase to avoid unicode issues\n const normalizedOrigin = originDomain.replace(/[A-Z]/g, (c) =>\n c.toLowerCase()\n )\n\n return allowedOrigins.some((allowedOrigin) => {\n if (!allowedOrigin) return false\n\n const normalizedAllowed = allowedOrigin.replace(/[A-Z]/g, (c) =>\n c.toLowerCase()\n )\n\n return (\n normalizedAllowed === normalizedOrigin ||\n matchWildcardDomain(originDomain, allowedOrigin)\n )\n })\n}\n"],"names":["matchWildcardDomain","domain","pattern","normalizedDomain","replace","c","toLowerCase","normalizedPattern","domainParts","split","patternParts","length","patternPart","pop","domainPart","undefined","isCsrfOriginAllowed","originDomain","allowedOrigins","normalizedOrigin","some","allowedOrigin","normalizedAllowed"],"mappings":"AAAA,uHAAuH;AACvH,yGAAyG;AACzG,8EAA8E;AAC9E,4EAA4E;AAC5E,kEAAkE;AAClE,SAASA,oBAAoBC,MAAc,EAAEC,OAAe;IAC1D,8CAA8C;IAC9C,qDAAqD;IACrD,MAAMC,mBAAmBF,OAAOG,OAAO,CAAC,UAAU,CAACC,IAAMA,EAAEC,WAAW;IACtE,MAAMC,oBAAoBL,QAAQE,OAAO,CAAC,UAAU,CAACC,IAAMA,EAAEC,WAAW;IAExE,MAAME,cAAcL,iBAAiBM,KAAK,CAAC;IAC3C,MAAMC,eAAeH,kBAAkBE,KAAK,CAAC;IAE7C,IAAIC,aAAaC,MAAM,GAAG,GAAG;QAC3B,0DAA0D;QAC1D,OAAO;IACT;IAEA,IAAIH,YAAYG,MAAM,GAAGD,aAAaC,MAAM,EAAE;QAC5C,oDAAoD;QACpD,OAAO;IACT;IAEA,wEAAwE;IACxE,wEAAwE;IACxE,IACED,aAAaC,MAAM,KAAK,KACvBD,CAAAA,YAAY,CAAC,EAAE,KAAK,OAAOA,YAAY,CAAC,EAAE,KAAK,IAAG,GACnD;QACA,OAAO;IACT;IAEA,MAAOA,aAAaC,MAAM,CAAE;QAC1B,MAAMC,cAAcF,aAAaG,GAAG;QACpC,MAAMC,aAAaN,YAAYK,GAAG;QAElC,OAAQD;YACN,KAAK;gBAAI;oBACP,sDAAsD;oBACtD,OAAO;gBACT;YACA,KAAK;gBAAK;oBACR,2EAA2E;oBAC3E,IAAIE,YAAY;wBACd;oBACF,OAAO;wBACL,OAAO;oBACT;gBACF;YACA,KAAK;gBAAM;oBACT,qEAAqE;oBACrE,IAAIJ,aAAaC,MAAM,GAAG,GAAG;wBAC3B,OAAO;oBACT;oBACA,2FAA2F;oBAC3F,OAAOG,eAAeC;gBACxB;YACA,KAAKA;YACL;gBAAS;oBACP,IAAID,eAAeF,aAAa;wBAC9B,OAAO;oBACT;gBACF;QACF;IACF;IAEA,4EAA4E;IAC5E,OAAOJ,YAAYG,MAAM,KAAK;AAChC;AAEA,OAAO,MAAMK,sBAAsB,CACjCC,cACAC,iBAA2B,EAAE;IAE7B,8CAA8C;IAC9C,qDAAqD;IACrD,MAAMC,mBAAmBF,aAAab,OAAO,CAAC,UAAU,CAACC,IACvDA,EAAEC,WAAW;IAGf,OAAOY,eAAeE,IAAI,CAAC,CAACC;QAC1B,IAAI,CAACA,eAAe,OAAO;QAE3B,MAAMC,oBAAoBD,cAAcjB,OAAO,CAAC,UAAU,CAACC,IACzDA,EAAEC,WAAW;QAGf,OACEgB,sBAAsBH,oBACtBnB,oBAAoBiB,cAAcI;IAEtC;AACF,EAAC","ignoreList":[0]}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Compile-time switcher for debug channel operations.
|
||||
*
|
||||
* Simple re-export from the web implementation.
|
||||
* A future change will add a conditional branch for node streams.
|
||||
*/ export { createDebugChannel, toNodeDebugChannel } from './debug-channel-server.web';
|
||||
|
||||
//# sourceMappingURL=debug-channel-server.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/debug-channel-server.ts"],"sourcesContent":["/**\n * Compile-time switcher for debug channel operations.\n *\n * Simple re-export from the web implementation.\n * A future change will add a conditional branch for node streams.\n */\nexport type {\n DebugChannelPair,\n DebugChannelServer,\n} from './debug-channel-server.web'\n\nexport {\n createDebugChannel,\n toNodeDebugChannel,\n} from './debug-channel-server.web'\n"],"names":["createDebugChannel","toNodeDebugChannel"],"mappings":"AAAA;;;;;CAKC,GAMD,SACEA,kBAAkB,EAClBC,kBAAkB,QACb,6BAA4B","ignoreList":[0]}
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Web debug channel implementation.
|
||||
* Loaded by debug-channel-server.ts.
|
||||
*/ // Types defined inline for now; will move to debug-channel-server.node.ts later.
|
||||
export function createDebugChannel() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return undefined;
|
||||
}
|
||||
return createWebDebugChannel();
|
||||
}
|
||||
export function createWebDebugChannel() {
|
||||
let readableController;
|
||||
const clientSideReadable = new ReadableStream({
|
||||
start (controller) {
|
||||
readableController = controller;
|
||||
}
|
||||
});
|
||||
return {
|
||||
serverSide: {
|
||||
writable: new WritableStream({
|
||||
write (chunk) {
|
||||
readableController == null ? void 0 : readableController.enqueue(chunk);
|
||||
},
|
||||
close () {
|
||||
readableController == null ? void 0 : readableController.close();
|
||||
},
|
||||
abort (err) {
|
||||
readableController == null ? void 0 : readableController.error(err);
|
||||
}
|
||||
})
|
||||
},
|
||||
clientSide: {
|
||||
readable: clientSideReadable
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* toNodeDebugChannel is a no-op stub on the web path.
|
||||
* It should never be called in edge/web builds.
|
||||
*/ export function toNodeDebugChannel(_webDebugChannel) {
|
||||
throw Object.defineProperty(new Error('toNodeDebugChannel cannot be used in edge/web runtime, this is a bug in the Next.js codebase'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1071",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=debug-channel-server.web.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/debug-channel-server.web.ts"],"sourcesContent":["/**\n * Web debug channel implementation.\n * Loaded by debug-channel-server.ts.\n */\n\n// Types defined inline for now; will move to debug-channel-server.node.ts later.\nexport type DebugChannelPair = {\n serverSide: DebugChannelServer\n clientSide: DebugChannelClient\n}\n\nexport type DebugChannelServer = {\n readable?: ReadableStream<Uint8Array>\n writable: WritableStream<Uint8Array>\n}\n\ntype DebugChannelClient = {\n readable: ReadableStream<Uint8Array>\n writable?: WritableStream<Uint8Array>\n}\n\nexport function createDebugChannel(): DebugChannelPair | undefined {\n if (process.env.NODE_ENV === 'production') {\n return undefined\n }\n return createWebDebugChannel()\n}\n\nexport function createWebDebugChannel(): DebugChannelPair {\n let readableController: ReadableStreamDefaultController | undefined\n\n const clientSideReadable = new ReadableStream<Uint8Array>({\n start(controller) {\n readableController = controller\n },\n })\n\n return {\n serverSide: {\n writable: new WritableStream<Uint8Array>({\n write(chunk) {\n readableController?.enqueue(chunk)\n },\n close() {\n readableController?.close()\n },\n abort(err) {\n readableController?.error(err)\n },\n }),\n },\n clientSide: { readable: clientSideReadable },\n }\n}\n\n/**\n * toNodeDebugChannel is a no-op stub on the web path.\n * It should never be called in edge/web builds.\n */\nexport function toNodeDebugChannel(\n _webDebugChannel: DebugChannelServer\n): never {\n throw new Error(\n 'toNodeDebugChannel cannot be used in edge/web runtime, this is a bug in the Next.js codebase'\n )\n}\n"],"names":["createDebugChannel","process","env","NODE_ENV","undefined","createWebDebugChannel","readableController","clientSideReadable","ReadableStream","start","controller","serverSide","writable","WritableStream","write","chunk","enqueue","close","abort","err","error","clientSide","readable","toNodeDebugChannel","_webDebugChannel","Error"],"mappings":"AAAA;;;CAGC,GAED,iFAAiF;AAgBjF,OAAO,SAASA;IACd,IAAIC,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAAc;QACzC,OAAOC;IACT;IACA,OAAOC;AACT;AAEA,OAAO,SAASA;IACd,IAAIC;IAEJ,MAAMC,qBAAqB,IAAIC,eAA2B;QACxDC,OAAMC,UAAU;YACdJ,qBAAqBI;QACvB;IACF;IAEA,OAAO;QACLC,YAAY;YACVC,UAAU,IAAIC,eAA2B;gBACvCC,OAAMC,KAAK;oBACTT,sCAAAA,mBAAoBU,OAAO,CAACD;gBAC9B;gBACAE;oBACEX,sCAAAA,mBAAoBW,KAAK;gBAC3B;gBACAC,OAAMC,GAAG;oBACPb,sCAAAA,mBAAoBc,KAAK,CAACD;gBAC5B;YACF;QACF;QACAE,YAAY;YAAEC,UAAUf;QAAmB;IAC7C;AACF;AAEA;;;CAGC,GACD,OAAO,SAASgB,mBACdC,gBAAoC;IAEpC,MAAM,qBAEL,CAFK,IAAIC,MACR,iGADI,qBAAA;eAAA;oBAAA;sBAAA;IAEN;AACF","ignoreList":[0]}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { createAsyncLocalStorage } from './async-local-storage';
|
||||
export const dynamicAccessAsyncStorageInstance = createAsyncLocalStorage();
|
||||
|
||||
//# sourceMappingURL=dynamic-access-async-storage-instance.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/dynamic-access-async-storage-instance.ts"],"sourcesContent":["import { createAsyncLocalStorage } from './async-local-storage'\nimport type { DynamicAccessStorage } from './dynamic-access-async-storage.external'\n\nexport const dynamicAccessAsyncStorageInstance: DynamicAccessStorage =\n createAsyncLocalStorage()\n"],"names":["createAsyncLocalStorage","dynamicAccessAsyncStorageInstance"],"mappings":"AAAA,SAASA,uBAAuB,QAAQ,wBAAuB;AAG/D,OAAO,MAAMC,oCACXD,0BAAyB","ignoreList":[0]}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
// Share the instance module in the next-shared layer
|
||||
import { dynamicAccessAsyncStorageInstance } from './dynamic-access-async-storage-instance' with {
|
||||
'turbopack-transition': 'next-shared'
|
||||
};
|
||||
export { dynamicAccessAsyncStorageInstance as dynamicAccessAsyncStorage };
|
||||
|
||||
//# sourceMappingURL=dynamic-access-async-storage.external.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/dynamic-access-async-storage.external.ts"],"sourcesContent":["import type { AsyncLocalStorage } from 'async_hooks'\n\n// Share the instance module in the next-shared layer\nimport { dynamicAccessAsyncStorageInstance } from './dynamic-access-async-storage-instance' with { 'turbopack-transition': 'next-shared' }\n\nexport interface DynamicAccessAsyncStore {\n readonly abortController: AbortController\n}\n\nexport type DynamicAccessStorage = AsyncLocalStorage<DynamicAccessAsyncStore>\nexport { dynamicAccessAsyncStorageInstance as dynamicAccessAsyncStorage }\n"],"names":["dynamicAccessAsyncStorageInstance","dynamicAccessAsyncStorage"],"mappings":"AAEA,qDAAqD;AACrD,SAASA,iCAAiC,QAAQ,+CAA+C;IAAE,wBAAwB;AAAc,EAAC;AAO1I,SAASA,qCAAqCC,yBAAyB,GAAE","ignoreList":[0]}
|
||||
+1031
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+99
@@ -0,0 +1,99 @@
|
||||
// This file should never be bundled into application's runtime code and should
|
||||
// stay in the Next.js server.
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { getStorageDirectory } from '../cache-dir';
|
||||
import { arrayBufferToString } from './encryption-utils';
|
||||
// Keep the key in memory as it should never change during the lifetime of the server in
|
||||
// both development and production.
|
||||
let __next_encryption_key_generation_promise = null;
|
||||
const CONFIG_FILE = '.rscinfo';
|
||||
const ENCRYPTION_KEY = 'encryption.key';
|
||||
const ENCRYPTION_EXPIRE_AT = 'encryption.expire_at';
|
||||
const EXPIRATION = 1000 * 60 * 60 * 24 * 14 // 14 days
|
||||
;
|
||||
async function writeCache(distDir, configValue) {
|
||||
const cacheBaseDir = getStorageDirectory(distDir);
|
||||
if (!cacheBaseDir) return;
|
||||
const configPath = path.join(cacheBaseDir, CONFIG_FILE);
|
||||
if (!fs.existsSync(cacheBaseDir)) {
|
||||
await fs.promises.mkdir(cacheBaseDir, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
await fs.promises.writeFile(configPath, JSON.stringify({
|
||||
[ENCRYPTION_KEY]: configValue,
|
||||
[ENCRYPTION_EXPIRE_AT]: Date.now() + EXPIRATION
|
||||
}));
|
||||
}
|
||||
// This utility is used to get a key for the cache directory. If the
|
||||
// key is not present, it will generate a new one and store it in the
|
||||
// cache directory inside dist.
|
||||
// The key will also expire after a certain amount of time. Once it
|
||||
// expires, a new one will be generated.
|
||||
// During the lifetime of the server, it will be reused and never refreshed.
|
||||
async function loadOrGenerateKey(distDir, isBuild, generateKey) {
|
||||
const cacheBaseDir = getStorageDirectory(distDir);
|
||||
if (!cacheBaseDir) {
|
||||
// There's no persistent storage available. We generate a new key.
|
||||
// This also covers development time.
|
||||
return await generateKey();
|
||||
}
|
||||
const configPath = path.join(cacheBaseDir, CONFIG_FILE);
|
||||
async function hasCachedKey() {
|
||||
if (!fs.existsSync(configPath)) return false;
|
||||
try {
|
||||
const config = JSON.parse(await fs.promises.readFile(configPath, 'utf8'));
|
||||
if (!config) return false;
|
||||
if (typeof config[ENCRYPTION_KEY] !== 'string' || typeof config[ENCRYPTION_EXPIRE_AT] !== 'number') {
|
||||
return false;
|
||||
}
|
||||
// For build time, we need to rotate the key if it's expired. Otherwise
|
||||
// (next start) we have to keep the key as it is so the runtime key matches
|
||||
// the build time key.
|
||||
if (isBuild && config[ENCRYPTION_EXPIRE_AT] < Date.now()) {
|
||||
return false;
|
||||
}
|
||||
const cachedKey = config[ENCRYPTION_KEY];
|
||||
// If encryption key is provided via env, and it's not same as valid cache,
|
||||
// we should not use the cached key and respect the env key.
|
||||
if (cachedKey && process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY && cachedKey !== process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY) {
|
||||
return false;
|
||||
}
|
||||
return cachedKey;
|
||||
} catch {
|
||||
// Broken config file. We should generate a new key and overwrite it.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const maybeValidKey = await hasCachedKey();
|
||||
if (typeof maybeValidKey === 'string') {
|
||||
return maybeValidKey;
|
||||
}
|
||||
const key = await generateKey();
|
||||
await writeCache(distDir, key);
|
||||
return key;
|
||||
}
|
||||
export async function generateEncryptionKeyBase64({ isBuild, distDir }) {
|
||||
// This avoids it being generated multiple times in parallel.
|
||||
if (!__next_encryption_key_generation_promise) {
|
||||
__next_encryption_key_generation_promise = loadOrGenerateKey(distDir, isBuild, async ()=>{
|
||||
const providedKey = process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY;
|
||||
if (providedKey) {
|
||||
return providedKey;
|
||||
}
|
||||
const key = await crypto.subtle.generateKey({
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
}, true, [
|
||||
'encrypt',
|
||||
'decrypt'
|
||||
]);
|
||||
const exported = await crypto.subtle.exportKey('raw', key);
|
||||
return btoa(arrayBufferToString(exported));
|
||||
});
|
||||
}
|
||||
return __next_encryption_key_generation_promise;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=encryption-utils-server.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+59
@@ -0,0 +1,59 @@
|
||||
import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
import { getServerActionsManifest } from './manifests-singleton';
|
||||
let __next_loaded_action_key;
|
||||
export function arrayBufferToString(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
// @anonrig: V8 has a limit of 65535 arguments in a function.
|
||||
// For len < 65535, this is faster.
|
||||
// https://github.com/vercel/next.js/pull/56377#pullrequestreview-1656181623
|
||||
if (len < 65535) {
|
||||
return String.fromCharCode.apply(null, bytes);
|
||||
}
|
||||
let binary = '';
|
||||
for(let i = 0; i < len; i++){
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return binary;
|
||||
}
|
||||
export function stringToUint8Array(binary) {
|
||||
const len = binary.length;
|
||||
const arr = new Uint8Array(len);
|
||||
for(let i = 0; i < len; i++){
|
||||
arr[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
export function encrypt(key, iv, data) {
|
||||
return crypto.subtle.encrypt({
|
||||
name: 'AES-GCM',
|
||||
iv
|
||||
}, key, data);
|
||||
}
|
||||
export function decrypt(key, iv, data) {
|
||||
return crypto.subtle.decrypt({
|
||||
name: 'AES-GCM',
|
||||
iv
|
||||
}, key, data);
|
||||
}
|
||||
export async function getActionEncryptionKey() {
|
||||
if (__next_loaded_action_key) {
|
||||
return __next_loaded_action_key;
|
||||
}
|
||||
const serverActionsManifest = getServerActionsManifest();
|
||||
const rawKey = process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY || serverActionsManifest.encryptionKey;
|
||||
if (rawKey === undefined) {
|
||||
throw Object.defineProperty(new InvariantError('Missing encryption key for Server Actions'), "__NEXT_ERROR_CODE", {
|
||||
value: "E571",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
__next_loaded_action_key = await crypto.subtle.importKey('raw', stringToUint8Array(atob(rawKey)), 'AES-GCM', true, [
|
||||
'encrypt',
|
||||
'decrypt'
|
||||
]);
|
||||
return __next_loaded_action_key;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=encryption-utils.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/encryption-utils.ts"],"sourcesContent":["import { InvariantError } from '../../shared/lib/invariant-error'\nimport { getServerActionsManifest } from './manifests-singleton'\n\nlet __next_loaded_action_key: CryptoKey\n\nexport function arrayBufferToString(\n buffer: ArrayBuffer | Uint8Array<ArrayBufferLike>\n) {\n const bytes = new Uint8Array(buffer)\n const len = bytes.byteLength\n\n // @anonrig: V8 has a limit of 65535 arguments in a function.\n // For len < 65535, this is faster.\n // https://github.com/vercel/next.js/pull/56377#pullrequestreview-1656181623\n if (len < 65535) {\n return String.fromCharCode.apply(null, bytes as unknown as number[])\n }\n\n let binary = ''\n for (let i = 0; i < len; i++) {\n binary += String.fromCharCode(bytes[i])\n }\n return binary\n}\n\nexport function stringToUint8Array(binary: string) {\n const len = binary.length\n const arr = new Uint8Array(len)\n\n for (let i = 0; i < len; i++) {\n arr[i] = binary.charCodeAt(i)\n }\n\n return arr\n}\n\nexport function encrypt(\n key: CryptoKey,\n iv: Uint8Array<ArrayBuffer>,\n data: Uint8Array<ArrayBuffer>\n) {\n return crypto.subtle.encrypt(\n {\n name: 'AES-GCM',\n iv,\n },\n key,\n data\n )\n}\n\nexport function decrypt(\n key: CryptoKey,\n iv: Uint8Array<ArrayBuffer>,\n data: Uint8Array<ArrayBuffer>\n) {\n return crypto.subtle.decrypt(\n {\n name: 'AES-GCM',\n iv,\n },\n key,\n data\n )\n}\n\nexport async function getActionEncryptionKey() {\n if (__next_loaded_action_key) {\n return __next_loaded_action_key\n }\n\n const serverActionsManifest = getServerActionsManifest()\n\n const rawKey =\n process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY ||\n serverActionsManifest.encryptionKey\n\n if (rawKey === undefined) {\n throw new InvariantError('Missing encryption key for Server Actions')\n }\n\n __next_loaded_action_key = await crypto.subtle.importKey(\n 'raw',\n stringToUint8Array(atob(rawKey)),\n 'AES-GCM',\n true,\n ['encrypt', 'decrypt']\n )\n\n return __next_loaded_action_key\n}\n"],"names":["InvariantError","getServerActionsManifest","__next_loaded_action_key","arrayBufferToString","buffer","bytes","Uint8Array","len","byteLength","String","fromCharCode","apply","binary","i","stringToUint8Array","length","arr","charCodeAt","encrypt","key","iv","data","crypto","subtle","name","decrypt","getActionEncryptionKey","serverActionsManifest","rawKey","process","env","NEXT_SERVER_ACTIONS_ENCRYPTION_KEY","encryptionKey","undefined","importKey","atob"],"mappings":"AAAA,SAASA,cAAc,QAAQ,mCAAkC;AACjE,SAASC,wBAAwB,QAAQ,wBAAuB;AAEhE,IAAIC;AAEJ,OAAO,SAASC,oBACdC,MAAiD;IAEjD,MAAMC,QAAQ,IAAIC,WAAWF;IAC7B,MAAMG,MAAMF,MAAMG,UAAU;IAE5B,6DAA6D;IAC7D,mCAAmC;IACnC,4EAA4E;IAC5E,IAAID,MAAM,OAAO;QACf,OAAOE,OAAOC,YAAY,CAACC,KAAK,CAAC,MAAMN;IACzC;IAEA,IAAIO,SAAS;IACb,IAAK,IAAIC,IAAI,GAAGA,IAAIN,KAAKM,IAAK;QAC5BD,UAAUH,OAAOC,YAAY,CAACL,KAAK,CAACQ,EAAE;IACxC;IACA,OAAOD;AACT;AAEA,OAAO,SAASE,mBAAmBF,MAAc;IAC/C,MAAML,MAAMK,OAAOG,MAAM;IACzB,MAAMC,MAAM,IAAIV,WAAWC;IAE3B,IAAK,IAAIM,IAAI,GAAGA,IAAIN,KAAKM,IAAK;QAC5BG,GAAG,CAACH,EAAE,GAAGD,OAAOK,UAAU,CAACJ;IAC7B;IAEA,OAAOG;AACT;AAEA,OAAO,SAASE,QACdC,GAAc,EACdC,EAA2B,EAC3BC,IAA6B;IAE7B,OAAOC,OAAOC,MAAM,CAACL,OAAO,CAC1B;QACEM,MAAM;QACNJ;IACF,GACAD,KACAE;AAEJ;AAEA,OAAO,SAASI,QACdN,GAAc,EACdC,EAA2B,EAC3BC,IAA6B;IAE7B,OAAOC,OAAOC,MAAM,CAACE,OAAO,CAC1B;QACED,MAAM;QACNJ;IACF,GACAD,KACAE;AAEJ;AAEA,OAAO,eAAeK;IACpB,IAAIxB,0BAA0B;QAC5B,OAAOA;IACT;IAEA,MAAMyB,wBAAwB1B;IAE9B,MAAM2B,SACJC,QAAQC,GAAG,CAACC,kCAAkC,IAC9CJ,sBAAsBK,aAAa;IAErC,IAAIJ,WAAWK,WAAW;QACxB,MAAM,qBAA+D,CAA/D,IAAIjC,eAAe,8CAAnB,qBAAA;mBAAA;wBAAA;0BAAA;QAA8D;IACtE;IAEAE,2BAA2B,MAAMoB,OAAOC,MAAM,CAACW,SAAS,CACtD,OACApB,mBAAmBqB,KAAKP,UACxB,WACA,MACA;QAAC;QAAW;KAAU;IAGxB,OAAO1B;AACT","ignoreList":[0]}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */ import 'server-only';
|
||||
/* eslint-disable import/no-extraneous-dependencies */ import { renderToReadableStream } from 'react-server-dom-webpack/server';
|
||||
/* eslint-disable import/no-extraneous-dependencies */ import { createFromReadableStream } from 'react-server-dom-webpack/client';
|
||||
import { streamToString } from '../stream-utils/node-web-streams-helper';
|
||||
import { arrayBufferToString, decrypt, encrypt, getActionEncryptionKey, stringToUint8Array } from './encryption-utils';
|
||||
import { getClientReferenceManifest, getServerModuleMap } from './manifests-singleton';
|
||||
import { getCacheSignal, getPrerenderResumeDataCache, getRenderResumeDataCache, workUnitAsyncStorage } from './work-unit-async-storage.external';
|
||||
import { createHangingInputAbortSignal } from './dynamic-rendering';
|
||||
import React from 'react';
|
||||
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge';
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
const filterStackFrame = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').filterStackFrameDEV : undefined;
|
||||
const findSourceMapURL = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').findSourceMapURLDEV : undefined;
|
||||
/**
|
||||
* Decrypt the serialized string with the action id as the salt.
|
||||
*/ async function decodeActionBoundArg(actionId, arg) {
|
||||
const key = await getActionEncryptionKey();
|
||||
if (typeof key === 'undefined') {
|
||||
throw Object.defineProperty(new Error(`Missing encryption key for Server Action. This is a bug in Next.js`), "__NEXT_ERROR_CODE", {
|
||||
value: "E65",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// Get the iv (16 bytes) and the payload from the arg.
|
||||
const originalPayload = atob(arg);
|
||||
const ivValue = originalPayload.slice(0, 16);
|
||||
const payload = originalPayload.slice(16);
|
||||
const decrypted = textDecoder.decode(await decrypt(key, stringToUint8Array(ivValue), stringToUint8Array(payload)));
|
||||
if (!decrypted.startsWith(actionId)) {
|
||||
throw Object.defineProperty(new Error('Invalid Server Action payload: failed to decrypt.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E191",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return decrypted.slice(actionId.length);
|
||||
}
|
||||
/**
|
||||
* Encrypt the serialized string with the action id as the salt. Add a prefix to
|
||||
* later ensure that the payload is correctly decrypted, similar to a checksum.
|
||||
*/ async function encodeActionBoundArg(actionId, arg) {
|
||||
const key = await getActionEncryptionKey();
|
||||
if (key === undefined) {
|
||||
throw Object.defineProperty(new Error(`Missing encryption key for Server Action. This is a bug in Next.js`), "__NEXT_ERROR_CODE", {
|
||||
value: "E65",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// Get 16 random bytes as iv.
|
||||
const randomBytes = new Uint8Array(16);
|
||||
workUnitAsyncStorage.exit(()=>crypto.getRandomValues(randomBytes));
|
||||
const ivValue = arrayBufferToString(randomBytes.buffer);
|
||||
const encrypted = await encrypt(key, randomBytes, textEncoder.encode(actionId + arg));
|
||||
return btoa(ivValue + arrayBufferToString(encrypted));
|
||||
}
|
||||
var ReadStatus = /*#__PURE__*/ function(ReadStatus) {
|
||||
ReadStatus[ReadStatus["Ready"] = 0] = "Ready";
|
||||
ReadStatus[ReadStatus["Pending"] = 1] = "Pending";
|
||||
ReadStatus[ReadStatus["Complete"] = 2] = "Complete";
|
||||
return ReadStatus;
|
||||
}(ReadStatus || {});
|
||||
// Encrypts the action's bound args into a string. For the same combination of
|
||||
// actionId and args the same cached promise is returned. This ensures reference
|
||||
// equality for returned objects from "use cache" functions when they're invoked
|
||||
// multiple times within one render pass using the same bound args.
|
||||
export const encryptActionBoundArgs = React.cache(async function encryptActionBoundArgs(actionId, ...args) {
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
const cacheSignal = workUnitStore ? getCacheSignal(workUnitStore) : undefined;
|
||||
const { clientModules } = getClientReferenceManifest();
|
||||
// Create an error before any asynchronous calls, to capture the original
|
||||
// call stack in case we need it when the serialization errors.
|
||||
const error = new Error();
|
||||
Error.captureStackTrace(error, encryptActionBoundArgs);
|
||||
let didCatchError = false;
|
||||
const hangingInputAbortSignal = workUnitStore ? createHangingInputAbortSignal(workUnitStore) : undefined;
|
||||
let readStatus = 0;
|
||||
function startReadOnce() {
|
||||
if (readStatus === 0) {
|
||||
readStatus = 1;
|
||||
cacheSignal == null ? void 0 : cacheSignal.beginRead();
|
||||
}
|
||||
}
|
||||
function endReadIfStarted() {
|
||||
if (readStatus === 1) {
|
||||
cacheSignal == null ? void 0 : cacheSignal.endRead();
|
||||
}
|
||||
readStatus = 2;
|
||||
}
|
||||
// streamToString might take longer than a microtask to resolve and then other things
|
||||
// waiting on the cache signal might not realize there is another cache to fill so if
|
||||
// we are no longer waiting on the bound args serialization via the hangingInputAbortSignal
|
||||
// we should eagerly start the cache read to prevent other readers of the cache signal from
|
||||
// missing this cache fill. We use a idempotent function to only start reading once because
|
||||
// it's also possible that streamToString finishes before the hangingInputAbortSignal aborts.
|
||||
if (hangingInputAbortSignal && cacheSignal) {
|
||||
hangingInputAbortSignal.addEventListener('abort', startReadOnce, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
const prerenderResumeDataCache = workUnitStore ? getPrerenderResumeDataCache(workUnitStore) : null;
|
||||
const renderResumeDataCache = workUnitStore ? getRenderResumeDataCache(workUnitStore) : null;
|
||||
// Using Flight to serialize the args into a string.
|
||||
const serialized = await streamToString(renderToReadableStream(args, clientModules, {
|
||||
filterStackFrame,
|
||||
signal: hangingInputAbortSignal,
|
||||
debugChannel: // In Cache Components, we want to cache the encrypted result,
|
||||
// and we use the unencrypted bound args as a cache key.
|
||||
// In order to do that we need to strip debug info, because it
|
||||
// contains timing information and thus changes each time we serialize the args.
|
||||
// We can do this by piping debug info into a debug channel that throws it away.
|
||||
//
|
||||
// Note that this can result in dangling debug info references when we decode the bound args,
|
||||
// but React ignores those as long as no debug channel is passed on the decode side, so it's fine:
|
||||
// https://github.com/facebook/react/blob/bb8a76c6cc77ea2976d690ea09f5a1b3d9b1792a/packages/react-client/src/ReactFlightClient.js#L1711-L1729
|
||||
// https://github.com/facebook/react/blob/bb8a76c6cc77ea2976d690ea09f5a1b3d9b1792a/packages/react-client/src/ReactFlightClient.js#L4005-L4025
|
||||
process.env.NODE_ENV === 'development' && (prerenderResumeDataCache || renderResumeDataCache) ? {
|
||||
writable: new WritableStream()
|
||||
} : undefined,
|
||||
onError (err) {
|
||||
if (hangingInputAbortSignal == null ? void 0 : hangingInputAbortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
// We're only reporting one error at a time, starting with the first.
|
||||
if (didCatchError) {
|
||||
return;
|
||||
}
|
||||
didCatchError = true;
|
||||
// Use the original error message together with the previously created
|
||||
// stack, because err.stack is a useless Flight Server call stack.
|
||||
error.message = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}), // We pass the abort signal to `streamToString` so that no chunks are
|
||||
// included that are emitted after the signal was already aborted. This
|
||||
// ensures that we can encode hanging promises.
|
||||
hangingInputAbortSignal);
|
||||
if (didCatchError) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Logging the error is needed for server functions that are passed to the
|
||||
// client where the decryption is not done during rendering. Console
|
||||
// replaying allows us to still show the error dev overlay in this case.
|
||||
console.error(error);
|
||||
}
|
||||
endReadIfStarted();
|
||||
throw error;
|
||||
}
|
||||
if (!workUnitStore) {
|
||||
// We don't need to call cacheSignal.endRead here because we can't have a cacheSignal
|
||||
// if we do not have a workUnitStore.
|
||||
return encodeActionBoundArg(actionId, serialized);
|
||||
}
|
||||
startReadOnce();
|
||||
const cacheKey = actionId + serialized;
|
||||
const cachedEncrypted = (prerenderResumeDataCache == null ? void 0 : prerenderResumeDataCache.encryptedBoundArgs.get(cacheKey)) ?? (renderResumeDataCache == null ? void 0 : renderResumeDataCache.encryptedBoundArgs.get(cacheKey));
|
||||
if (cachedEncrypted) {
|
||||
return cachedEncrypted;
|
||||
}
|
||||
const encrypted = await encodeActionBoundArg(actionId, serialized);
|
||||
endReadIfStarted();
|
||||
prerenderResumeDataCache == null ? void 0 : prerenderResumeDataCache.encryptedBoundArgs.set(cacheKey, encrypted);
|
||||
return encrypted;
|
||||
});
|
||||
// Decrypts the action's bound args from the encrypted string.
|
||||
export async function decryptActionBoundArgs(actionId, encryptedPromise) {
|
||||
const encrypted = await encryptedPromise;
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
let decrypted;
|
||||
if (workUnitStore) {
|
||||
const cacheSignal = getCacheSignal(workUnitStore);
|
||||
const prerenderResumeDataCache = getPrerenderResumeDataCache(workUnitStore);
|
||||
const renderResumeDataCache = getRenderResumeDataCache(workUnitStore);
|
||||
decrypted = (prerenderResumeDataCache == null ? void 0 : prerenderResumeDataCache.decryptedBoundArgs.get(encrypted)) ?? (renderResumeDataCache == null ? void 0 : renderResumeDataCache.decryptedBoundArgs.get(encrypted));
|
||||
if (!decrypted) {
|
||||
cacheSignal == null ? void 0 : cacheSignal.beginRead();
|
||||
decrypted = await decodeActionBoundArg(actionId, encrypted);
|
||||
cacheSignal == null ? void 0 : cacheSignal.endRead();
|
||||
prerenderResumeDataCache == null ? void 0 : prerenderResumeDataCache.decryptedBoundArgs.set(encrypted, decrypted);
|
||||
}
|
||||
} else {
|
||||
decrypted = await decodeActionBoundArg(actionId, encrypted);
|
||||
}
|
||||
const { edgeRscModuleMapping, rscModuleMapping } = getClientReferenceManifest();
|
||||
// Using Flight to deserialize the args from the string.
|
||||
const deserialized = await createFromReadableStream(new ReadableStream({
|
||||
start (controller) {
|
||||
controller.enqueue(textEncoder.encode(decrypted));
|
||||
switch(workUnitStore == null ? void 0 : workUnitStore.type){
|
||||
case 'prerender':
|
||||
case 'prerender-runtime':
|
||||
// Explicitly don't close the stream here (until prerendering is
|
||||
// complete) so that hanging promises are not rejected.
|
||||
if (workUnitStore.renderSignal.aborted) {
|
||||
controller.close();
|
||||
} else {
|
||||
workUnitStore.renderSignal.addEventListener('abort', ()=>controller.close(), {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'prerender-client':
|
||||
case 'validation-client':
|
||||
case 'prerender-ppr':
|
||||
case 'prerender-legacy':
|
||||
case 'request':
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
case undefined:
|
||||
return controller.close();
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
}), {
|
||||
findSourceMapURL,
|
||||
// NOTE: When we serialized the bound args, we may have used a dummy debug channel to strip debug info.
|
||||
// In that case, it's important that we also *don't* pass a debug channel here, because that will make
|
||||
// the Flight Client ignore the dangling references:
|
||||
// https://github.com/facebook/react/blob/bb8a76c6cc77ea2976d690ea09f5a1b3d9b1792a/packages/react-client/src/ReactFlightClient.js#L1711-L1729
|
||||
// https://github.com/facebook/react/blob/bb8a76c6cc77ea2976d690ea09f5a1b3d9b1792a/packages/react-client/src/ReactFlightClient.js#L4005-L4025
|
||||
debugChannel: undefined,
|
||||
serverConsumerManifest: {
|
||||
// moduleLoading must be null because we don't want to trigger preloads of ClientReferences
|
||||
// to be added to the current execution. Instead, we'll wait for any ClientReference
|
||||
// to be emitted which themselves will handle the preloading.
|
||||
moduleLoading: null,
|
||||
moduleMap: isEdgeRuntime ? edgeRscModuleMapping : rscModuleMapping,
|
||||
serverModuleMap: getServerModuleMap()
|
||||
}
|
||||
});
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=encryption.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+60
@@ -0,0 +1,60 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
export { createTemporaryReferenceSet, renderToReadableStream, decodeReply, decodeAction, decodeFormState } from 'react-server-dom-webpack/server';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
export { prerender } from 'react-server-dom-webpack/static';
|
||||
// TODO: Just re-export `* as ReactServer`
|
||||
export { captureOwnerStack, createElement, Fragment } from 'react';
|
||||
export { default as LayoutRouter, LoadingBoundaryProvider } from '../../client/components/layout-router';
|
||||
export { default as RenderFromTemplateContext } from '../../client/components/render-from-template-context';
|
||||
export { workAsyncStorage } from '../app-render/work-async-storage.external';
|
||||
export { workUnitAsyncStorage } from './work-unit-async-storage.external';
|
||||
export { actionAsyncStorage } from '../app-render/action-async-storage.external';
|
||||
export { ClientPageRoot } from '../../client/components/client-page';
|
||||
export { ClientSegmentRoot } from '../../client/components/client-segment';
|
||||
export { createServerSearchParamsForServerPage, createPrerenderSearchParamsForClientPage } from '../request/search-params';
|
||||
export { createServerParamsForServerSegment, createPrerenderParamsForClientSegment } from '../request/params';
|
||||
export * as serverHooks from '../../client/components/hooks-server-context';
|
||||
export { HTTPAccessFallbackBoundary } from '../../client/components/http-access-fallback/error-boundary';
|
||||
export { createMetadataComponents } from '../../lib/metadata/metadata';
|
||||
export { RootLayoutBoundary } from '../../lib/framework/boundary-components';
|
||||
export { preloadStyle, preloadFont, preconnect } from './rsc/preloads';
|
||||
export { Postpone } from './rsc/postpone';
|
||||
export { taintObjectReference } from './rsc/taint';
|
||||
export { collectSegmentData, collectPrefetchHints } from './collect-segment-data';
|
||||
export const InstantValidation = ()=>{
|
||||
if (process.env.NEXT_RUNTIME !== 'edge' && process.env.__NEXT_CACHE_COMPONENTS) {
|
||||
return require('./instant-validation/instant-validation');
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
import { workAsyncStorage } from '../app-render/work-async-storage.external';
|
||||
import { workUnitAsyncStorage } from './work-unit-async-storage.external';
|
||||
import { patchFetch as _patchFetch } from '../lib/patch-fetch';
|
||||
let SegmentViewNode = ()=>null;
|
||||
let SegmentViewStateNode = ()=>null;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const mod = require('../../next-devtools/userspace/app/segment-explorer-node');
|
||||
SegmentViewNode = mod.SegmentViewNode;
|
||||
SegmentViewStateNode = mod.SegmentViewStateNode;
|
||||
}
|
||||
// hot-reloader modules are not bundled so we need to inject `__next__clear_chunk_cache__`
|
||||
// into globalThis from this file which is bundled.
|
||||
if (process.env.TURBOPACK) {
|
||||
globalThis.__next__clear_chunk_cache__ = __turbopack_clear_chunk_cache__;
|
||||
} else {
|
||||
// Webpack does not have chunks on the server
|
||||
globalThis.__next__clear_chunk_cache__ = null;
|
||||
}
|
||||
// patchFetch makes use of APIs such as `React.unstable_postpone` which are only available
|
||||
// in the experimental channel of React, so export it from here so that it comes from the bundled runtime
|
||||
export function patchFetch() {
|
||||
return _patchFetch({
|
||||
workAsyncStorage,
|
||||
workUnitAsyncStorage
|
||||
});
|
||||
}
|
||||
// Development only
|
||||
export { SegmentViewNode, SegmentViewStateNode };
|
||||
|
||||
//# sourceMappingURL=entry-base.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+15
@@ -0,0 +1,15 @@
|
||||
import { RSC_CONTENT_TYPE_HEADER } from '../../client/components/app-router-headers';
|
||||
import RenderResult from '../render-result';
|
||||
/**
|
||||
* Flight Response is always set to RSC_CONTENT_TYPE_HEADER to ensure it does not get interpreted as HTML.
|
||||
*/ export class FlightRenderResult extends RenderResult {
|
||||
constructor(response, metadata = {}, waitUntil){
|
||||
super(response, {
|
||||
contentType: RSC_CONTENT_TYPE_HEADER,
|
||||
metadata,
|
||||
waitUntil
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=flight-render-result.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/flight-render-result.ts"],"sourcesContent":["import { RSC_CONTENT_TYPE_HEADER } from '../../client/components/app-router-headers'\nimport RenderResult, { type RenderResultMetadata } from '../render-result'\n\n/**\n * Flight Response is always set to RSC_CONTENT_TYPE_HEADER to ensure it does not get interpreted as HTML.\n */\nexport class FlightRenderResult extends RenderResult {\n constructor(\n response: string | ReadableStream<Uint8Array>,\n metadata: RenderResultMetadata = {},\n waitUntil?: Promise<unknown>\n ) {\n super(response, {\n contentType: RSC_CONTENT_TYPE_HEADER,\n metadata,\n waitUntil,\n })\n }\n}\n"],"names":["RSC_CONTENT_TYPE_HEADER","RenderResult","FlightRenderResult","constructor","response","metadata","waitUntil","contentType"],"mappings":"AAAA,SAASA,uBAAuB,QAAQ,6CAA4C;AACpF,OAAOC,kBAAiD,mBAAkB;AAE1E;;CAEC,GACD,OAAO,MAAMC,2BAA2BD;IACtCE,YACEC,QAA6C,EAC7CC,WAAiC,CAAC,CAAC,EACnCC,SAA4B,CAC5B;QACA,KAAK,CAACF,UAAU;YACdG,aAAaP;YACbK;YACAC;QACF;IACF;AACF","ignoreList":[0]}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const isTurbopack = !!process.env.TURBOPACK;
|
||||
export function getAssetQueryString(ctx, addTimestamp) {
|
||||
let qs = '';
|
||||
// In development we add the request timestamp to allow react to
|
||||
// reload assets when a new RSC response is received.
|
||||
// Turbopack handles HMR of assets itself and react doesn't need to reload them
|
||||
// so this approach is not needed for Turbopack.
|
||||
const shouldAddVersion = isDev && !isTurbopack && addTimestamp;
|
||||
if (shouldAddVersion) {
|
||||
qs += `?v=${ctx.requestTimestamp}`;
|
||||
}
|
||||
if (ctx.sharedContext.clientAssetToken) {
|
||||
qs += `${shouldAddVersion ? '&' : '?'}dpl=${ctx.sharedContext.clientAssetToken}`;
|
||||
}
|
||||
return qs;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=get-asset-query-string.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/get-asset-query-string.ts"],"sourcesContent":["import type { AppRenderContext } from './app-render'\n\nconst isDev = process.env.NODE_ENV === 'development'\nconst isTurbopack = !!process.env.TURBOPACK\n\nexport function getAssetQueryString(\n ctx: AppRenderContext,\n addTimestamp: boolean\n) {\n let qs = ''\n\n // In development we add the request timestamp to allow react to\n // reload assets when a new RSC response is received.\n // Turbopack handles HMR of assets itself and react doesn't need to reload them\n // so this approach is not needed for Turbopack.\n const shouldAddVersion = isDev && !isTurbopack && addTimestamp\n if (shouldAddVersion) {\n qs += `?v=${ctx.requestTimestamp}`\n }\n\n if (ctx.sharedContext.clientAssetToken) {\n qs += `${shouldAddVersion ? '&' : '?'}dpl=${ctx.sharedContext.clientAssetToken}`\n }\n return qs\n}\n"],"names":["isDev","process","env","NODE_ENV","isTurbopack","TURBOPACK","getAssetQueryString","ctx","addTimestamp","qs","shouldAddVersion","requestTimestamp","sharedContext","clientAssetToken"],"mappings":"AAEA,MAAMA,QAAQC,QAAQC,GAAG,CAACC,QAAQ,KAAK;AACvC,MAAMC,cAAc,CAAC,CAACH,QAAQC,GAAG,CAACG,SAAS;AAE3C,OAAO,SAASC,oBACdC,GAAqB,EACrBC,YAAqB;IAErB,IAAIC,KAAK;IAET,gEAAgE;IAChE,qDAAqD;IACrD,+EAA+E;IAC/E,gDAAgD;IAChD,MAAMC,mBAAmBV,SAAS,CAACI,eAAeI;IAClD,IAAIE,kBAAkB;QACpBD,MAAM,CAAC,GAAG,EAAEF,IAAII,gBAAgB,EAAE;IACpC;IAEA,IAAIJ,IAAIK,aAAa,CAACC,gBAAgB,EAAE;QACtCJ,MAAM,GAAGC,mBAAmB,MAAM,IAAI,IAAI,EAAEH,IAAIK,aAAa,CAACC,gBAAgB,EAAE;IAClF;IACA,OAAOJ;AACT","ignoreList":[0]}
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import { getClientReferenceManifest } from './manifests-singleton';
|
||||
/**
|
||||
* Get external stylesheet link hrefs based on server CSS manifest.
|
||||
*/ export function getLinkAndScriptTags(filePath, injectedCSS, injectedScripts, collectNewImports) {
|
||||
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, '');
|
||||
const cssChunks = new Set();
|
||||
const jsChunks = new Set();
|
||||
const { entryCSSFiles, entryJSFiles } = getClientReferenceManifest();
|
||||
const cssFiles = entryCSSFiles[filePathWithoutExt];
|
||||
const jsFiles = entryJSFiles == null ? void 0 : entryJSFiles[filePathWithoutExt];
|
||||
if (cssFiles) {
|
||||
for (const css of cssFiles){
|
||||
if (!injectedCSS.has(css.path)) {
|
||||
if (collectNewImports) {
|
||||
injectedCSS.add(css.path);
|
||||
}
|
||||
cssChunks.add(css);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (jsFiles) {
|
||||
for (const file of jsFiles){
|
||||
if (!injectedScripts.has(file)) {
|
||||
if (collectNewImports) {
|
||||
injectedScripts.add(file);
|
||||
}
|
||||
jsChunks.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
styles: [
|
||||
...cssChunks
|
||||
],
|
||||
scripts: [
|
||||
...jsChunks
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=get-css-inlined-link-tags.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/get-css-inlined-link-tags.tsx"],"sourcesContent":["import type { CssResource } from '../../build/webpack/plugins/flight-manifest-plugin'\nimport { getClientReferenceManifest } from './manifests-singleton'\n\n/**\n * Get external stylesheet link hrefs based on server CSS manifest.\n */\nexport function getLinkAndScriptTags(\n filePath: string,\n injectedCSS: Set<string>,\n injectedScripts: Set<string>,\n collectNewImports?: boolean\n): { styles: CssResource[]; scripts: string[] } {\n const filePathWithoutExt = filePath.replace(/\\.[^.]+$/, '')\n const cssChunks = new Set<CssResource>()\n const jsChunks = new Set<string>()\n const { entryCSSFiles, entryJSFiles } = getClientReferenceManifest()\n const cssFiles = entryCSSFiles[filePathWithoutExt]\n const jsFiles = entryJSFiles?.[filePathWithoutExt]\n\n if (cssFiles) {\n for (const css of cssFiles) {\n if (!injectedCSS.has(css.path)) {\n if (collectNewImports) {\n injectedCSS.add(css.path)\n }\n cssChunks.add(css)\n }\n }\n }\n\n if (jsFiles) {\n for (const file of jsFiles) {\n if (!injectedScripts.has(file)) {\n if (collectNewImports) {\n injectedScripts.add(file)\n }\n jsChunks.add(file)\n }\n }\n }\n\n return { styles: [...cssChunks], scripts: [...jsChunks] }\n}\n"],"names":["getClientReferenceManifest","getLinkAndScriptTags","filePath","injectedCSS","injectedScripts","collectNewImports","filePathWithoutExt","replace","cssChunks","Set","jsChunks","entryCSSFiles","entryJSFiles","cssFiles","jsFiles","css","has","path","add","file","styles","scripts"],"mappings":"AACA,SAASA,0BAA0B,QAAQ,wBAAuB;AAElE;;CAEC,GACD,OAAO,SAASC,qBACdC,QAAgB,EAChBC,WAAwB,EACxBC,eAA4B,EAC5BC,iBAA2B;IAE3B,MAAMC,qBAAqBJ,SAASK,OAAO,CAAC,YAAY;IACxD,MAAMC,YAAY,IAAIC;IACtB,MAAMC,WAAW,IAAID;IACrB,MAAM,EAAEE,aAAa,EAAEC,YAAY,EAAE,GAAGZ;IACxC,MAAMa,WAAWF,aAAa,CAACL,mBAAmB;IAClD,MAAMQ,UAAUF,gCAAAA,YAAc,CAACN,mBAAmB;IAElD,IAAIO,UAAU;QACZ,KAAK,MAAME,OAAOF,SAAU;YAC1B,IAAI,CAACV,YAAYa,GAAG,CAACD,IAAIE,IAAI,GAAG;gBAC9B,IAAIZ,mBAAmB;oBACrBF,YAAYe,GAAG,CAACH,IAAIE,IAAI;gBAC1B;gBACAT,UAAUU,GAAG,CAACH;YAChB;QACF;IACF;IAEA,IAAID,SAAS;QACX,KAAK,MAAMK,QAAQL,QAAS;YAC1B,IAAI,CAACV,gBAAgBY,GAAG,CAACG,OAAO;gBAC9B,IAAId,mBAAmB;oBACrBD,gBAAgBc,GAAG,CAACC;gBACtB;gBACAT,SAASQ,GAAG,CAACC;YACf;QACF;IACF;IAEA,OAAO;QAAEC,QAAQ;eAAIZ;SAAU;QAAEa,SAAS;eAAIX;SAAS;IAAC;AAC1D","ignoreList":[0]}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { getLinkAndScriptTags } from './get-css-inlined-link-tags';
|
||||
import { getPreloadableFonts } from './get-preloadable-fonts';
|
||||
import { getAssetQueryString } from './get-asset-query-string';
|
||||
import { encodeURIPath } from '../../shared/lib/encode-uri-path';
|
||||
import { renderCssResource } from './render-css-resource';
|
||||
export function getLayerAssets({ ctx, layoutOrPagePath, injectedCSS: injectedCSSWithCurrentLayout, injectedJS: injectedJSWithCurrentLayout, injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, preloadCallbacks }) {
|
||||
const { componentMod: { createElement } } = ctx;
|
||||
const { styles: styleTags, scripts: scriptTags } = layoutOrPagePath ? getLinkAndScriptTags(layoutOrPagePath, injectedCSSWithCurrentLayout, injectedJSWithCurrentLayout, true) : {
|
||||
styles: [],
|
||||
scripts: []
|
||||
};
|
||||
const preloadedFontFiles = layoutOrPagePath ? getPreloadableFonts(ctx.renderOpts.nextFontManifest, layoutOrPagePath, injectedFontPreloadTagsWithCurrentLayout) : null;
|
||||
if (preloadedFontFiles) {
|
||||
if (preloadedFontFiles.length) {
|
||||
for(let i = 0; i < preloadedFontFiles.length; i++){
|
||||
const fontFilename = preloadedFontFiles[i];
|
||||
const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFilename)[1];
|
||||
const type = `font/${ext}`;
|
||||
const href = `${ctx.assetPrefix}/_next/${encodeURIPath(fontFilename)}${getAssetQueryString(ctx, true)}`;
|
||||
preloadCallbacks.push(()=>{
|
||||
ctx.componentMod.preloadFont(href, type, ctx.renderOpts.crossOrigin, ctx.nonce);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let url = new URL(ctx.assetPrefix);
|
||||
preloadCallbacks.push(()=>{
|
||||
ctx.componentMod.preconnect(url.origin, 'anonymous', ctx.nonce);
|
||||
});
|
||||
} catch (error) {
|
||||
// assetPrefix must not be a fully qualified domain name. We assume
|
||||
// we should preconnect to same origin instead
|
||||
preloadCallbacks.push(()=>{
|
||||
ctx.componentMod.preconnect('/', 'anonymous', ctx.nonce);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const styles = renderCssResource(styleTags, ctx, preloadCallbacks);
|
||||
const scripts = scriptTags ? scriptTags.map((href, index)=>{
|
||||
const fullSrc = `${ctx.assetPrefix}/_next/${encodeURIPath(href)}${getAssetQueryString(ctx, true)}`;
|
||||
return createElement('script', {
|
||||
src: fullSrc,
|
||||
async: true,
|
||||
key: `script-${index}`,
|
||||
nonce: ctx.nonce
|
||||
});
|
||||
}) : [];
|
||||
return styles.length || scripts.length ? [
|
||||
...styles,
|
||||
...scripts
|
||||
] : null;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=get-layer-assets.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Get hrefs for fonts to preload
|
||||
* Returns null if there are no fonts at all.
|
||||
* Returns string[] if there are fonts to preload (font paths)
|
||||
* Returns empty string[] if there are fonts but none to preload and no other fonts have been preloaded
|
||||
* Returns null if there are fonts but none to preload and at least some were previously preloaded
|
||||
*/ export function getPreloadableFonts(nextFontManifest, filePath, injectedFontPreloadTags) {
|
||||
if (!nextFontManifest || !filePath) {
|
||||
return null;
|
||||
}
|
||||
const filepathWithoutExtension = filePath.replace(/\.[^.]+$/, '');
|
||||
const fontFiles = new Set();
|
||||
let foundFontUsage = false;
|
||||
const preloadedFontFiles = nextFontManifest.app[filepathWithoutExtension];
|
||||
if (preloadedFontFiles) {
|
||||
foundFontUsage = true;
|
||||
for (const fontFile of preloadedFontFiles){
|
||||
if (!injectedFontPreloadTags.has(fontFile)) {
|
||||
fontFiles.add(fontFile);
|
||||
injectedFontPreloadTags.add(fontFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fontFiles.size) {
|
||||
return [
|
||||
...fontFiles
|
||||
].sort();
|
||||
} else if (foundFontUsage && injectedFontPreloadTags.size === 0) {
|
||||
return [];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=get-preloadable-fonts.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/get-preloadable-fonts.tsx"],"sourcesContent":["import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'\nimport type { DeepReadonly } from '../../shared/lib/deep-readonly'\n\n/**\n * Get hrefs for fonts to preload\n * Returns null if there are no fonts at all.\n * Returns string[] if there are fonts to preload (font paths)\n * Returns empty string[] if there are fonts but none to preload and no other fonts have been preloaded\n * Returns null if there are fonts but none to preload and at least some were previously preloaded\n */\nexport function getPreloadableFonts(\n nextFontManifest: DeepReadonly<NextFontManifest> | undefined,\n filePath: string | undefined,\n injectedFontPreloadTags: Set<string>\n): string[] | null {\n if (!nextFontManifest || !filePath) {\n return null\n }\n const filepathWithoutExtension = filePath.replace(/\\.[^.]+$/, '')\n const fontFiles = new Set<string>()\n let foundFontUsage = false\n\n const preloadedFontFiles = nextFontManifest.app[filepathWithoutExtension]\n if (preloadedFontFiles) {\n foundFontUsage = true\n for (const fontFile of preloadedFontFiles) {\n if (!injectedFontPreloadTags.has(fontFile)) {\n fontFiles.add(fontFile)\n injectedFontPreloadTags.add(fontFile)\n }\n }\n }\n\n if (fontFiles.size) {\n return [...fontFiles].sort()\n } else if (foundFontUsage && injectedFontPreloadTags.size === 0) {\n return []\n } else {\n return null\n }\n}\n"],"names":["getPreloadableFonts","nextFontManifest","filePath","injectedFontPreloadTags","filepathWithoutExtension","replace","fontFiles","Set","foundFontUsage","preloadedFontFiles","app","fontFile","has","add","size","sort"],"mappings":"AAGA;;;;;;CAMC,GACD,OAAO,SAASA,oBACdC,gBAA4D,EAC5DC,QAA4B,EAC5BC,uBAAoC;IAEpC,IAAI,CAACF,oBAAoB,CAACC,UAAU;QAClC,OAAO;IACT;IACA,MAAME,2BAA2BF,SAASG,OAAO,CAAC,YAAY;IAC9D,MAAMC,YAAY,IAAIC;IACtB,IAAIC,iBAAiB;IAErB,MAAMC,qBAAqBR,iBAAiBS,GAAG,CAACN,yBAAyB;IACzE,IAAIK,oBAAoB;QACtBD,iBAAiB;QACjB,KAAK,MAAMG,YAAYF,mBAAoB;YACzC,IAAI,CAACN,wBAAwBS,GAAG,CAACD,WAAW;gBAC1CL,UAAUO,GAAG,CAACF;gBACdR,wBAAwBU,GAAG,CAACF;YAC9B;QACF;IACF;IAEA,IAAIL,UAAUQ,IAAI,EAAE;QAClB,OAAO;eAAIR;SAAU,CAACS,IAAI;IAC5B,OAAO,IAAIP,kBAAkBL,wBAAwBW,IAAI,KAAK,GAAG;QAC/D,OAAO,EAAE;IACX,OAAO;QACL,OAAO;IACT;AACF","ignoreList":[0]}
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import { ESCAPE_REGEX } from '../htmlescape';
|
||||
export function getScriptNonceFromHeader(cspHeaderValue) {
|
||||
var _directive_split_slice_map_find;
|
||||
const directives = cspHeaderValue// Directives are split by ';'.
|
||||
.split(';').map((directive)=>directive.trim());
|
||||
// First try to find the directive for the 'script-src', otherwise try to
|
||||
// fallback to the 'default-src'.
|
||||
const directive = directives.find((dir)=>dir.startsWith('script-src')) || directives.find((dir)=>dir.startsWith('default-src'));
|
||||
// If no directive could be found, then we're done.
|
||||
if (!directive) {
|
||||
return;
|
||||
}
|
||||
// Extract the nonce from the directive
|
||||
const nonce = (_directive_split_slice_map_find = directive.split(' ')// Remove the 'strict-src'/'default-src' string, this can't be the nonce.
|
||||
.slice(1).map((source)=>source.trim())// Find the first source with the 'nonce-' prefix.
|
||||
.find((source)=>source.startsWith("'nonce-") && source.length > 8 && source.endsWith("'"))) == null ? void 0 : _directive_split_slice_map_find.slice(7, -1);
|
||||
// If we could't find the nonce, then we're done.
|
||||
if (!nonce) {
|
||||
return;
|
||||
}
|
||||
// Don't accept the nonce value if it contains HTML escape characters.
|
||||
// Technically, the spec requires a base64'd value, but this is just an
|
||||
// extra layer.
|
||||
if (ESCAPE_REGEX.test(nonce)) {
|
||||
throw Object.defineProperty(new Error('Nonce value from Content-Security-Policy contained HTML escape characters.\nLearn more: https://nextjs.org/docs/messages/nonce-contained-invalid-characters'), "__NEXT_ERROR_CODE", {
|
||||
value: "E440",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return nonce;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=get-script-nonce-from-header.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/get-script-nonce-from-header.tsx"],"sourcesContent":["import { ESCAPE_REGEX } from '../htmlescape'\n\nexport function getScriptNonceFromHeader(\n cspHeaderValue: string\n): string | undefined {\n const directives = cspHeaderValue\n // Directives are split by ';'.\n .split(';')\n .map((directive) => directive.trim())\n\n // First try to find the directive for the 'script-src', otherwise try to\n // fallback to the 'default-src'.\n const directive =\n directives.find((dir) => dir.startsWith('script-src')) ||\n directives.find((dir) => dir.startsWith('default-src'))\n\n // If no directive could be found, then we're done.\n if (!directive) {\n return\n }\n\n // Extract the nonce from the directive\n const nonce = directive\n .split(' ')\n // Remove the 'strict-src'/'default-src' string, this can't be the nonce.\n .slice(1)\n .map((source) => source.trim())\n // Find the first source with the 'nonce-' prefix.\n .find(\n (source) =>\n source.startsWith(\"'nonce-\") &&\n source.length > 8 &&\n source.endsWith(\"'\")\n )\n // Grab the nonce by trimming the 'nonce-' prefix.\n ?.slice(7, -1)\n\n // If we could't find the nonce, then we're done.\n if (!nonce) {\n return\n }\n\n // Don't accept the nonce value if it contains HTML escape characters.\n // Technically, the spec requires a base64'd value, but this is just an\n // extra layer.\n if (ESCAPE_REGEX.test(nonce)) {\n throw new Error(\n 'Nonce value from Content-Security-Policy contained HTML escape characters.\\nLearn more: https://nextjs.org/docs/messages/nonce-contained-invalid-characters'\n )\n }\n\n return nonce\n}\n"],"names":["ESCAPE_REGEX","getScriptNonceFromHeader","cspHeaderValue","directive","directives","split","map","trim","find","dir","startsWith","nonce","slice","source","length","endsWith","test","Error"],"mappings":"AAAA,SAASA,YAAY,QAAQ,gBAAe;AAE5C,OAAO,SAASC,yBACdC,cAAsB;QAmBRC;IAjBd,MAAMC,aAAaF,cACjB,+BAA+B;KAC9BG,KAAK,CAAC,KACNC,GAAG,CAAC,CAACH,YAAcA,UAAUI,IAAI;IAEpC,yEAAyE;IACzE,iCAAiC;IACjC,MAAMJ,YACJC,WAAWI,IAAI,CAAC,CAACC,MAAQA,IAAIC,UAAU,CAAC,kBACxCN,WAAWI,IAAI,CAAC,CAACC,MAAQA,IAAIC,UAAU,CAAC;IAE1C,mDAAmD;IACnD,IAAI,CAACP,WAAW;QACd;IACF;IAEA,uCAAuC;IACvC,MAAMQ,SAAQR,kCAAAA,UACXE,KAAK,CAAC,IACP,yEAAyE;KACxEO,KAAK,CAAC,GACNN,GAAG,CAAC,CAACO,SAAWA,OAAON,IAAI,GAC5B,kDAAkD;KACjDC,IAAI,CACH,CAACK,SACCA,OAAOH,UAAU,CAAC,cAClBG,OAAOC,MAAM,GAAG,KAChBD,OAAOE,QAAQ,CAAC,0BAVRZ,gCAaVS,KAAK,CAAC,GAAG,CAAC;IAEd,iDAAiD;IACjD,IAAI,CAACD,OAAO;QACV;IACF;IAEA,sEAAsE;IACtE,uEAAuE;IACvE,eAAe;IACf,IAAIX,aAAagB,IAAI,CAACL,QAAQ;QAC5B,MAAM,qBAEL,CAFK,IAAIM,MACR,gKADI,qBAAA;mBAAA;wBAAA;0BAAA;QAEN;IACF;IAEA,OAAON;AACT","ignoreList":[0]}
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
export const dynamicParamTypes = {
|
||||
catchall: 'c',
|
||||
'catchall-intercepted-(..)(..)': 'ci(..)(..)',
|
||||
'catchall-intercepted-(.)': 'ci(.)',
|
||||
'catchall-intercepted-(..)': 'ci(..)',
|
||||
'catchall-intercepted-(...)': 'ci(...)',
|
||||
'optional-catchall': 'oc',
|
||||
dynamic: 'd',
|
||||
'dynamic-intercepted-(..)(..)': 'di(..)(..)',
|
||||
'dynamic-intercepted-(.)': 'di(.)',
|
||||
'dynamic-intercepted-(..)': 'di(..)',
|
||||
'dynamic-intercepted-(...)': 'di(...)'
|
||||
};
|
||||
|
||||
//# sourceMappingURL=get-short-dynamic-param-type.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/get-short-dynamic-param-type.tsx"],"sourcesContent":["import type {\n DynamicParamTypes,\n DynamicParamTypesShort,\n} from '../../shared/lib/app-router-types'\n\nexport const dynamicParamTypes: Record<\n DynamicParamTypes,\n DynamicParamTypesShort\n> = {\n catchall: 'c',\n 'catchall-intercepted-(..)(..)': 'ci(..)(..)',\n 'catchall-intercepted-(.)': 'ci(.)',\n 'catchall-intercepted-(..)': 'ci(..)',\n 'catchall-intercepted-(...)': 'ci(...)',\n 'optional-catchall': 'oc',\n dynamic: 'd',\n 'dynamic-intercepted-(..)(..)': 'di(..)(..)',\n 'dynamic-intercepted-(.)': 'di(.)',\n 'dynamic-intercepted-(..)': 'di(..)',\n 'dynamic-intercepted-(...)': 'di(...)',\n}\n"],"names":["dynamicParamTypes","catchall","dynamic"],"mappings":"AAKA,OAAO,MAAMA,oBAGT;IACFC,UAAU;IACV,iCAAiC;IACjC,4BAA4B;IAC5B,6BAA6B;IAC7B,8BAA8B;IAC9B,qBAAqB;IACrBC,SAAS;IACT,gCAAgC;IAChC,2BAA2B;IAC3B,4BAA4B;IAC5B,6BAA6B;AAC/B,EAAC","ignoreList":[0]}
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
export function hasLoadingComponentInTree(tree) {
|
||||
const [, parallelRoutes, { loading }] = tree;
|
||||
if (loading) {
|
||||
return true;
|
||||
}
|
||||
return Object.values(parallelRoutes).some((parallelRoute)=>hasLoadingComponentInTree(parallelRoute));
|
||||
}
|
||||
|
||||
//# sourceMappingURL=has-loading-component-in-tree.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/has-loading-component-in-tree.tsx"],"sourcesContent":["import type { LoaderTree } from '../lib/app-dir-module'\n\nexport function hasLoadingComponentInTree(tree: LoaderTree): boolean {\n const [, parallelRoutes, { loading }] = tree\n\n if (loading) {\n return true\n }\n\n return Object.values(parallelRoutes).some((parallelRoute) =>\n hasLoadingComponentInTree(parallelRoute)\n ) as boolean\n}\n"],"names":["hasLoadingComponentInTree","tree","parallelRoutes","loading","Object","values","some","parallelRoute"],"mappings":"AAEA,OAAO,SAASA,0BAA0BC,IAAgB;IACxD,MAAM,GAAGC,gBAAgB,EAAEC,OAAO,EAAE,CAAC,GAAGF;IAExC,IAAIE,SAAS;QACX,OAAO;IACT;IAEA,OAAOC,OAAOC,MAAM,CAACH,gBAAgBI,IAAI,CAAC,CAACC,gBACzCP,0BAA0BO;AAE9B","ignoreList":[0]}
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export const INSTANT_VALIDATION_BOUNDARY_NAME = '__next_instant_validation_boundary__';
|
||||
|
||||
//# sourceMappingURL=boundary-constants.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/server/app-render/instant-validation/boundary-constants.ts"],"sourcesContent":["export const INSTANT_VALIDATION_BOUNDARY_NAME =\n '__next_instant_validation_boundary__'\n"],"names":["INSTANT_VALIDATION_BOUNDARY_NAME"],"mappings":"AAAA,OAAO,MAAMA,mCACX,uCAAsC","ignoreList":[0]}
|
||||
Generated
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable @next/internal/no-ambiguous-jsx -- React Client */ // Do not put a "use client" directive here. Import this module via the shim in
|
||||
// `packages/next/src/client/components/instant-validation/boundary.tsx` instead.
|
||||
// 'use client'
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { createContext } from 'react';
|
||||
import { INSTANT_VALIDATION_BOUNDARY_NAME } from './boundary-constants';
|
||||
import { InvariantError } from '../../../shared/lib/invariant-error';
|
||||
import { workUnitAsyncStorage } from '../work-unit-async-storage.external';
|
||||
if (typeof window !== 'undefined') {
|
||||
throw Object.defineProperty(new InvariantError('Instant validation boundaries should never appear in browser bundles.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1117",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
function getValidationBoundaryTracking() {
|
||||
const store = workUnitAsyncStorage.getStore();
|
||||
if (!store) return null;
|
||||
switch(store.type){
|
||||
case 'validation-client':
|
||||
return store.boundaryState;
|
||||
case 'prerender':
|
||||
case 'prerender-client':
|
||||
case 'prerender-ppr':
|
||||
case 'prerender-legacy':
|
||||
case 'prerender-runtime':
|
||||
case 'request':
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
store;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// We use a namespace object to allow us to recover the name of the function
|
||||
// at runtime even when production bundling/minification is used.
|
||||
const NameSpace = {
|
||||
[INSTANT_VALIDATION_BOUNDARY_NAME]: function({ id, children }) {
|
||||
// Track which boundaries we actually managed to render.
|
||||
const state = getValidationBoundaryTracking();
|
||||
if (state === null) {
|
||||
throw Object.defineProperty(new InvariantError('Missing boundary tracking state'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1060",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
state.renderedIds.add(id);
|
||||
return children;
|
||||
}
|
||||
};
|
||||
export const InstantValidationBoundaryContext = /*#__PURE__*/ createContext(null);
|
||||
export function PlaceValidationBoundaryBelowThisLevel({ id, children }) {
|
||||
return(// OuterLayoutRouter will see this and render a `RenderValidationBoundaryAtThisLevel`.
|
||||
/*#__PURE__*/ _jsx(InstantValidationBoundaryContext, {
|
||||
value: id,
|
||||
children: children
|
||||
}));
|
||||
}
|
||||
export function RenderValidationBoundaryAtThisLevel({ id, children }) {
|
||||
// We got a boundaryId from the context. Clear the context so that the children don't render another boundary.
|
||||
return /*#__PURE__*/ _jsx(InstantValidationBoundary, {
|
||||
id: id,
|
||||
children: /*#__PURE__*/ _jsx(InstantValidationBoundaryContext, {
|
||||
value: null,
|
||||
children: children
|
||||
})
|
||||
});
|
||||
}
|
||||
const InstantValidationBoundary = // We use slice(0) to trick the bundler into not inlining/minifying the function
|
||||
// so it retains the name inferred from the namespace object
|
||||
NameSpace[INSTANT_VALIDATION_BOUNDARY_NAME.slice(0)];
|
||||
|
||||
//# sourceMappingURL=boundary-impl.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/server/app-render/instant-validation/boundary-impl.tsx"],"sourcesContent":["/* eslint-disable @next/internal/no-ambiguous-jsx -- React Client */\n\n// Do not put a \"use client\" directive here. Import this module via the shim in\n// `packages/next/src/client/components/instant-validation/boundary.tsx` instead.\n// 'use client'\n\nimport { createContext, type ReactNode } from 'react'\nimport { INSTANT_VALIDATION_BOUNDARY_NAME } from './boundary-constants'\nimport { InvariantError } from '../../../shared/lib/invariant-error'\nimport type { ValidationBoundaryTracking } from './boundary-tracking'\nimport { workUnitAsyncStorage } from '../work-unit-async-storage.external'\n\nif (typeof window !== 'undefined') {\n throw new InvariantError(\n 'Instant validation boundaries should never appear in browser bundles.'\n )\n}\n\nfunction getValidationBoundaryTracking(): ValidationBoundaryTracking | null {\n const store = workUnitAsyncStorage.getStore()\n if (!store) return null\n switch (store.type) {\n case 'validation-client':\n return store.boundaryState\n case 'prerender':\n case 'prerender-client':\n case 'prerender-ppr':\n case 'prerender-legacy':\n case 'prerender-runtime':\n case 'request':\n case 'cache':\n case 'private-cache':\n case 'unstable-cache':\n case 'generate-static-params':\n break\n default:\n store satisfies never\n }\n return null\n}\n\n// We use a namespace object to allow us to recover the name of the function\n// at runtime even when production bundling/minification is used.\nconst NameSpace = {\n [INSTANT_VALIDATION_BOUNDARY_NAME]: function ({\n id,\n children,\n }: {\n id: string\n children: ReactNode\n }) {\n // Track which boundaries we actually managed to render.\n const state = getValidationBoundaryTracking()\n if (state === null) {\n throw new InvariantError('Missing boundary tracking state')\n }\n state.renderedIds.add(id)\n\n return children\n },\n}\n\ntype BoundaryPlacement =\n | null // do not place here\n | string // boundaryId -- place here\n\nexport const InstantValidationBoundaryContext =\n createContext<BoundaryPlacement>(null)\n\nexport function PlaceValidationBoundaryBelowThisLevel({\n id,\n children,\n}: {\n id: string\n children: ReactNode\n}) {\n return (\n // OuterLayoutRouter will see this and render a `RenderValidationBoundaryAtThisLevel`.\n <InstantValidationBoundaryContext value={id}>\n {children}\n </InstantValidationBoundaryContext>\n )\n}\n\nexport function RenderValidationBoundaryAtThisLevel({\n id,\n children,\n}: {\n id: string\n children: ReactNode\n}) {\n // We got a boundaryId from the context. Clear the context so that the children don't render another boundary.\n return (\n <InstantValidationBoundary id={id}>\n <InstantValidationBoundaryContext value={null}>\n {children}\n </InstantValidationBoundaryContext>\n </InstantValidationBoundary>\n )\n}\n\nconst InstantValidationBoundary =\n // We use slice(0) to trick the bundler into not inlining/minifying the function\n // so it retains the name inferred from the namespace object\n NameSpace[\n INSTANT_VALIDATION_BOUNDARY_NAME.slice(\n 0\n ) as typeof INSTANT_VALIDATION_BOUNDARY_NAME\n ]\n"],"names":["createContext","INSTANT_VALIDATION_BOUNDARY_NAME","InvariantError","workUnitAsyncStorage","window","getValidationBoundaryTracking","store","getStore","type","boundaryState","NameSpace","id","children","state","renderedIds","add","InstantValidationBoundaryContext","PlaceValidationBoundaryBelowThisLevel","value","RenderValidationBoundaryAtThisLevel","InstantValidationBoundary","slice"],"mappings":"AAAA,kEAAkE,GAElE,+EAA+E;AAC/E,iFAAiF;AACjF,eAAe;;AAEf,SAASA,aAAa,QAAwB,QAAO;AACrD,SAASC,gCAAgC,QAAQ,uBAAsB;AACvE,SAASC,cAAc,QAAQ,sCAAqC;AAEpE,SAASC,oBAAoB,QAAQ,sCAAqC;AAE1E,IAAI,OAAOC,WAAW,aAAa;IACjC,MAAM,qBAEL,CAFK,IAAIF,eACR,0EADI,qBAAA;eAAA;oBAAA;sBAAA;IAEN;AACF;AAEA,SAASG;IACP,MAAMC,QAAQH,qBAAqBI,QAAQ;IAC3C,IAAI,CAACD,OAAO,OAAO;IACnB,OAAQA,MAAME,IAAI;QAChB,KAAK;YACH,OAAOF,MAAMG,aAAa;QAC5B,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;YACH;QACF;YACEH;IACJ;IACA,OAAO;AACT;AAEA,4EAA4E;AAC5E,iEAAiE;AACjE,MAAMI,YAAY;IAChB,CAACT,iCAAiC,EAAE,SAAU,EAC5CU,EAAE,EACFC,QAAQ,EAIT;QACC,wDAAwD;QACxD,MAAMC,QAAQR;QACd,IAAIQ,UAAU,MAAM;YAClB,MAAM,qBAAqD,CAArD,IAAIX,eAAe,oCAAnB,qBAAA;uBAAA;4BAAA;8BAAA;YAAoD;QAC5D;QACAW,MAAMC,WAAW,CAACC,GAAG,CAACJ;QAEtB,OAAOC;IACT;AACF;AAMA,OAAO,MAAMI,iDACXhB,cAAiC,MAAK;AAExC,OAAO,SAASiB,sCAAsC,EACpDN,EAAE,EACFC,QAAQ,EAIT;IACC,OACE,sFAAsF;kBACtF,KAACI;QAAiCE,OAAOP;kBACtCC;;AAGP;AAEA,OAAO,SAASO,oCAAoC,EAClDR,EAAE,EACFC,QAAQ,EAIT;IACC,8GAA8G;IAC9G,qBACE,KAACQ;QAA0BT,IAAIA;kBAC7B,cAAA,KAACK;YAAiCE,OAAO;sBACtCN;;;AAIT;AAEA,MAAMQ,4BACJ,gFAAgF;AAChF,4DAA4D;AAC5DV,SAAS,CACPT,iCAAiCoB,KAAK,CACpC,GAEH","ignoreList":[0]}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export function createValidationBoundaryTracking() {
|
||||
return {
|
||||
expectedIds: new Set(),
|
||||
renderedIds: new Set()
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=boundary-tracking.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/server/app-render/instant-validation/boundary-tracking.tsx"],"sourcesContent":["export type ValidationBoundaryTracking = {\n expectedIds: Set<string>\n renderedIds: Set<string>\n}\n\nexport function createValidationBoundaryTracking(): ValidationBoundaryTracking {\n return {\n expectedIds: new Set(),\n renderedIds: new Set(),\n }\n}\n"],"names":["createValidationBoundaryTracking","expectedIds","Set","renderedIds"],"mappings":"AAKA,OAAO,SAASA;IACd,OAAO;QACLC,aAAa,IAAIC;QACjBC,aAAa,IAAID;IACnB;AACF","ignoreList":[0]}
|
||||
Generated
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
import { getLayoutOrPageModule } from '../../lib/app-dir-module';
|
||||
import { parseLoaderTree } from '../../../shared/lib/router/utils/parse-loader-tree';
|
||||
import { workAsyncStorage } from '../work-async-storage.external';
|
||||
export async function anySegmentHasRuntimePrefetchEnabled(tree) {
|
||||
const { mod: layoutOrPageMod } = await getLayoutOrPageModule(tree);
|
||||
// TODO(restart-on-cache-miss): Does this work correctly for client page/layout modules?
|
||||
const instantConfig = layoutOrPageMod ? layoutOrPageMod.unstable_instant : undefined;
|
||||
const hasRuntimePrefetch = instantConfig && typeof instantConfig === 'object' ? instantConfig.prefetch === 'runtime' : false;
|
||||
if (hasRuntimePrefetch) {
|
||||
return true;
|
||||
}
|
||||
const { parallelRoutes } = parseLoaderTree(tree);
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
const parallelRoute = parallelRoutes[parallelRouteKey];
|
||||
const hasChildRuntimePrefetch = await anySegmentHasRuntimePrefetchEnabled(parallelRoute);
|
||||
if (hasChildRuntimePrefetch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export async function isPageAllowedToBlock(tree) {
|
||||
const { mod: layoutOrPageMod } = await getLayoutOrPageModule(tree);
|
||||
// TODO(restart-on-cache-miss): Does this work correctly for client page/layout modules?
|
||||
const instantConfig = layoutOrPageMod ? layoutOrPageMod.unstable_instant : undefined;
|
||||
// If we encounter a non-false instant config before a instant=false,
|
||||
// the page isn't allowed to block. The config expresses a requirement for
|
||||
// instant UI, so we should make sure that a static shell exists.
|
||||
// (even if it'd use runtime prefetching for client navs)
|
||||
if (instantConfig !== undefined) {
|
||||
if (typeof instantConfig === 'object') {
|
||||
return false;
|
||||
} else if (instantConfig === false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const { parallelRoutes } = parseLoaderTree(tree);
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
const parallelRoute = parallelRoutes[parallelRouteKey];
|
||||
const subtreeIsBlocking = await isPageAllowedToBlock(parallelRoute);
|
||||
if (subtreeIsBlocking) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Checks if any segments in the loader tree have `instant` configs that need validating.
|
||||
* NOTE: Client navigations call this multiple times, so we cache it.
|
||||
* */ // Shared helper (not exported, not cached — called by the cached wrappers)
|
||||
async function anySegmentNeedsInstantValidation(rootTree, mode) {
|
||||
const segments = await findSegmentsWithInstantConfig(rootTree);
|
||||
// Check if there's any configs with `prefetch: 'static'` or `mode: 'instant'`.
|
||||
// (If there's only `false`, there's no need to run validation).
|
||||
// If any segment has `unstable_disableValidation`, we skip validation for the whole tree.
|
||||
let needsValidation = false;
|
||||
for (const { config } of segments){
|
||||
if (typeof config === 'object') {
|
||||
if (config.unstable_disableValidation === true || mode === 'dev' && config.unstable_disableDevValidation === true || mode === 'build' && config.unstable_disableBuildValidation === true) {
|
||||
return false;
|
||||
}
|
||||
// do not short-circuit, some other segment might still have `unstable_disableValidation`
|
||||
needsValidation = true;
|
||||
}
|
||||
}
|
||||
return needsValidation;
|
||||
}
|
||||
export const anySegmentNeedsInstantValidationInDev = cacheScopedToWorkStore(async (rootTree)=>anySegmentNeedsInstantValidation(rootTree, 'dev'));
|
||||
export const anySegmentNeedsInstantValidationInBuild = cacheScopedToWorkStore(async (rootTree)=>anySegmentNeedsInstantValidation(rootTree, 'build'));
|
||||
export const findSegmentsWithInstantConfig = cacheScopedToWorkStore(async (rootTree)=>{
|
||||
const results = [];
|
||||
async function visit(tree, path) {
|
||||
const { mod: layoutOrPageMod } = await getLayoutOrPageModule(tree);
|
||||
// TODO(restart-on-cache-miss): Does this work correctly for client page/layout modules?
|
||||
const instantConfig = layoutOrPageMod ? layoutOrPageMod.unstable_instant : undefined;
|
||||
if (instantConfig !== undefined) {
|
||||
results.push({
|
||||
path,
|
||||
config: instantConfig
|
||||
});
|
||||
}
|
||||
const { parallelRoutes } = parseLoaderTree(tree);
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
const childTree = parallelRoutes[parallelRouteKey];
|
||||
await visit(childTree, [
|
||||
...path,
|
||||
parallelRouteKey
|
||||
]);
|
||||
}
|
||||
}
|
||||
await visit(rootTree, []);
|
||||
return results;
|
||||
});
|
||||
export const resolveInstantConfigSamplesForPage = async (tree)=>{
|
||||
const { mod: layoutOrPageMod } = await getLayoutOrPageModule(tree);
|
||||
const instantConfig = layoutOrPageMod ? layoutOrPageMod.unstable_instant : undefined;
|
||||
let samples = null;
|
||||
if (instantConfig !== undefined && typeof instantConfig === 'object' && instantConfig.samples) {
|
||||
samples = instantConfig.samples;
|
||||
}
|
||||
// The samples from inner segments override samples from outer segments,
|
||||
// i.e. a page overrides the samples from a layout.
|
||||
// We do not perform any merging logic.
|
||||
const { parallelRoutes } = parseLoaderTree(tree);
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
if (parallelRouteKey !== 'children') {
|
||||
continue;
|
||||
}
|
||||
const childTree = parallelRoutes[parallelRouteKey];
|
||||
const childSamples = await resolveInstantConfigSamplesForPage(childTree);
|
||||
if (childSamples !== null) {
|
||||
samples = childSamples;
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
};
|
||||
/**
|
||||
* A simple cache wrapper for 1-argument functions.
|
||||
* The cache will live as long as the current WorkStore,
|
||||
* i.e. it's scoped to a single request.
|
||||
*/ function cacheScopedToWorkStore(func) {
|
||||
const resultsPerWorkStore = new WeakMap();
|
||||
return (arg)=>{
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
if (!workStore) {
|
||||
// No caching.
|
||||
return func(arg);
|
||||
}
|
||||
let results = resultsPerWorkStore.get(workStore);
|
||||
if (results && results.has(arg)) {
|
||||
return results.get(arg);
|
||||
}
|
||||
const result = func(arg);
|
||||
if (!results) {
|
||||
results = new WeakMap();
|
||||
resultsPerWorkStore.set(workStore, results);
|
||||
}
|
||||
results.set(arg, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=instant-config.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
import { workUnitAsyncStorage } from '../work-unit-async-storage.external';
|
||||
import { workAsyncStorage } from '../work-async-storage.external';
|
||||
import { createExhaustiveParamsProxy, createExhaustiveURLSearchParamsProxy, trackMissingSampleErrorAndThrow } from './instant-samples';
|
||||
import { InstantValidationError } from './instant-validation-error';
|
||||
export function instrumentParamsForClientValidation(underlyingParams) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workStore && workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'validation-client':
|
||||
{
|
||||
if (workUnitStore.validationSamples) {
|
||||
const declaredKeys = new Set(Object.keys(workUnitStore.validationSamples.params ?? {}));
|
||||
return createExhaustiveParamsProxy(underlyingParams, declaredKeys, workStore.route);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'prerender-runtime':
|
||||
case 'prerender-client':
|
||||
case 'prerender-legacy':
|
||||
case 'prerender-ppr':
|
||||
case 'prerender':
|
||||
case 'cache':
|
||||
case 'request':
|
||||
case 'private-cache':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
return underlyingParams;
|
||||
}
|
||||
export function expectCompleteParamsInClientValidation(expression) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workStore && workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'validation-client':
|
||||
{
|
||||
if (workUnitStore.validationSamples) {
|
||||
const fallbackParams = workUnitStore.fallbackRouteParams;
|
||||
if (fallbackParams && fallbackParams.size > 0) {
|
||||
const missingParams = Array.from(fallbackParams.keys());
|
||||
trackMissingSampleErrorAndThrow(Object.defineProperty(new InstantValidationError(`Route "${workStore.route}" called ${expression} but param${missingParams.length > 1 ? 's' : ''} ${missingParams.map((p)=>`"${p}"`).join(', ')} ${missingParams.length > 1 ? 'are' : 'is'} not defined in the \`samples\` of \`unstable_instant\`. ` + `${expression} requires all route params to be provided.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1109",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'prerender-runtime':
|
||||
case 'prerender-client':
|
||||
case 'prerender-legacy':
|
||||
case 'prerender-ppr':
|
||||
case 'prerender':
|
||||
case 'cache':
|
||||
case 'request':
|
||||
case 'private-cache':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function instrumentSearchParamsForClientValidation(underlyingSearchParams) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workStore && workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'validation-client':
|
||||
{
|
||||
if (workUnitStore.validationSamples) {
|
||||
const declaredKeys = new Set(Object.keys(workUnitStore.validationSamples.searchParams ?? {}));
|
||||
return createExhaustiveURLSearchParamsProxy(underlyingSearchParams, declaredKeys, workStore.route);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'prerender-runtime':
|
||||
case 'prerender-client':
|
||||
case 'prerender-legacy':
|
||||
case 'prerender-ppr':
|
||||
case 'prerender':
|
||||
case 'cache':
|
||||
case 'request':
|
||||
case 'private-cache':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
return underlyingSearchParams;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=instant-samples-client.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+395
@@ -0,0 +1,395 @@
|
||||
import { RequestCookies } from '../../web/spec-extension/cookies';
|
||||
import { RequestCookiesAdapter } from '../../web/spec-extension/adapters/request-cookies';
|
||||
import { HeadersAdapter } from '../../web/spec-extension/adapters/headers';
|
||||
import { getSegmentParam } from '../../../shared/lib/router/utils/get-segment-param';
|
||||
import { parseRelativeUrl } from '../../../shared/lib/router/utils/parse-relative-url';
|
||||
import { InvariantError } from '../../../shared/lib/invariant-error';
|
||||
import { InstantValidationError } from './instant-validation-error';
|
||||
import { workUnitAsyncStorage } from '../work-unit-async-storage.external';
|
||||
import { wellKnownProperties } from '../../../shared/lib/utils/reflect-utils';
|
||||
export function createValidationSampleTracking() {
|
||||
return {
|
||||
missingSampleErrors: []
|
||||
};
|
||||
}
|
||||
function getExpectedSampleTracking() {
|
||||
let validationSampleTracking = null;
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'request':
|
||||
case 'validation-client':
|
||||
// TODO(instant-validation-build): do we need any special handling for caches?
|
||||
validationSampleTracking = workUnitStore.validationSampleTracking ?? null;
|
||||
break;
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'unstable-cache':
|
||||
case 'prerender-legacy':
|
||||
case 'prerender-ppr':
|
||||
case 'prerender-client':
|
||||
case 'prerender':
|
||||
case 'prerender-runtime':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
if (!validationSampleTracking) {
|
||||
throw Object.defineProperty(new InvariantError('Expected to have a workUnitStore that provides validationSampleTracking'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1110",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return validationSampleTracking;
|
||||
}
|
||||
export function trackMissingSampleError(error) {
|
||||
const validationSampleTracking = getExpectedSampleTracking();
|
||||
validationSampleTracking.missingSampleErrors.push(error);
|
||||
}
|
||||
export function trackMissingSampleErrorAndThrow(error) {
|
||||
// TODO(instant-validation-build): this should abort the render
|
||||
trackMissingSampleError(error);
|
||||
throw error;
|
||||
}
|
||||
/**
|
||||
* Creates ReadonlyRequestCookies from sample cookie data.
|
||||
* Accessing a cookie not declared in the sample will throw an error.
|
||||
* Cookies with `value: null` are declared (allowed to access) but return no value.
|
||||
*/ export function createCookiesFromSample(sampleCookies, route) {
|
||||
const declaredNames = new Set();
|
||||
const cookies = new RequestCookies(new Headers());
|
||||
if (sampleCookies) {
|
||||
for (const cookie of sampleCookies){
|
||||
declaredNames.add(cookie.name);
|
||||
if (cookie.value !== null) {
|
||||
cookies.set(cookie.name, cookie.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
const sealed = RequestCookiesAdapter.seal(cookies);
|
||||
return new Proxy(sealed, {
|
||||
get (target, prop, receiver) {
|
||||
if (prop === 'has') {
|
||||
const originalMethod = Reflect.get(target, prop, receiver);
|
||||
const wrappedMethod = function(name) {
|
||||
if (!declaredNames.has(name)) {
|
||||
trackMissingSampleErrorAndThrow(createMissingCookieSampleError(route, name));
|
||||
}
|
||||
return originalMethod.call(target, name);
|
||||
};
|
||||
return wrappedMethod;
|
||||
}
|
||||
if (prop === 'get') {
|
||||
const originalMethod = Reflect.get(target, prop, receiver);
|
||||
const wrappedMethod = function(nameOrCookie) {
|
||||
let name;
|
||||
if (typeof nameOrCookie === 'string') {
|
||||
name = nameOrCookie;
|
||||
} else if (nameOrCookie && typeof nameOrCookie === 'object' && typeof nameOrCookie.name === 'string') {
|
||||
name = nameOrCookie.name;
|
||||
} else {
|
||||
// This is an invalid input. Pass it through to the original method so it can error.
|
||||
return originalMethod.call(target, nameOrCookie);
|
||||
}
|
||||
if (!declaredNames.has(name)) {
|
||||
trackMissingSampleErrorAndThrow(createMissingCookieSampleError(route, name));
|
||||
}
|
||||
return originalMethod.call(target, name);
|
||||
};
|
||||
return wrappedMethod;
|
||||
}
|
||||
// TODO(instant-validation-build): what should getAll do?
|
||||
// Maybe we should only allow it if there's an array (possibly empty?)
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
});
|
||||
}
|
||||
function createMissingCookieSampleError(route, name) {
|
||||
return Object.defineProperty(new InstantValidationError(`Route "${route}" accessed cookie "${name}" which is not defined in the \`samples\` ` + `of \`unstable_instant\`. Add it to the sample's \`cookies\` array, ` + `or \`{ name: "${name}", value: null }\` if it should be absent.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1115",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Creates ReadonlyHeaders from sample header data.
|
||||
* Accessing a header not declared in the sample will throw an error.
|
||||
* Headers with `value: null` are declared (allowed to access) but return null.
|
||||
*/ export function createHeadersFromSample(rawSampleHeaders, sampleCookies, route) {
|
||||
// If we have cookie samples, add a `cookie` header to match.
|
||||
// Accessing it will be implicitly allowed by the proxy --
|
||||
// if the user defined some cookies, accessing the "cookie" header is also fine.
|
||||
const sampleHeaders = rawSampleHeaders ? [
|
||||
...rawSampleHeaders
|
||||
] : [];
|
||||
if (sampleHeaders.find(([name])=>name.toLowerCase() === 'cookie')) {
|
||||
throw Object.defineProperty(new InstantValidationError('Invalid sample: Defining cookies via a "cookie" header is not supported. Use `cookies: [{ name: ..., value: ... }]` instead.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1111",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
if (sampleCookies) {
|
||||
const cookieHeaderValue = sampleCookies.toString();
|
||||
sampleHeaders.push([
|
||||
'cookie',
|
||||
// if the `cookies` samples were empty, or they were all `null`, then we have no cookies,
|
||||
// and the header isn't present, but should remains readable, so we set it to null.
|
||||
cookieHeaderValue !== '' ? cookieHeaderValue : null
|
||||
]);
|
||||
}
|
||||
const declaredNames = new Set();
|
||||
const headersInit = {};
|
||||
for (const [name, value] of sampleHeaders){
|
||||
declaredNames.add(name.toLowerCase());
|
||||
if (value !== null) {
|
||||
headersInit[name.toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
const sealed = HeadersAdapter.seal(HeadersAdapter.from(headersInit));
|
||||
return new Proxy(sealed, {
|
||||
get (target, prop, receiver) {
|
||||
if (prop === 'get' || prop === 'has') {
|
||||
const originalMethod = Reflect.get(target, prop, receiver);
|
||||
const patchedMethod = function(rawName) {
|
||||
const name = rawName.toLowerCase();
|
||||
if (!declaredNames.has(name)) {
|
||||
trackMissingSampleErrorAndThrow(Object.defineProperty(new InstantValidationError(`Route "${route}" accessed header "${name}" which is not defined in the \`samples\` ` + `of \`unstable_instant\`. Add it to the sample's \`headers\` array, ` + `or \`["${name}", null]\` if it should be absent.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1116",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}));
|
||||
}
|
||||
// typescript can't reconcile a union of functions with a union of return types,
|
||||
// so we have to cast the original return type away
|
||||
return originalMethod.call(target, name);
|
||||
};
|
||||
return patchedMethod;
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Creates a DraftModeProvider that always returns isEnabled: false.
|
||||
*/ export function createDraftModeForValidation() {
|
||||
// Create a minimal DraftModeProvider-compatible object
|
||||
// that always reports draft mode as disabled.
|
||||
//
|
||||
// private properties that can't be set from outside the class.
|
||||
return {
|
||||
get isEnabled () {
|
||||
return false;
|
||||
},
|
||||
enable () {
|
||||
throw Object.defineProperty(new Error('Draft mode cannot be enabled during build-time instant validation.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1092",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
},
|
||||
disable () {
|
||||
throw Object.defineProperty(new Error('Draft mode cannot be disabled during build-time instant validation.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1094",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates params wrapped with an exhaustive proxy.
|
||||
* Accessing a param not declared in the sample will throw an error.
|
||||
*/ export function createExhaustiveParamsProxy(underlyingParams, declaredParamNames, route) {
|
||||
return new Proxy(underlyingParams, {
|
||||
get (target, prop, receiver) {
|
||||
if (typeof prop === 'string' && !wellKnownProperties.has(prop) && // Only error when accessing a param that is part of the route but wasn't provided.
|
||||
// accessing properties that aren't expected to be a valid param value is fine.
|
||||
prop in underlyingParams && !declaredParamNames.has(prop)) {
|
||||
trackMissingSampleErrorAndThrow(Object.defineProperty(new InstantValidationError(`Route "${route}" accessed param "${prop}" which is not defined in the \`samples\` ` + `of \`unstable_instant\`. Add it to the sample's \`params\` object.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1095",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}));
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Creates searchParams wrapped with an exhaustive proxy.
|
||||
* Accessing a searchParam not declared in the sample will throw an error.
|
||||
* A searchParam with `value: undefined` means "declared but absent" (allowed to access, returns undefined).
|
||||
*/ export function createExhaustiveSearchParamsProxy(searchParams, declaredSearchParamNames, route) {
|
||||
return new Proxy(searchParams, {
|
||||
get (target, prop, receiver) {
|
||||
if (typeof prop === 'string' && !wellKnownProperties.has(prop) && !declaredSearchParamNames.has(prop)) {
|
||||
trackMissingSampleErrorAndThrow(createMissingSearchParamSampleError(route, prop));
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
has (target, prop) {
|
||||
if (typeof prop === 'string' && !wellKnownProperties.has(prop) && !declaredSearchParamNames.has(prop)) {
|
||||
trackMissingSampleErrorAndThrow(createMissingSearchParamSampleError(route, prop));
|
||||
}
|
||||
return Reflect.has(target, prop);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Wraps a URLSearchParams (or subclass like ReadonlyURLSearchParams) with an
|
||||
* exhaustive proxy. Accessing a search param not declared in the sample via
|
||||
* get/getAll/has will throw an error.
|
||||
*/ export function createExhaustiveURLSearchParamsProxy(searchParams, declaredSearchParamNames, route) {
|
||||
return new Proxy(searchParams, {
|
||||
get (target, prop, receiver) {
|
||||
// Intercept method calls that access specific param names
|
||||
if (prop === 'get' || prop === 'getAll' || prop === 'has') {
|
||||
const originalMathod = Reflect.get(target, prop, receiver);
|
||||
return (name)=>{
|
||||
if (typeof name === 'string' && !declaredSearchParamNames.has(name)) {
|
||||
trackMissingSampleErrorAndThrow(createMissingSearchParamSampleError(route, name));
|
||||
}
|
||||
return originalMathod.call(target, name);
|
||||
};
|
||||
}
|
||||
const value = Reflect.get(target, prop, receiver);
|
||||
// Prevent `TypeError: Value of "this" must be of type URLSearchParams` for methods
|
||||
if (typeof value === 'function' && !Object.hasOwn(target, prop)) {
|
||||
return value.bind(target);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
function createMissingSearchParamSampleError(route, name) {
|
||||
return Object.defineProperty(new InstantValidationError(`Route "${route}" accessed searchParam "${name}" which is not defined in the \`samples\` ` + `of \`unstable_instant\`. Add it to the sample's \`searchParams\` object, ` + `or \`{ "${name}": null }\` if it should be absent.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1098",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
export function createRelativeURLFromSamples(route, sampleParams, sampleSearchParams) {
|
||||
// Build searchParams query object and URL search string from sample
|
||||
const pathname = createPathnameFromRouteAndSampleParams(route, sampleParams ?? {});
|
||||
let search = '';
|
||||
if (sampleSearchParams) {
|
||||
const qs = createURLSearchParamsFromSample(sampleSearchParams).toString();
|
||||
if (qs) {
|
||||
search = '?' + qs;
|
||||
}
|
||||
}
|
||||
return parseRelativeUrl(pathname + search, undefined, true);
|
||||
}
|
||||
function createURLSearchParamsFromSample(sampleSearchParams) {
|
||||
const result = new URLSearchParams();
|
||||
if (sampleSearchParams) {
|
||||
for (const [key, value] of Object.entries(sampleSearchParams)){
|
||||
if (value === null || value === undefined) continue;
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value){
|
||||
result.append(key, v);
|
||||
}
|
||||
} else {
|
||||
result.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Substitute sample params into `workStore.route` to create a plausible pathname.
|
||||
* TODO(instant-validation-build): this logic is somewhat hacky and likely incomplete,
|
||||
* but it should be good enough for some initial testing.
|
||||
*/ function createPathnameFromRouteAndSampleParams(route, params) {
|
||||
let interpolatedSegments = [];
|
||||
const rawSegments = route.split('/');
|
||||
for (const rawSegment of rawSegments){
|
||||
const param = getSegmentParam(rawSegment);
|
||||
if (param) {
|
||||
switch(param.paramType){
|
||||
case 'catchall':
|
||||
case 'optional-catchall':
|
||||
{
|
||||
let paramValue = params[param.paramName];
|
||||
if (paramValue === undefined) {
|
||||
// The value for the param was not provided. `usePathname` will detect this and throw
|
||||
// before this can surface to userspace. Use `[...NAME]` as a placeholder for the param value
|
||||
// in case it pops up somewhere unexpectedly.
|
||||
paramValue = [
|
||||
rawSegment
|
||||
];
|
||||
} else if (!Array.isArray(paramValue)) {
|
||||
// NOTE: this happens outside of render, so we don't need `trackMissingSampleErrorAndThrow`
|
||||
throw Object.defineProperty(new InstantValidationError(`Expected sample param value for segment '${rawSegment}' to be an array of strings, got ${typeof paramValue}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1104",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
interpolatedSegments.push(...paramValue.map((v)=>encodeURIComponent(v)));
|
||||
break;
|
||||
}
|
||||
case 'dynamic':
|
||||
{
|
||||
let paramValue = params[param.paramName];
|
||||
if (paramValue === undefined) {
|
||||
// The value for the param was not provided. `usePathname` will detect this and throw
|
||||
// before this can surface to userspace. Use `[NAME]` as a placeholder for the param value
|
||||
// in case it pops up somewhere unexpectedly.
|
||||
paramValue = rawSegment;
|
||||
} else if (typeof paramValue !== 'string') {
|
||||
// NOTE: this happens outside of render, so we don't need `trackMissingSampleErrorAndThrow`
|
||||
throw Object.defineProperty(new InstantValidationError(`Expected sample param value for segment '${rawSegment}' to be a string, got ${typeof paramValue}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1108",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
interpolatedSegments.push(encodeURIComponent(paramValue));
|
||||
break;
|
||||
}
|
||||
case 'catchall-intercepted-(..)(..)':
|
||||
case 'catchall-intercepted-(.)':
|
||||
case 'catchall-intercepted-(..)':
|
||||
case 'catchall-intercepted-(...)':
|
||||
case 'dynamic-intercepted-(..)(..)':
|
||||
case 'dynamic-intercepted-(.)':
|
||||
case 'dynamic-intercepted-(..)':
|
||||
case 'dynamic-intercepted-(...)':
|
||||
{
|
||||
// TODO(instant-validation-build): i don't know how these are supposed to work, or if we can even get them here
|
||||
throw Object.defineProperty(new InvariantError('Not implemented: Validation of interception routes'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1106",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
default:
|
||||
{
|
||||
param.paramType;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
interpolatedSegments.push(rawSegment);
|
||||
}
|
||||
}
|
||||
return interpolatedSegments.join('/');
|
||||
}
|
||||
export function assertRootParamInSamples(workStore, sampleParams, paramName) {
|
||||
if (sampleParams && paramName in sampleParams) {
|
||||
// The param is defined in the samples.
|
||||
} else {
|
||||
const route = workStore.route;
|
||||
trackMissingSampleErrorAndThrow(Object.defineProperty(new InstantValidationError(`Route "${route}" accessed root param "${paramName}" which is not defined in the \`samples\` ` + `of \`unstable_instant\`. Add it to the sample's \`params\` object.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1114",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=instant-samples.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
frontend/node_modules/next/dist/esm/server/app-render/instant-validation/instant-validation-error.js
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
const INSTANT_VALIDATION_ERROR_DIGEST = 'INSTANT_VALIDATION_ERROR';
|
||||
/** Check if an error is an exhaustive samples validation error (by digest). */ export function isInstantValidationError(err) {
|
||||
return !!(err && typeof err === 'object' && err instanceof Error && err.digest === INSTANT_VALIDATION_ERROR_DIGEST);
|
||||
}
|
||||
export class InstantValidationError extends Error {
|
||||
constructor(...args){
|
||||
super(...args), this.digest = INSTANT_VALIDATION_ERROR_DIGEST;
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=instant-validation-error.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/server/app-render/instant-validation/instant-validation-error.ts"],"sourcesContent":["const INSTANT_VALIDATION_ERROR_DIGEST = 'INSTANT_VALIDATION_ERROR'\n\n/** Check if an error is an exhaustive samples validation error (by digest). */\nexport function isInstantValidationError(\n err: unknown\n): err is InstantValidationError {\n return !!(\n err &&\n typeof err === 'object' &&\n err instanceof Error &&\n (err as any).digest === INSTANT_VALIDATION_ERROR_DIGEST\n )\n}\n\nexport class InstantValidationError extends Error {\n digest = INSTANT_VALIDATION_ERROR_DIGEST\n}\n"],"names":["INSTANT_VALIDATION_ERROR_DIGEST","isInstantValidationError","err","Error","digest","InstantValidationError"],"mappings":"AAAA,MAAMA,kCAAkC;AAExC,6EAA6E,GAC7E,OAAO,SAASC,yBACdC,GAAY;IAEZ,OAAO,CAAC,CACNA,CAAAA,OACA,OAAOA,QAAQ,YACfA,eAAeC,SACf,AAACD,IAAYE,MAAM,KAAKJ,+BAA8B;AAE1D;AAEA,OAAO,MAAMK,+BAA+BF;;QAArC,qBACLC,SAASJ;;AACX","ignoreList":[0]}
|
||||
Generated
Vendored
+727
@@ -0,0 +1,727 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { InvariantError } from '../../../shared/lib/invariant-error';
|
||||
import { RenderStage } from '../staged-rendering';
|
||||
import { getServerModuleMap } from '../manifests-singleton';
|
||||
import { runInSequentialTasks } from '../app-render-render-utils';
|
||||
import { workAsyncStorage } from '../work-async-storage.external';
|
||||
import { Phase, printDebugThrownValueForProspectiveRender } from '../prospective-render-utils';
|
||||
import { getDigestForWellKnownError } from '../create-error-handler';
|
||||
import { // NOTE: we're in the server layer, so this is a client reference
|
||||
PlaceValidationBoundaryBelowThisLevel } from '../../../client/components/instant-validation/boundary';
|
||||
import { getLayoutOrPageModule } from '../../lib/app-dir-module';
|
||||
import { parseLoaderTree } from '../../../shared/lib/router/utils/parse-loader-tree';
|
||||
import { Readable } from 'node:stream';
|
||||
import { createNodeStreamWithLateRelease, createNodeStreamFromChunks } from './stream-utils';
|
||||
import { createDebugChannel } from '../debug-channel-server';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createFromNodeStream } from 'react-server-dom-webpack/client';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { renderToReadableStream } from 'react-server-dom-webpack/server';
|
||||
import { addSearchParamsIfPageSegment, isGroupSegment, PAGE_SEGMENT_KEY, DEFAULT_SEGMENT_KEY, NOT_FOUND_SEGMENT_KEY } from '../../../shared/lib/segment';
|
||||
const filterStackFrame = process.env.NODE_ENV !== 'production' ? require('../../lib/source-maps').filterStackFrameDEV : undefined;
|
||||
const findSourceMapURL = process.env.NODE_ENV !== 'production' ? require('../../lib/source-maps').findSourceMapURLDEV : undefined;
|
||||
const debug = process.env.NEXT_PRIVATE_DEBUG_VALIDATION === '1' ? console.log : undefined;
|
||||
function traverseRootSeedDataSegments(initialRSCPayload, processSegment) {
|
||||
const { flightRouterState, seedData } = getRootDataFromPayload(initialRSCPayload);
|
||||
const [rootSegment] = flightRouterState;
|
||||
const rootPath = stringifySegment(rootSegment);
|
||||
return traverseCacheNodeSegments(rootPath, flightRouterState, seedData, processSegment);
|
||||
}
|
||||
function traverseCacheNodeSegments(path, route, seedData, processSegment) {
|
||||
processSegment(path, seedData);
|
||||
const [_segment, childRoutes] = route;
|
||||
const [_node, parallelRoutesData, _loading, _isPartial] = seedData;
|
||||
for(const parallelRouteKey in childRoutes){
|
||||
const childSeedData = parallelRoutesData[parallelRouteKey];
|
||||
if (!childSeedData) {
|
||||
throw Object.defineProperty(new InvariantError(`Got unexpected empty seed data during instant validation`), "__NEXT_ERROR_CODE", {
|
||||
value: "E992",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const childRoute = childRoutes[parallelRouteKey];
|
||||
// NOTE: if this is a __PAGE__ segment, it might have search params appended.
|
||||
// Whoever reads from the cache needs to append them as well.
|
||||
const [childSegment] = childRoute;
|
||||
const childPath = createChildSegmentPath(path, parallelRouteKey, childSegment);
|
||||
traverseCacheNodeSegments(childPath, childRoute, childSeedData, processSegment);
|
||||
}
|
||||
}
|
||||
function createChildSegmentPath(parentPath, parallelRouteKey, segment) {
|
||||
const parallelRoutePrefix = parallelRouteKey === 'children' ? '' : `@${encodeURIComponent(parallelRouteKey)}/`;
|
||||
return `${parentPath}/${parallelRoutePrefix}${stringifySegment(segment)}`;
|
||||
}
|
||||
function stringifySegment(segment) {
|
||||
return typeof segment === 'string' ? encodeURIComponent(segment) : encodeURIComponent(segment[0]) + '|' + segment[1] + '|' + segment[2];
|
||||
}
|
||||
/**
|
||||
* Splits an existing staged stream (represented as arrays of chunks)
|
||||
* into separate staged streams (also in arrays-of-chunks form), one for each segment.
|
||||
* */ export async function collectStagedSegmentData(fullPageChunks, fullPageDebugChunks, startTime, hasRuntimePrefetch, clientReferenceManifest) {
|
||||
const debugChannelAbortController = new AbortController();
|
||||
const debugStream = fullPageDebugChunks ? createNodeStreamFromChunks(fullPageDebugChunks, debugChannelAbortController.signal) : null;
|
||||
const { stream, controller } = createStagedStreamFromChunks(fullPageChunks);
|
||||
stream.on('end', ()=>{
|
||||
// When the stream finishes, we have to close the debug stream too,
|
||||
// but delay it to avoid "Connection closed." errors.
|
||||
setImmediate(()=>debugChannelAbortController.abort());
|
||||
});
|
||||
// Technically we're just re-encoding, so nothing new should be emitted,
|
||||
// but we add an environment name just in case.
|
||||
const environmentName = ()=>{
|
||||
const currentStage = controller.currentStage;
|
||||
switch(currentStage){
|
||||
case RenderStage.Static:
|
||||
return 'Prerender';
|
||||
case RenderStage.Runtime:
|
||||
return hasRuntimePrefetch ? 'Prefetch' : 'Prefetchable';
|
||||
case RenderStage.Dynamic:
|
||||
return 'Server';
|
||||
default:
|
||||
currentStage;
|
||||
throw Object.defineProperty(new InvariantError(`Invalid render stage: ${currentStage}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E881",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
};
|
||||
// Deserialize the payload.
|
||||
// NOTE: the stream will initially be in the static stage, so that's as far as we get here.
|
||||
// We still expect the outer structure of the payload to be readable in this state.
|
||||
const serverConsumerManifest = {
|
||||
moduleLoading: null,
|
||||
moduleMap: clientReferenceManifest.rscModuleMapping,
|
||||
serverModuleMap: getServerModuleMap()
|
||||
};
|
||||
const payload = await createFromNodeStream(stream, serverConsumerManifest, {
|
||||
findSourceMapURL,
|
||||
debugChannel: debugStream ?? undefined,
|
||||
// Do not pass start/end timings - we do not want to omit any debug info.
|
||||
startTime: undefined,
|
||||
endTime: undefined
|
||||
});
|
||||
// Deconstruct the payload into separate streams per segment.
|
||||
// We have to preserve the stage information for each of them,
|
||||
// so that we can later render each segment in any stage we need.
|
||||
const { head } = getRootDataFromPayload(payload);
|
||||
const segments = new Map();
|
||||
traverseRootSeedDataSegments(payload, (segmentPath, seedData)=>{
|
||||
segments.set(segmentPath, createSegmentData(seedData));
|
||||
});
|
||||
const cache = createSegmentCache();
|
||||
const pendingTasks = [];
|
||||
/** Track when we advance stages so we can pass them as `endTime` later. */ const stageEndTimes = {
|
||||
[RenderStage.Static]: -1,
|
||||
[RenderStage.Runtime]: -1
|
||||
};
|
||||
const renderIntoCacheItem = async (data, cacheEntry)=>{
|
||||
const segmentDebugChannel = cacheEntry.debugChunks ? createDebugChannel() : undefined;
|
||||
const itemStream = renderToReadableStream(data, clientReferenceManifest.clientModules, {
|
||||
filterStackFrame,
|
||||
debugChannel: segmentDebugChannel == null ? void 0 : segmentDebugChannel.serverSide,
|
||||
environmentName,
|
||||
startTime,
|
||||
onError (error) {
|
||||
const digest = getDigestForWellKnownError(error);
|
||||
if (digest) {
|
||||
return digest;
|
||||
}
|
||||
// Forward existing digests
|
||||
if (error && typeof error === 'object' && 'digest' in error && typeof error.digest === 'string') {
|
||||
return error.digest;
|
||||
}
|
||||
// We don't need to log the errors because we would have already done that
|
||||
// when generating the original Flight stream for the whole page.
|
||||
if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
printDebugThrownValueForProspectiveRender(error, (workStore == null ? void 0 : workStore.route) ?? 'unknown route', Phase.InstantValidation);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all([
|
||||
// accumulate Flight chunks
|
||||
(async ()=>{
|
||||
for await (const chunk of itemStream.values()){
|
||||
writeChunk(cacheEntry.chunks, controller.currentStage, chunk);
|
||||
}
|
||||
})(),
|
||||
// accumulate Debug chunks
|
||||
segmentDebugChannel && (async ()=>{
|
||||
for await (const chunk of segmentDebugChannel.clientSide.readable.values()){
|
||||
cacheEntry.debugChunks.push(chunk);
|
||||
}
|
||||
})()
|
||||
]);
|
||||
};
|
||||
await runInSequentialTasks(()=>{
|
||||
{
|
||||
const headCacheItem = createSegmentCacheItem(!!fullPageDebugChunks);
|
||||
cache.head = headCacheItem;
|
||||
pendingTasks.push(renderIntoCacheItem(head, headCacheItem));
|
||||
}
|
||||
for (const [segmentPath, segmentData] of segments){
|
||||
const segmentCacheItem = createSegmentCacheItem(!!fullPageDebugChunks);
|
||||
cache.segments.set(segmentPath, segmentCacheItem);
|
||||
pendingTasks.push(renderIntoCacheItem(segmentData, segmentCacheItem));
|
||||
}
|
||||
}, ()=>{
|
||||
stageEndTimes[RenderStage.Static] = performance.now() + performance.timeOrigin;
|
||||
controller.advanceStage(RenderStage.Runtime);
|
||||
}, ()=>{
|
||||
stageEndTimes[RenderStage.Runtime] = performance.now() + performance.timeOrigin;
|
||||
controller.advanceStage(RenderStage.Dynamic);
|
||||
});
|
||||
await Promise.all(pendingTasks);
|
||||
return {
|
||||
cache,
|
||||
payload,
|
||||
stageEndTimes
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Turns accumulated stage chunks into a stream.
|
||||
* The stream starts out in Static stage, and can be advanced further
|
||||
* using the returned controller object.
|
||||
* Conceptually, this is similar to how we unblock more content
|
||||
* by advancing stages in a regular staged render.
|
||||
* */ function createStagedStreamFromChunks(stageChunks) {
|
||||
// The successive stages are supersets of one another,
|
||||
// so we can index into the dynamic chunks everywhere
|
||||
// and just look at the lengths of the Static/Runtime arrays
|
||||
const allChunks = stageChunks[RenderStage.Dynamic];
|
||||
const numStaticChunks = stageChunks[RenderStage.Static].length;
|
||||
const numRuntimeChunks = stageChunks[RenderStage.Runtime].length;
|
||||
const numDynamicChunks = stageChunks[RenderStage.Dynamic].length;
|
||||
let chunkIx = 0;
|
||||
let currentStage = RenderStage.Static;
|
||||
let closed = false;
|
||||
function push(chunk) {
|
||||
stream.push(chunk);
|
||||
}
|
||||
function close() {
|
||||
closed = true;
|
||||
stream.push(null);
|
||||
}
|
||||
const stream = new Readable({
|
||||
read () {
|
||||
// Emit static chunks
|
||||
for(; chunkIx < numStaticChunks; chunkIx++){
|
||||
push(allChunks[chunkIx]);
|
||||
}
|
||||
// If there's no more chunks after this stage, finish the stream.
|
||||
if (chunkIx >= allChunks.length) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
function advanceStage(stage) {
|
||||
if (closed) return true;
|
||||
switch(stage){
|
||||
case RenderStage.Runtime:
|
||||
{
|
||||
currentStage = RenderStage.Runtime;
|
||||
for(; chunkIx < numRuntimeChunks; chunkIx++){
|
||||
push(allChunks[chunkIx]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RenderStage.Dynamic:
|
||||
{
|
||||
currentStage = RenderStage.Dynamic;
|
||||
for(; chunkIx < numDynamicChunks; chunkIx++){
|
||||
push(allChunks[chunkIx]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
stage;
|
||||
}
|
||||
}
|
||||
// If there's no more chunks after this stage, finish the stream.
|
||||
if (chunkIx >= allChunks.length) {
|
||||
close();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
stream,
|
||||
controller: {
|
||||
get currentStage () {
|
||||
return currentStage;
|
||||
},
|
||||
advanceStage
|
||||
}
|
||||
};
|
||||
}
|
||||
function writeChunk(stageChunks, stage, chunk) {
|
||||
switch(stage){
|
||||
case RenderStage.Static:
|
||||
{
|
||||
stageChunks[RenderStage.Static].push(chunk);
|
||||
// fallthrough
|
||||
}
|
||||
case RenderStage.Runtime:
|
||||
{
|
||||
stageChunks[RenderStage.Runtime].push(chunk);
|
||||
// fallthrough
|
||||
}
|
||||
case RenderStage.Dynamic:
|
||||
{
|
||||
stageChunks[RenderStage.Dynamic].push(chunk);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
stage;
|
||||
}
|
||||
}
|
||||
}
|
||||
//===============================================================
|
||||
// 3. Recombining segments into a new payload
|
||||
//===============================================================
|
||||
/**
|
||||
* Creates a late-release stream for a given payload.
|
||||
* When `renderSignal` is triggered, the stream will release late chunks
|
||||
* to provide extra debug info.
|
||||
* */ export async function createCombinedPayloadStream(payload, extraChunksAbortController, renderSignal, clientReferenceManifest, startTime, isDebugChannelEnabled) {
|
||||
// Collect all the chunks so that we're not dependent on timing of the render.
|
||||
let isRenderable = true;
|
||||
const renderableChunks = [];
|
||||
const allChunks = [];
|
||||
const debugChunks = isDebugChannelEnabled ? [] : null;
|
||||
const debugChannel = isDebugChannelEnabled ? createDebugChannel() : null;
|
||||
let streamFinished;
|
||||
await runInSequentialTasks(()=>{
|
||||
const stream = renderToReadableStream(payload, clientReferenceManifest.clientModules, {
|
||||
filterStackFrame,
|
||||
debugChannel: debugChannel == null ? void 0 : debugChannel.serverSide,
|
||||
startTime,
|
||||
onError (error) {
|
||||
const digest = getDigestForWellKnownError(error);
|
||||
if (digest) {
|
||||
return digest;
|
||||
}
|
||||
// Forward existing digests
|
||||
if (error && typeof error === 'object' && 'digest' in error && typeof error.digest === 'string') {
|
||||
return error.digest;
|
||||
}
|
||||
// We don't need to log the errors because we would have already done that
|
||||
// when generating the original Flight stream for the whole page.
|
||||
if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
printDebugThrownValueForProspectiveRender(error, (workStore == null ? void 0 : workStore.route) ?? 'unknown route', Phase.InstantValidation);
|
||||
}
|
||||
}
|
||||
});
|
||||
streamFinished = Promise.all([
|
||||
// Accumulate Flight chunks
|
||||
(async ()=>{
|
||||
for await (const chunk of stream.values()){
|
||||
allChunks.push(chunk);
|
||||
if (isRenderable) {
|
||||
renderableChunks.push(chunk);
|
||||
}
|
||||
}
|
||||
})(),
|
||||
// Accumulate debug chunks
|
||||
debugChannel && (async ()=>{
|
||||
for await (const chunk of debugChannel.clientSide.readable.values()){
|
||||
debugChunks.push(chunk);
|
||||
}
|
||||
})()
|
||||
]);
|
||||
}, ()=>{
|
||||
isRenderable = false;
|
||||
extraChunksAbortController.abort();
|
||||
});
|
||||
await streamFinished;
|
||||
return {
|
||||
stream: createNodeStreamWithLateRelease(renderableChunks, allChunks, renderSignal),
|
||||
debugStream: debugChunks ? createNodeStreamFromChunks(debugChunks, renderSignal) : null
|
||||
};
|
||||
}
|
||||
function getRootDataFromPayload(initialRSCPayload) {
|
||||
// FlightDataPath is an unsound type, hence the additional checks.
|
||||
const flightDataPaths = initialRSCPayload.f;
|
||||
if (flightDataPaths.length !== 1 && flightDataPaths[0].length !== 3) {
|
||||
throw Object.defineProperty(new InvariantError('InitialRSCPayload does not match the expected shape during instant validation.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E994",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const flightRouterState = flightDataPaths[0][0];
|
||||
const seedData = flightDataPaths[0][1];
|
||||
// TODO: handle head
|
||||
const head = flightDataPaths[0][2];
|
||||
return {
|
||||
flightRouterState,
|
||||
seedData,
|
||||
head
|
||||
};
|
||||
}
|
||||
async function createValidationHead(cache, releaseSignal, clientReferenceManifest, stageEndTimes, stage) {
|
||||
const segmentCacheItem = cache.head;
|
||||
if (!segmentCacheItem) {
|
||||
throw Object.defineProperty(new InvariantError(`Missing segment data: <head>`), "__NEXT_ERROR_CODE", {
|
||||
value: "E1072",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return await deserializeFromChunks(segmentCacheItem.chunks[stage], segmentCacheItem.chunks[RenderStage.Dynamic], segmentCacheItem.debugChunks, releaseSignal, clientReferenceManifest, {
|
||||
startTime: undefined,
|
||||
endTime: stageEndTimes[stage]
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Deserializes a (partial possibly partial) RSC stream, given as a chunk-array.
|
||||
* If the stream is partial, we'll wait for `releaseSignal` to fire
|
||||
* and then complete the deserialization using `allChunks`.
|
||||
*
|
||||
* This is used to obtain a partially-complete model (that might contain unresolved holes)
|
||||
* and then release any late debug info from chunks that came later before we abort the render.
|
||||
* */ function deserializeFromChunks(partialChunks, allChunks, debugChunks, releaseSignal, clientReferenceManifest, timings) {
|
||||
const debugChannelAbortController = new AbortController();
|
||||
const debugStream = debugChunks ? createNodeStreamFromChunks(debugChunks, debugChannelAbortController.signal) : null;
|
||||
const serverConsumerManifest = {
|
||||
moduleLoading: null,
|
||||
moduleMap: clientReferenceManifest.rscModuleMapping,
|
||||
serverModuleMap: getServerModuleMap()
|
||||
};
|
||||
const segmentStream = partialChunks.length < allChunks.length ? createNodeStreamWithLateRelease(partialChunks, allChunks, releaseSignal) : createNodeStreamFromChunks(partialChunks);
|
||||
segmentStream.on('end', ()=>{
|
||||
// When the stream finishes, we have to close the debug stream too,
|
||||
// but delay it to avoid "Connection closed." errors.
|
||||
setImmediate(()=>debugChannelAbortController.abort());
|
||||
});
|
||||
return createFromNodeStream(segmentStream, serverConsumerManifest, {
|
||||
findSourceMapURL,
|
||||
debugChannel: debugStream ?? undefined,
|
||||
startTime: timings == null ? void 0 : timings.startTime,
|
||||
endTime: timings == null ? void 0 : timings.endTime
|
||||
});
|
||||
}
|
||||
function createSegmentData(seedData) {
|
||||
const [node, _parallelRoutesData, _unused, isPartial, varyParams] = seedData;
|
||||
return {
|
||||
node,
|
||||
isPartial,
|
||||
varyParams
|
||||
};
|
||||
}
|
||||
function getCacheNodeSeedDataFromSegment(data, slots) {
|
||||
return [
|
||||
data.node,
|
||||
slots,
|
||||
/* unused (previously `loading`) */ null,
|
||||
data.isPartial,
|
||||
data.varyParams
|
||||
];
|
||||
}
|
||||
function createSegmentCache() {
|
||||
return {
|
||||
head: null,
|
||||
segments: new Map()
|
||||
};
|
||||
}
|
||||
function createSegmentCacheItem(withDebugChunks) {
|
||||
return {
|
||||
chunks: {
|
||||
[RenderStage.Static]: [],
|
||||
[RenderStage.Runtime]: [],
|
||||
[RenderStage.Dynamic]: []
|
||||
},
|
||||
debugChunks: withDebugChunks ? [] : null
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Whether this segment consumes a URL depth level. Each URL depth
|
||||
* represents a potential navigation boundary.
|
||||
*
|
||||
* The root segment ('') consumes depth 0. Regular segments like
|
||||
* 'dashboard' consume the next depth — whether or not they have a
|
||||
* layout. Route groups, __PAGE__, __DEFAULT__, and /_not-found don't
|
||||
* consume a depth — they share the boundary of their parent.
|
||||
*/ function segmentConsumesURLDepth(segment) {
|
||||
// Dynamic segments (tuples) always consume a URL depth.
|
||||
if (typeof segment !== 'string') return true;
|
||||
// Route groups, pages, defaults, and not-found don't consume a depth.
|
||||
if (segment.startsWith(PAGE_SEGMENT_KEY) || isGroupSegment(segment) || segment === DEFAULT_SEGMENT_KEY || segment === NOT_FOUND_SEGMENT_KEY) {
|
||||
return false;
|
||||
}
|
||||
// Everything else consumes a depth, including the root segment ''.
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Walks the LoaderTree to discover validation depth bounds.
|
||||
*
|
||||
* Each route group between URL segments represents a potential
|
||||
* shared/new boundary in a client navigation. When a user navigates
|
||||
* between sibling routes that share a route group layout, that
|
||||
* layout is already mounted — its Suspense boundaries are revealed
|
||||
* and don't cover new content below. By tracking the max group
|
||||
* depth at each URL depth, we can iterate all possible group
|
||||
* boundaries and validate that blocking code is always covered by
|
||||
* Suspense in the new tree. This is conservative: some boundaries
|
||||
* may not correspond to real navigations (e.g. a route group with
|
||||
* no siblings), but it ensures we don't miss real violations.
|
||||
*
|
||||
* The max is taken across all parallel slots. When slots have
|
||||
* different numbers of groups, the deepest slot determines the
|
||||
* iteration range. Shallower slots simply stay entirely shared
|
||||
* at group depths beyond their own group count — they run out
|
||||
* of groups before reaching the boundary, so their content
|
||||
* remains in the Dynamic stage.
|
||||
*
|
||||
* Returns an array where:
|
||||
* - length = max URL depth (number of URL-consuming segments)
|
||||
* - array[i] = max group depth at URL depth i (number of route group
|
||||
* segments between this URL depth and the next)
|
||||
*
|
||||
* For example, a tree like:
|
||||
* '' / (outer) / (inner) / dashboard / page
|
||||
* returns [2, 0] — URL depth 0 (root) has 2 group layers before
|
||||
* the next URL segment (dashboard), and URL depth 1 (dashboard) has
|
||||
* 0 group layers before the leaf.
|
||||
*/ export function discoverValidationDepths(loaderTree) {
|
||||
const groupDepthsByUrlDepth = [];
|
||||
function recordGroupDepth(urlDepth, groupDepth) {
|
||||
while(groupDepthsByUrlDepth.length <= urlDepth){
|
||||
groupDepthsByUrlDepth.push(0);
|
||||
}
|
||||
if (groupDepth > groupDepthsByUrlDepth[urlDepth]) {
|
||||
groupDepthsByUrlDepth[urlDepth] = groupDepth;
|
||||
}
|
||||
}
|
||||
// urlDepth tracks the index of the current URL-consuming segment.
|
||||
// Groups accumulate at the same index. When the next URL segment
|
||||
// is reached, it increments the index and resets the group counter.
|
||||
// We start at -1 so the root segment '' increments to 0.
|
||||
function walk(tree, urlDepth, groupDepth) {
|
||||
const segment = tree[0];
|
||||
const { parallelRoutes } = parseLoaderTree(tree);
|
||||
const consumesDepth = segmentConsumesURLDepth(segment);
|
||||
let nextUrlDepth = urlDepth;
|
||||
let nextGroupDepth = groupDepth;
|
||||
if (consumesDepth) {
|
||||
nextUrlDepth = urlDepth + 1;
|
||||
nextGroupDepth = 0;
|
||||
recordGroupDepth(nextUrlDepth, 0);
|
||||
} else if (typeof segment === 'string' && isGroupSegment(segment) && segment !== '(__SLOT__)') {
|
||||
// Count real route groups but not the synthetic '(__SLOT__)' segment
|
||||
// that Next.js inserts for parallel slots. The synthetic group
|
||||
// can't be a real navigation boundary.
|
||||
nextGroupDepth++;
|
||||
recordGroupDepth(urlDepth, nextGroupDepth);
|
||||
}
|
||||
for(const key in parallelRoutes){
|
||||
walk(parallelRoutes[key], nextUrlDepth, nextGroupDepth);
|
||||
}
|
||||
}
|
||||
walk(loaderTree, -1, 0);
|
||||
return groupDepthsByUrlDepth;
|
||||
}
|
||||
export async function createCombinedPayloadAtDepth(initialRSCPayload, cache, initialLoaderTree, getDynamicParamFromSegment, query, depth, groupDepth, releaseSignal, boundaryState, clientReferenceManifest, stageEndTimes, useRuntimeStageForPartialSegments) {
|
||||
let hasStaticSegments = false;
|
||||
let hasRuntimeSegments = false;
|
||||
function getSegment(loaderTree) {
|
||||
const dynamicParam = getDynamicParamFromSegment(loaderTree);
|
||||
if (dynamicParam) {
|
||||
return dynamicParam.treeSegment;
|
||||
}
|
||||
const segment = loaderTree[0];
|
||||
return query ? addSearchParamsIfPageSegment(segment, query) : segment;
|
||||
}
|
||||
async function buildSharedTreeSeedData(loaderTree, parentPath, key, urlDepthConsumed, groupDepthConsumed) {
|
||||
const { parallelRoutes } = parseLoaderTree(loaderTree);
|
||||
const segment = getSegment(loaderTree);
|
||||
const path = parentPath === null ? stringifySegment(segment) : createChildSegmentPath(parentPath, key, segment);
|
||||
debug == null ? void 0 : debug(` ${path || '/'} - Dynamic`);
|
||||
const segmentCacheItem = cache.segments.get(path);
|
||||
if (!segmentCacheItem) {
|
||||
throw Object.defineProperty(new InvariantError(`Missing segment data: ${path}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E995",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const segmentData = await deserializeFromChunks(segmentCacheItem.chunks[RenderStage.Dynamic], segmentCacheItem.chunks[RenderStage.Dynamic], segmentCacheItem.debugChunks, releaseSignal, clientReferenceManifest, null);
|
||||
const consumesUrlDepth = segmentConsumesURLDepth(segment);
|
||||
const isGroup = typeof segment === 'string' && isGroupSegment(segment) && segment !== '(__SLOT__)';
|
||||
// Advance counters for this segment before the boundary check,
|
||||
// mirroring how discoverValidationDepths counts. URL segments
|
||||
// increment urlDepthConsumed, groups increment groupDepthConsumed.
|
||||
// The synthetic '(__SLOT__)' segment is excluded — it can't be a
|
||||
// real navigation boundary.
|
||||
let nextUrlDepth = urlDepthConsumed;
|
||||
let currentGroupDepth = groupDepthConsumed;
|
||||
if (consumesUrlDepth) {
|
||||
nextUrlDepth++;
|
||||
currentGroupDepth = 0;
|
||||
} else if (isGroup) {
|
||||
currentGroupDepth++;
|
||||
}
|
||||
const pastUrlBoundary = nextUrlDepth > depth;
|
||||
const isBoundary = pastUrlBoundary && currentGroupDepth >= groupDepth;
|
||||
if (isBoundary) {
|
||||
debug == null ? void 0 : debug(` ['${path}' is the boundary (url=${nextUrlDepth}, group=${currentGroupDepth})]`);
|
||||
boundaryState.expectedIds.add(path);
|
||||
const finalSegmentData = {
|
||||
...segmentData,
|
||||
node: // eslint-disable-next-line @next/internal/no-ambiguous-jsx -- bundled in the server layer
|
||||
/*#__PURE__*/ _jsx(PlaceValidationBoundaryBelowThisLevel, {
|
||||
id: path,
|
||||
children: segmentData.node
|
||||
}, "c")
|
||||
};
|
||||
const slots = {};
|
||||
let requiresInstantUI = false;
|
||||
let createInstantStack = null;
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
const result = await buildNewTreeSeedData(parallelRoutes[parallelRouteKey], path, parallelRouteKey, false);
|
||||
slots[parallelRouteKey] = result.seedData;
|
||||
if (result.requiresInstantUI) {
|
||||
requiresInstantUI = true;
|
||||
if (createInstantStack === null) {
|
||||
createInstantStack = result.createInstantStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
seedData: getCacheNodeSeedDataFromSegment(finalSegmentData, slots),
|
||||
requiresInstantUI,
|
||||
createInstantStack
|
||||
};
|
||||
}
|
||||
// Not at the boundary yet — keep walking as shared.
|
||||
const slots = {};
|
||||
let requiresInstantUI = false;
|
||||
let createInstantStack = null;
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
const result = await buildSharedTreeSeedData(parallelRoutes[parallelRouteKey], path, parallelRouteKey, nextUrlDepth, currentGroupDepth);
|
||||
slots[parallelRouteKey] = result.seedData;
|
||||
if (result.requiresInstantUI) {
|
||||
requiresInstantUI = true;
|
||||
if (createInstantStack === null) {
|
||||
createInstantStack = result.createInstantStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
seedData: getCacheNodeSeedDataFromSegment(segmentData, slots),
|
||||
requiresInstantUI,
|
||||
createInstantStack
|
||||
};
|
||||
}
|
||||
async function buildNewTreeSeedData(lt, parentPath, key, isInsideRuntimePrefetch) {
|
||||
const { parallelRoutes } = parseLoaderTree(lt);
|
||||
const { mod: layoutOrPageMod } = await getLayoutOrPageModule(lt);
|
||||
const segment = getSegment(lt);
|
||||
const path = parentPath === null ? stringifySegment(segment) : createChildSegmentPath(parentPath, key, segment);
|
||||
let instantConfig = null;
|
||||
let localCreateInstantStack = null;
|
||||
if (layoutOrPageMod !== undefined) {
|
||||
instantConfig = layoutOrPageMod.unstable_instant ?? null;
|
||||
if (instantConfig && typeof instantConfig === 'object') {
|
||||
const rawFactory = layoutOrPageMod.__debugCreateInstantConfigStack;
|
||||
localCreateInstantStack = typeof rawFactory === 'function' ? rawFactory : null;
|
||||
}
|
||||
}
|
||||
let childIsInsideRuntimePrefetch = isInsideRuntimePrefetch;
|
||||
let stage;
|
||||
if (!isInsideRuntimePrefetch) {
|
||||
if (instantConfig && typeof instantConfig === 'object' && instantConfig.prefetch === 'runtime') {
|
||||
stage = RenderStage.Runtime;
|
||||
childIsInsideRuntimePrefetch = true;
|
||||
hasRuntimeSegments = true;
|
||||
} else {
|
||||
if (useRuntimeStageForPartialSegments) {
|
||||
stage = RenderStage.Runtime;
|
||||
hasRuntimeSegments = true;
|
||||
} else {
|
||||
stage = RenderStage.Static;
|
||||
hasStaticSegments = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stage = RenderStage.Runtime;
|
||||
hasRuntimeSegments = true;
|
||||
}
|
||||
debug == null ? void 0 : debug(` ${path || '/'} - ${RenderStage[stage]}`);
|
||||
const segmentCacheItem = cache.segments.get(path);
|
||||
if (!segmentCacheItem) {
|
||||
throw Object.defineProperty(new InvariantError(`Missing segment data: ${path}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E995",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const segmentData = await deserializeFromChunks(segmentCacheItem.chunks[stage], segmentCacheItem.chunks[RenderStage.Dynamic], segmentCacheItem.debugChunks, releaseSignal, clientReferenceManifest, {
|
||||
startTime: undefined,
|
||||
endTime: stageEndTimes[stage]
|
||||
});
|
||||
// Build children first, then determine requiresInstantUI.
|
||||
const slots = {};
|
||||
let childrenRequireInstantUI = false;
|
||||
let childCreateInstantStack = null;
|
||||
for(const parallelRouteKey in parallelRoutes){
|
||||
const result = await buildNewTreeSeedData(parallelRoutes[parallelRouteKey], path, parallelRouteKey, childIsInsideRuntimePrefetch);
|
||||
slots[parallelRouteKey] = result.seedData;
|
||||
if (result.requiresInstantUI) {
|
||||
childrenRequireInstantUI = true;
|
||||
if (childCreateInstantStack === null) {
|
||||
childCreateInstantStack = result.createInstantStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local config takes precedence over children.
|
||||
let requiresInstantUI;
|
||||
let createInstantStack;
|
||||
if (instantConfig === false) {
|
||||
requiresInstantUI = false;
|
||||
createInstantStack = null;
|
||||
} else if (instantConfig && typeof instantConfig === 'object') {
|
||||
requiresInstantUI = true;
|
||||
createInstantStack = localCreateInstantStack;
|
||||
} else {
|
||||
requiresInstantUI = childrenRequireInstantUI;
|
||||
createInstantStack = childCreateInstantStack;
|
||||
}
|
||||
return {
|
||||
seedData: getCacheNodeSeedDataFromSegment(segmentData, slots),
|
||||
requiresInstantUI,
|
||||
createInstantStack
|
||||
};
|
||||
}
|
||||
const { seedData, requiresInstantUI, createInstantStack } = await buildSharedTreeSeedData(initialLoaderTree, null, null, 0 /* urlDepthConsumed */ , 0 /* groupDepthConsumed */ );
|
||||
if (!requiresInstantUI) {
|
||||
return null;
|
||||
}
|
||||
const { flightRouterState } = getRootDataFromPayload(initialRSCPayload);
|
||||
const headStage = hasRuntimeSegments ? RenderStage.Runtime : RenderStage.Static;
|
||||
const head = await createValidationHead(cache, releaseSignal, clientReferenceManifest, stageEndTimes, headStage);
|
||||
const payload = {
|
||||
...initialRSCPayload,
|
||||
f: [
|
||||
[
|
||||
flightRouterState,
|
||||
seedData,
|
||||
head
|
||||
]
|
||||
]
|
||||
};
|
||||
return {
|
||||
payload,
|
||||
hasAmbiguousErrors: hasStaticSegments,
|
||||
createInstantStack
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=instant-validation.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
import { InvariantError } from '../../../shared/lib/invariant-error';
|
||||
/**
|
||||
* When we abort a staged render, we can still provide react with more chunks from later phases
|
||||
* to use for their debug info. This will not cause more contents to be rendered.
|
||||
*/ export function createNodeStreamWithLateRelease(partialChunks, allChunks, releaseSignal) {
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
throw Object.defineProperty(new InvariantError('createNodeStreamWithLateRelease cannot be used in the edge runtime'), "__NEXT_ERROR_CODE", {
|
||||
value: "E993",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
} else {
|
||||
const { Readable } = require('node:stream');
|
||||
let nextIndex = 0;
|
||||
const readable = new Readable({
|
||||
read () {
|
||||
while(nextIndex < partialChunks.length){
|
||||
this.push(partialChunks[nextIndex]);
|
||||
nextIndex++;
|
||||
}
|
||||
}
|
||||
});
|
||||
releaseSignal.addEventListener('abort', ()=>{
|
||||
// Flush any remaining chunks from the original set
|
||||
while(nextIndex < partialChunks.length){
|
||||
readable.push(partialChunks[nextIndex]);
|
||||
nextIndex++;
|
||||
}
|
||||
// Flush all chunks since we're now aborted and can't schedule
|
||||
// any new work but these chunks might unblock debugInfo
|
||||
while(nextIndex < allChunks.length){
|
||||
readable.push(allChunks[nextIndex]);
|
||||
nextIndex++;
|
||||
}
|
||||
setImmediate(()=>{
|
||||
readable.push(null);
|
||||
});
|
||||
}, {
|
||||
once: true
|
||||
});
|
||||
return readable;
|
||||
}
|
||||
}
|
||||
export function createNodeStreamFromChunks(chunks, signal) {
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
throw Object.defineProperty(new InvariantError('createNodeStreamFromChunks cannot be used in the edge runtime'), "__NEXT_ERROR_CODE", {
|
||||
value: "E945",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
} else {
|
||||
const { Readable } = require('node:stream');
|
||||
// If there's a signal, delay closing until it fires
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', ()=>{
|
||||
readable.push(null);
|
||||
}, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
let nextIndex = 0;
|
||||
const readable = new Readable({
|
||||
read () {
|
||||
while(nextIndex < chunks.length){
|
||||
this.push(chunks[nextIndex]);
|
||||
nextIndex++;
|
||||
}
|
||||
if (!signal) {
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
return readable;
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=stream-utils.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/server/app-render/instant-validation/stream-utils.ts"],"sourcesContent":["import type { Readable } from 'node:stream'\nimport { InvariantError } from '../../../shared/lib/invariant-error'\n\n/**\n * When we abort a staged render, we can still provide react with more chunks from later phases\n * to use for their debug info. This will not cause more contents to be rendered.\n */\nexport function createNodeStreamWithLateRelease(\n partialChunks: Array<Uint8Array>,\n allChunks: Array<Uint8Array>,\n releaseSignal: AbortSignal\n): Readable {\n if (process.env.NEXT_RUNTIME === 'edge') {\n throw new InvariantError(\n 'createNodeStreamWithLateRelease cannot be used in the edge runtime'\n )\n } else {\n const { Readable } = require('node:stream') as typeof import('node:stream')\n\n let nextIndex = 0\n\n const readable = new Readable({\n read() {\n while (nextIndex < partialChunks.length) {\n this.push(partialChunks[nextIndex])\n nextIndex++\n }\n },\n })\n\n releaseSignal.addEventListener(\n 'abort',\n () => {\n // Flush any remaining chunks from the original set\n while (nextIndex < partialChunks.length) {\n readable.push(partialChunks[nextIndex])\n nextIndex++\n }\n // Flush all chunks since we're now aborted and can't schedule\n // any new work but these chunks might unblock debugInfo\n while (nextIndex < allChunks.length) {\n readable.push(allChunks[nextIndex])\n nextIndex++\n }\n\n setImmediate(() => {\n readable.push(null)\n })\n },\n { once: true }\n )\n\n return readable\n }\n}\n\nexport function createNodeStreamFromChunks(\n chunks: Array<Uint8Array>,\n signal?: AbortSignal\n): Readable {\n if (process.env.NEXT_RUNTIME === 'edge') {\n throw new InvariantError(\n 'createNodeStreamFromChunks cannot be used in the edge runtime'\n )\n } else {\n const { Readable } = require('node:stream') as typeof import('node:stream')\n\n // If there's a signal, delay closing until it fires\n if (signal) {\n signal.addEventListener(\n 'abort',\n () => {\n readable.push(null)\n },\n { once: true }\n )\n }\n\n let nextIndex = 0\n const readable = new Readable({\n read() {\n while (nextIndex < chunks.length) {\n this.push(chunks[nextIndex])\n nextIndex++\n }\n if (!signal) {\n this.push(null)\n }\n },\n })\n return readable\n }\n}\n"],"names":["InvariantError","createNodeStreamWithLateRelease","partialChunks","allChunks","releaseSignal","process","env","NEXT_RUNTIME","Readable","require","nextIndex","readable","read","length","push","addEventListener","setImmediate","once","createNodeStreamFromChunks","chunks","signal"],"mappings":"AACA,SAASA,cAAc,QAAQ,sCAAqC;AAEpE;;;CAGC,GACD,OAAO,SAASC,gCACdC,aAAgC,EAChCC,SAA4B,EAC5BC,aAA0B;IAE1B,IAAIC,QAAQC,GAAG,CAACC,YAAY,KAAK,QAAQ;QACvC,MAAM,qBAEL,CAFK,IAAIP,eACR,uEADI,qBAAA;mBAAA;wBAAA;0BAAA;QAEN;IACF,OAAO;QACL,MAAM,EAAEQ,QAAQ,EAAE,GAAGC,QAAQ;QAE7B,IAAIC,YAAY;QAEhB,MAAMC,WAAW,IAAIH,SAAS;YAC5BI;gBACE,MAAOF,YAAYR,cAAcW,MAAM,CAAE;oBACvC,IAAI,CAACC,IAAI,CAACZ,aAAa,CAACQ,UAAU;oBAClCA;gBACF;YACF;QACF;QAEAN,cAAcW,gBAAgB,CAC5B,SACA;YACE,mDAAmD;YACnD,MAAOL,YAAYR,cAAcW,MAAM,CAAE;gBACvCF,SAASG,IAAI,CAACZ,aAAa,CAACQ,UAAU;gBACtCA;YACF;YACA,8DAA8D;YAC9D,wDAAwD;YACxD,MAAOA,YAAYP,UAAUU,MAAM,CAAE;gBACnCF,SAASG,IAAI,CAACX,SAAS,CAACO,UAAU;gBAClCA;YACF;YAEAM,aAAa;gBACXL,SAASG,IAAI,CAAC;YAChB;QACF,GACA;YAAEG,MAAM;QAAK;QAGf,OAAON;IACT;AACF;AAEA,OAAO,SAASO,2BACdC,MAAyB,EACzBC,MAAoB;IAEpB,IAAIf,QAAQC,GAAG,CAACC,YAAY,KAAK,QAAQ;QACvC,MAAM,qBAEL,CAFK,IAAIP,eACR,kEADI,qBAAA;mBAAA;wBAAA;0BAAA;QAEN;IACF,OAAO;QACL,MAAM,EAAEQ,QAAQ,EAAE,GAAGC,QAAQ;QAE7B,oDAAoD;QACpD,IAAIW,QAAQ;YACVA,OAAOL,gBAAgB,CACrB,SACA;gBACEJ,SAASG,IAAI,CAAC;YAChB,GACA;gBAAEG,MAAM;YAAK;QAEjB;QAEA,IAAIP,YAAY;QAChB,MAAMC,WAAW,IAAIH,SAAS;YAC5BI;gBACE,MAAOF,YAAYS,OAAON,MAAM,CAAE;oBAChC,IAAI,CAACC,IAAI,CAACK,MAAM,CAACT,UAAU;oBAC3BA;gBACF;gBACA,IAAI,CAACU,QAAQ;oBACX,IAAI,CAACN,IAAI,CAAC;gBACZ;YACF;QACF;QACA,OAAOH;IACT;AACF","ignoreList":[0]}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Interop between "export default" and "module.exports".
|
||||
*/ export function interopDefault(mod) {
|
||||
return mod.default || mod;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=interop-default.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/server/app-render/interop-default.ts"],"sourcesContent":["/**\n * Interop between \"export default\" and \"module.exports\".\n */\nexport function interopDefault(mod: any) {\n return mod.default || mod\n}\n"],"names":["interopDefault","mod","default"],"mappings":"AAAA;;CAEC,GACD,OAAO,SAASA,eAAeC,GAAQ;IACrC,OAAOA,IAAIC,OAAO,IAAID;AACxB","ignoreList":[0]}
|
||||
Generated
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable @next/internal/no-ambiguous-jsx -- whole module is used in React Client */ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import React from 'react';
|
||||
import { isHTTPAccessFallbackError } from '../../client/components/http-access-fallback/http-access-fallback';
|
||||
import { getURLFromRedirectError, getRedirectStatusCodeFromError } from '../../client/components/redirect';
|
||||
import { isRedirectError } from '../../client/components/redirect-error';
|
||||
import { renderToReadableStream } from 'react-dom/server';
|
||||
import { streamToString } from '../stream-utils/node-web-streams-helper';
|
||||
import { RedirectStatusCode } from '../../client/components/redirect-status-code';
|
||||
import { addPathPrefix } from '../../shared/lib/router/utils/add-path-prefix';
|
||||
export function makeGetServerInsertedHTML({ polyfills, renderServerInsertedHTML, serverCapturedErrors, tracingMetadata, basePath }) {
|
||||
let flushedErrorMetaTagsUntilIndex = 0;
|
||||
// These only need to be rendered once, they'll be set to empty arrays once flushed.
|
||||
let polyfillTags = polyfills.map((polyfill)=>{
|
||||
return /*#__PURE__*/ _jsx("script", {
|
||||
...polyfill
|
||||
}, polyfill.src);
|
||||
});
|
||||
let traceMetaTags = (tracingMetadata || []).map(({ key, value }, index)=>/*#__PURE__*/ _jsx("meta", {
|
||||
name: key,
|
||||
content: value
|
||||
}, `next-trace-data-${index}`));
|
||||
return async function getServerInsertedHTML() {
|
||||
// Loop through all the errors that have been captured but not yet
|
||||
// flushed.
|
||||
const errorMetaTags = [];
|
||||
while(flushedErrorMetaTagsUntilIndex < serverCapturedErrors.length){
|
||||
const error = serverCapturedErrors[flushedErrorMetaTagsUntilIndex];
|
||||
flushedErrorMetaTagsUntilIndex++;
|
||||
if (isHTTPAccessFallbackError(error)) {
|
||||
errorMetaTags.push(/*#__PURE__*/ _jsx("meta", {
|
||||
name: "robots",
|
||||
content: "noindex"
|
||||
}, error.digest), process.env.NODE_ENV === 'development' ? /*#__PURE__*/ _jsx("meta", {
|
||||
name: "next-error",
|
||||
content: "not-found"
|
||||
}, "next-error") : null);
|
||||
} else if (isRedirectError(error)) {
|
||||
const redirectUrl = addPathPrefix(getURLFromRedirectError(error), basePath);
|
||||
const statusCode = getRedirectStatusCodeFromError(error);
|
||||
const isPermanent = statusCode === RedirectStatusCode.PermanentRedirect ? true : false;
|
||||
if (redirectUrl) {
|
||||
errorMetaTags.push(/*#__PURE__*/ _jsx("meta", {
|
||||
id: "__next-page-redirect",
|
||||
httpEquiv: "refresh",
|
||||
content: `${isPermanent ? 0 : 1};url=${redirectUrl}`
|
||||
}, error.digest));
|
||||
}
|
||||
}
|
||||
}
|
||||
const serverInsertedHTML = renderServerInsertedHTML();
|
||||
// Skip React rendering if we know the content is empty.
|
||||
if (polyfillTags.length === 0 && traceMetaTags.length === 0 && errorMetaTags.length === 0 && Array.isArray(serverInsertedHTML) && serverInsertedHTML.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const stream = await renderToReadableStream(/*#__PURE__*/ _jsxs(_Fragment, {
|
||||
children: [
|
||||
polyfillTags,
|
||||
serverInsertedHTML,
|
||||
traceMetaTags,
|
||||
errorMetaTags
|
||||
]
|
||||
}), {
|
||||
// Larger chunk because this isn't sent over the network.
|
||||
// Let's set it to 1MB.
|
||||
progressiveChunkSize: 1024 * 1024
|
||||
});
|
||||
// The polyfills and trace metadata have been flushed, so they don't need to be rendered again
|
||||
polyfillTags = [];
|
||||
traceMetaTags = [];
|
||||
// There's no need to wait for the stream to be ready
|
||||
// e.g. calling `await stream.allReady` because `streamToString` will
|
||||
// wait and decode the stream progressively with better parallelism.
|
||||
return streamToString(stream);
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=make-get-server-inserted-html.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+233
@@ -0,0 +1,233 @@
|
||||
import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths';
|
||||
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix';
|
||||
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix';
|
||||
import { workAsyncStorage } from './work-async-storage.external';
|
||||
// This is a global singleton that is, among other things, also used to
|
||||
// encode/decode bound args of server function closures. This can't be using a
|
||||
// AsyncLocalStorage as it might happen at the module level.
|
||||
const MANIFESTS_SINGLETON = Symbol.for('next.server.manifests');
|
||||
const globalThisWithManifests = globalThis;
|
||||
function createProxiedClientReferenceManifest(clientReferenceManifestsPerRoute) {
|
||||
const createMappingProxy = (prop)=>{
|
||||
return new Proxy({}, {
|
||||
get (_, id) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
if (workStore) {
|
||||
const currentManifest = clientReferenceManifestsPerRoute.get(workStore.route);
|
||||
if (currentManifest == null ? void 0 : currentManifest[prop][id]) {
|
||||
return currentManifest[prop][id];
|
||||
}
|
||||
// In development, we also check all other manifests to see if the
|
||||
// module exists there. This is to support a scenario where React's
|
||||
// I/O tracking (dev-only) creates a connection from one page to
|
||||
// another through an emitted async I/O node that references client
|
||||
// components from the other page, e.g. in owner props.
|
||||
// TODO: Maybe we need to add a `debugBundlerConfig` option to React
|
||||
// to avoid this workaround. The current workaround has the
|
||||
// disadvantage that one might accidentally or intentionally share
|
||||
// client references across pages (e.g. by storing them in a global
|
||||
// variable), which would then only be caught in production.
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
for (const [route, manifest] of clientReferenceManifestsPerRoute){
|
||||
if (route === workStore.route) {
|
||||
continue;
|
||||
}
|
||||
const entry = manifest[prop][id];
|
||||
if (entry !== undefined) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no work store defined, we can assume that a client
|
||||
// reference manifest is needed during module evaluation, e.g. to
|
||||
// create a server function using a higher-order function. This
|
||||
// might also use client components which need to be serialized by
|
||||
// Flight, and therefore client references need to be resolvable. In
|
||||
// that case we search all page manifests to find the module.
|
||||
for (const manifest of clientReferenceManifestsPerRoute.values()){
|
||||
const entry = manifest[prop][id];
|
||||
if (entry !== undefined) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
const mappingProxies = new Map();
|
||||
return new Proxy({}, {
|
||||
get (_, prop) {
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
switch(prop){
|
||||
case 'moduleLoading':
|
||||
case 'entryCSSFiles':
|
||||
case 'entryJSFiles':
|
||||
{
|
||||
if (!workStore) {
|
||||
throw Object.defineProperty(new InvariantError(`Cannot access "${prop}" without a work store.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E952",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const currentManifest = clientReferenceManifestsPerRoute.get(workStore.route);
|
||||
if (!currentManifest) {
|
||||
throw Object.defineProperty(new InvariantError(`The client reference manifest for route "${workStore.route}" does not exist.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E951",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return currentManifest[prop];
|
||||
}
|
||||
case 'clientModules':
|
||||
case 'rscModuleMapping':
|
||||
case 'edgeRscModuleMapping':
|
||||
case 'ssrModuleMapping':
|
||||
case 'edgeSSRModuleMapping':
|
||||
{
|
||||
let proxy = mappingProxies.get(prop);
|
||||
if (!proxy) {
|
||||
proxy = createMappingProxy(prop);
|
||||
mappingProxies.set(prop, proxy);
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw Object.defineProperty(new InvariantError(`This is a proxied client reference manifest. The property "${String(prop)}" is not handled.`), "__NEXT_ERROR_CODE", {
|
||||
value: "E953",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* This function creates a Flight-acceptable server module map proxy from our
|
||||
* Server Reference Manifest similar to our client module map. This is because
|
||||
* our manifest contains a lot of internal Next.js data that are relevant to the
|
||||
* runtime, workers, etc. that React doesn't need to know.
|
||||
*/ function createServerModuleMap() {
|
||||
return new Proxy({}, {
|
||||
get: (_, id)=>{
|
||||
var _getServerActionsManifest__id, _getServerActionsManifest_;
|
||||
const workers = (_getServerActionsManifest_ = getServerActionsManifest()[process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node']) == null ? void 0 : (_getServerActionsManifest__id = _getServerActionsManifest_[id]) == null ? void 0 : _getServerActionsManifest__id.workers;
|
||||
if (!workers) {
|
||||
return undefined;
|
||||
}
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
let workerEntry;
|
||||
if (workStore) {
|
||||
workerEntry = workers[normalizeWorkerPageName(workStore.page)];
|
||||
} else {
|
||||
// If there's no work store defined, we can assume that a server
|
||||
// module map is needed during module evaluation, e.g. to create a
|
||||
// server action using a higher-order function. Therefore it should be
|
||||
// safe to return any entry from the manifest that matches the action
|
||||
// ID. They all refer to the same module ID, which must also exist in
|
||||
// the current page bundle. TODO: This is currently not guaranteed in
|
||||
// Turbopack, and needs to be fixed.
|
||||
workerEntry = Object.values(workers).at(0);
|
||||
}
|
||||
if (!workerEntry) {
|
||||
return undefined;
|
||||
}
|
||||
const { moduleId, async } = workerEntry;
|
||||
return {
|
||||
id: moduleId,
|
||||
name: id,
|
||||
chunks: [],
|
||||
async
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* The flight entry loader keys actions by bundlePath. bundlePath corresponds
|
||||
* with the relative path (including 'app') to the page entrypoint.
|
||||
*/ function normalizeWorkerPageName(pageName) {
|
||||
if (pathHasPrefix(pageName, 'app')) {
|
||||
return pageName;
|
||||
}
|
||||
return 'app' + pageName;
|
||||
}
|
||||
/**
|
||||
* Converts a bundlePath (relative path to the entrypoint) to a routable page
|
||||
* name.
|
||||
*/ function denormalizeWorkerPageName(bundlePath) {
|
||||
return normalizeAppPath(removePathPrefix(bundlePath, 'app'));
|
||||
}
|
||||
/**
|
||||
* Checks if the requested action has a worker for the current page.
|
||||
* If not, it returns the first worker that has a handler for the action.
|
||||
*/ export function selectWorkerForForwarding(actionId, pageName) {
|
||||
var _serverActionsManifest__actionId;
|
||||
const serverActionsManifest = getServerActionsManifest();
|
||||
const workers = (_serverActionsManifest__actionId = serverActionsManifest[process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'][actionId]) == null ? void 0 : _serverActionsManifest__actionId.workers;
|
||||
// There are no workers to handle this action, nothing to forward to.
|
||||
if (!workers) {
|
||||
return;
|
||||
}
|
||||
// If there is an entry for the current page, we don't need to forward.
|
||||
if (workers[normalizeWorkerPageName(pageName)]) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, grab the first worker that has a handler for this action id.
|
||||
return denormalizeWorkerPageName(Object.keys(workers)[0]);
|
||||
}
|
||||
export function setManifestsSingleton({ page, clientReferenceManifest, serverActionsManifest: rawServerActionsManifest }) {
|
||||
const existingSingleton = globalThisWithManifests[MANIFESTS_SINGLETON];
|
||||
const serverActionsManifest = {
|
||||
encryptionKey: rawServerActionsManifest.encryptionKey,
|
||||
// Use null-prototypes for the action objects to prevent prototype pollution
|
||||
// from affecting action ID lookups.
|
||||
node: Object.assign(Object.create(null), rawServerActionsManifest.node),
|
||||
edge: Object.assign(Object.create(null), rawServerActionsManifest.edge)
|
||||
};
|
||||
if (existingSingleton) {
|
||||
existingSingleton.clientReferenceManifestsPerRoute.set(normalizeAppPath(page), clientReferenceManifest);
|
||||
existingSingleton.serverActionsManifest = serverActionsManifest;
|
||||
} else {
|
||||
const clientReferenceManifestsPerRoute = new Map([
|
||||
[
|
||||
normalizeAppPath(page),
|
||||
clientReferenceManifest
|
||||
]
|
||||
]);
|
||||
const proxiedClientReferenceManifest = createProxiedClientReferenceManifest(clientReferenceManifestsPerRoute);
|
||||
globalThisWithManifests[MANIFESTS_SINGLETON] = {
|
||||
clientReferenceManifestsPerRoute,
|
||||
proxiedClientReferenceManifest,
|
||||
serverActionsManifest,
|
||||
serverModuleMap: createServerModuleMap()
|
||||
};
|
||||
}
|
||||
}
|
||||
function getManifestsSingleton() {
|
||||
const manifestSingleton = globalThisWithManifests[MANIFESTS_SINGLETON];
|
||||
if (!manifestSingleton) {
|
||||
throw Object.defineProperty(new InvariantError('The manifests singleton was not initialized.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E950",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return manifestSingleton;
|
||||
}
|
||||
export function getClientReferenceManifest() {
|
||||
return getManifestsSingleton().proxiedClientReferenceManifest;
|
||||
}
|
||||
export function getServerActionsManifest() {
|
||||
return getManifestsSingleton().serverActionsManifest;
|
||||
}
|
||||
export function getServerModuleMap() {
|
||||
return getManifestsSingleton().serverModuleMap;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=manifests-singleton.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* For chromium based browsers (Chrome, Edge, etc.) and Safari,
|
||||
* icons need to stay under <head> to be picked up by the browser.
|
||||
*
|
||||
*/ const REINSERT_ICON_SCRIPT = `\
|
||||
document.querySelectorAll('body link[rel="icon"], body link[rel="apple-touch-icon"]').forEach(el => document.head.appendChild(el))`;
|
||||
export function createServerInsertedMetadata(nonce) {
|
||||
let inserted = false;
|
||||
return async function getServerInsertedMetadata() {
|
||||
if (inserted) {
|
||||
return '';
|
||||
}
|
||||
inserted = true;
|
||||
return `<script ${nonce ? `nonce="${nonce}"` : ''}>${REINSERT_ICON_SCRIPT}</script>`;
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-server-inserted-metadata.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/server/app-render/metadata-insertion/create-server-inserted-metadata.tsx"],"sourcesContent":["/**\n * For chromium based browsers (Chrome, Edge, etc.) and Safari,\n * icons need to stay under <head> to be picked up by the browser.\n *\n */\nconst REINSERT_ICON_SCRIPT = `\\\ndocument.querySelectorAll('body link[rel=\"icon\"], body link[rel=\"apple-touch-icon\"]').forEach(el => document.head.appendChild(el))`\n\nexport function createServerInsertedMetadata(nonce: string | undefined) {\n let inserted = false\n\n return async function getServerInsertedMetadata(): Promise<string> {\n if (inserted) {\n return ''\n }\n\n inserted = true\n return `<script ${nonce ? `nonce=\"${nonce}\"` : ''}>${REINSERT_ICON_SCRIPT}</script>`\n }\n}\n"],"names":["REINSERT_ICON_SCRIPT","createServerInsertedMetadata","nonce","inserted","getServerInsertedMetadata"],"mappings":"AAAA;;;;CAIC,GACD,MAAMA,uBAAuB,CAAC;kIACoG,CAAC;AAEnI,OAAO,SAASC,6BAA6BC,KAAyB;IACpE,IAAIC,WAAW;IAEf,OAAO,eAAeC;QACpB,IAAID,UAAU;YACZ,OAAO;QACT;QAEAA,WAAW;QACX,OAAO,CAAC,QAAQ,EAAED,QAAQ,CAAC,OAAO,EAAEA,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,EAAEF,qBAAqB,SAAS,CAAC;IACtF;AACF","ignoreList":[0]}
|
||||
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
import { InvariantError } from '../../../shared/lib/invariant-error';
|
||||
import { isThenable } from '../../../shared/lib/is-thenable';
|
||||
import { trackPendingImport } from './track-module-loading.external';
|
||||
/**
|
||||
* in CacheComponents, `import(...)` will be transformed into `trackDynamicImport(import(...))`.
|
||||
* A dynamic import is essentially a cached async function, except it's cached by the module system.
|
||||
*
|
||||
* The promises are tracked globally regardless of if the `import()` happens inside a render or outside of it.
|
||||
* When rendering, we can make the `cacheSignal` wait for all pending promises via `trackPendingModules`.
|
||||
* */ export function trackDynamicImport(modulePromise) {
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
throw Object.defineProperty(new InvariantError("Dynamic imports should not be instrumented in the edge runtime, because `cacheComponents` doesn't support it"), "__NEXT_ERROR_CODE", {
|
||||
value: "E687",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
if (!isThenable(modulePromise)) {
|
||||
// We're expecting `import()` to always return a promise. If it's not, something's very wrong.
|
||||
throw Object.defineProperty(new InvariantError('`trackDynamicImport` should always receive a promise. Something went wrong in the dynamic imports transform.'), "__NEXT_ERROR_CODE", {
|
||||
value: "E677",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
// Even if we're inside a prerender and have `workUnitStore.cacheSignal`, we always track the promise globally.
|
||||
// (i.e. via the global `moduleLoadingSignal` that `trackPendingImport` uses internally).
|
||||
//
|
||||
// We do this because the `import()` promise might be cached in userspace:
|
||||
// (which is quite common for e.g. lazy initialization in libraries)
|
||||
//
|
||||
// let promise;
|
||||
// function doDynamicImportOnce() {
|
||||
// if (!promise) {
|
||||
// promise = import("...");
|
||||
// // transformed into:
|
||||
// // promise = trackDynamicImport(import("..."));
|
||||
// }
|
||||
// return promise;
|
||||
// }
|
||||
//
|
||||
// If multiple prerenders (e.g. multiple pages) depend on `doDynamicImportOnce`,
|
||||
// we have to wait for the import *in all of them*.
|
||||
// If we only tracked it using `workUnitStore.cacheSignal.trackRead()`,
|
||||
// then only the first prerender to call `doDynamicImportOnce` would wait --
|
||||
// Subsequent prerenders would re-use the existing `promise`,
|
||||
// and `trackDynamicImport` wouldn't be called again in their scope,
|
||||
// so their respective CacheSignals wouldn't wait for the promise.
|
||||
trackPendingImport(modulePromise);
|
||||
return modulePromise;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=track-dynamic-import.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/server/app-render/module-loading/track-dynamic-import.ts"],"sourcesContent":["import { InvariantError } from '../../../shared/lib/invariant-error'\nimport { isThenable } from '../../../shared/lib/is-thenable'\nimport { trackPendingImport } from './track-module-loading.external'\n\n/**\n * in CacheComponents, `import(...)` will be transformed into `trackDynamicImport(import(...))`.\n * A dynamic import is essentially a cached async function, except it's cached by the module system.\n *\n * The promises are tracked globally regardless of if the `import()` happens inside a render or outside of it.\n * When rendering, we can make the `cacheSignal` wait for all pending promises via `trackPendingModules`.\n * */\nexport function trackDynamicImport<TExports extends Record<string, any>>(\n modulePromise: Promise<TExports>\n): Promise<TExports> {\n if (process.env.NEXT_RUNTIME === 'edge') {\n throw new InvariantError(\n \"Dynamic imports should not be instrumented in the edge runtime, because `cacheComponents` doesn't support it\"\n )\n }\n\n if (!isThenable(modulePromise)) {\n // We're expecting `import()` to always return a promise. If it's not, something's very wrong.\n throw new InvariantError(\n '`trackDynamicImport` should always receive a promise. Something went wrong in the dynamic imports transform.'\n )\n }\n\n // Even if we're inside a prerender and have `workUnitStore.cacheSignal`, we always track the promise globally.\n // (i.e. via the global `moduleLoadingSignal` that `trackPendingImport` uses internally).\n //\n // We do this because the `import()` promise might be cached in userspace:\n // (which is quite common for e.g. lazy initialization in libraries)\n //\n // let promise;\n // function doDynamicImportOnce() {\n // if (!promise) {\n // promise = import(\"...\");\n // // transformed into:\n // // promise = trackDynamicImport(import(\"...\"));\n // }\n // return promise;\n // }\n //\n // If multiple prerenders (e.g. multiple pages) depend on `doDynamicImportOnce`,\n // we have to wait for the import *in all of them*.\n // If we only tracked it using `workUnitStore.cacheSignal.trackRead()`,\n // then only the first prerender to call `doDynamicImportOnce` would wait --\n // Subsequent prerenders would re-use the existing `promise`,\n // and `trackDynamicImport` wouldn't be called again in their scope,\n // so their respective CacheSignals wouldn't wait for the promise.\n trackPendingImport(modulePromise)\n\n return modulePromise\n}\n"],"names":["InvariantError","isThenable","trackPendingImport","trackDynamicImport","modulePromise","process","env","NEXT_RUNTIME"],"mappings":"AAAA,SAASA,cAAc,QAAQ,sCAAqC;AACpE,SAASC,UAAU,QAAQ,kCAAiC;AAC5D,SAASC,kBAAkB,QAAQ,kCAAiC;AAEpE;;;;;;GAMG,GACH,OAAO,SAASC,mBACdC,aAAgC;IAEhC,IAAIC,QAAQC,GAAG,CAACC,YAAY,KAAK,QAAQ;QACvC,MAAM,qBAEL,CAFK,IAAIP,eACR,iHADI,qBAAA;mBAAA;wBAAA;0BAAA;QAEN;IACF;IAEA,IAAI,CAACC,WAAWG,gBAAgB;QAC9B,8FAA8F;QAC9F,MAAM,qBAEL,CAFK,IAAIJ,eACR,iHADI,qBAAA;mBAAA;wBAAA;0BAAA;QAEN;IACF;IAEA,+GAA+G;IAC/G,yFAAyF;IACzF,EAAE;IACF,0EAA0E;IAC1E,oEAAoE;IACpE,EAAE;IACF,iBAAiB;IACjB,qCAAqC;IACrC,sBAAsB;IACtB,iCAAiC;IACjC,6BAA6B;IAC7B,wDAAwD;IACxD,QAAQ;IACR,sBAAsB;IACtB,MAAM;IACN,EAAE;IACF,gFAAgF;IAChF,mDAAmD;IACnD,uEAAuE;IACvE,4EAA4E;IAC5E,6DAA6D;IAC7D,oEAAoE;IACpE,kEAAkE;IAClEE,mBAAmBE;IAEnB,OAAOA;AACT","ignoreList":[0]}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user