This commit is contained in:
2026-04-12 01:02:14 +08:00
parent 509487f155
commit 9b053e302b
14085 changed files with 2680009 additions and 12 deletions

View File

@@ -0,0 +1,3 @@
export declare const DOC_PREFETCH_RANGE_HEADER_VALUE = "bytes=0-63";
export declare function insertBuildIdComment(originalHtml: string, buildId: string): string;
export declare function doesExportedHtmlMatchBuildId(partialHtmlDocument: string, buildId: string): boolean;

View File

@@ -0,0 +1,73 @@
// In output: export mode, the build id is added to the start of the HTML
// document, directly after the doctype declaration. During a prefetch, the
// client performs a range request to get the build id, so it can check whether
// the target page belongs to the same build.
//
// The first 64 bytes of the document are requested. The exact number isn't
// too important; it must be larger than the build id + doctype + closing and
// ending comment markers, but it doesn't need to match the end of the
// comment exactly.
//
// Build ids are 21 bytes long in the default implementation, though this
// can be overridden in the Next.js config. For the purposes of this check,
// it's OK to only match the start of the id, so we'll truncate it if exceeds
// a certain length.
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
DOC_PREFETCH_RANGE_HEADER_VALUE: null,
doesExportedHtmlMatchBuildId: null,
insertBuildIdComment: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
DOC_PREFETCH_RANGE_HEADER_VALUE: function() {
return DOC_PREFETCH_RANGE_HEADER_VALUE;
},
doesExportedHtmlMatchBuildId: function() {
return doesExportedHtmlMatchBuildId;
},
insertBuildIdComment: function() {
return insertBuildIdComment;
}
});
const DOCTYPE_PREFIX = '<!DOCTYPE html>' // 15 bytes
;
const MAX_BUILD_ID_LENGTH = 24;
const DOC_PREFETCH_RANGE_HEADER_VALUE = 'bytes=0-63';
function escapeBuildId(buildId) {
// If the build id is longer than the given limit, it's OK for our purposes
// to only match the beginning.
const truncated = buildId.slice(0, MAX_BUILD_ID_LENGTH);
// Replace hyphens with underscores so it doesn't break the HTML comment.
// (Unlikely, but if this did happen it would break the whole document.)
return truncated.replace(/-/g, '_');
}
function insertBuildIdComment(originalHtml, buildId) {
if (// Skip if the build id contains a closing comment marker.
buildId.includes('-->') || // React always inserts a doctype at the start of the document. Skip if it
// isn't present. Shouldn't happen; suggests an issue elsewhere.
!originalHtml.startsWith(DOCTYPE_PREFIX)) {
// Return the original HTML unchanged. This means the document will not
// be prefetched.
// TODO: The build id comment is currently only used during prefetches, but
// if we eventually use this mechanism for regular navigations, we may need
// to error during build if we fail to insert it for some reason.
return originalHtml;
}
// The comment must be inserted after the doctype.
return originalHtml.replace(DOCTYPE_PREFIX, DOCTYPE_PREFIX + '<!--' + escapeBuildId(buildId) + '-->');
}
function doesExportedHtmlMatchBuildId(partialHtmlDocument, buildId) {
// Check whether the document starts with the expected buildId.
return partialHtmlDocument.startsWith(DOCTYPE_PREFIX + '<!--' + escapeBuildId(buildId) + '-->');
}
//# sourceMappingURL=output-export-prefetch-encoding.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/shared/lib/segment-cache/output-export-prefetch-encoding.ts"],"sourcesContent":["// In output: export mode, the build id is added to the start of the HTML\n// document, directly after the doctype declaration. During a prefetch, the\n// client performs a range request to get the build id, so it can check whether\n// the target page belongs to the same build.\n//\n// The first 64 bytes of the document are requested. The exact number isn't\n// too important; it must be larger than the build id + doctype + closing and\n// ending comment markers, but it doesn't need to match the end of the\n// comment exactly.\n//\n// Build ids are 21 bytes long in the default implementation, though this\n// can be overridden in the Next.js config. For the purposes of this check,\n// it's OK to only match the start of the id, so we'll truncate it if exceeds\n// a certain length.\n\nconst DOCTYPE_PREFIX = '<!DOCTYPE html>' // 15 bytes\nconst MAX_BUILD_ID_LENGTH = 24\n\n// Request the first 64 bytes. The Range header is inclusive of the end value.\nexport const DOC_PREFETCH_RANGE_HEADER_VALUE = 'bytes=0-63'\n\nfunction escapeBuildId(buildId: string) {\n // If the build id is longer than the given limit, it's OK for our purposes\n // to only match the beginning.\n const truncated = buildId.slice(0, MAX_BUILD_ID_LENGTH)\n // Replace hyphens with underscores so it doesn't break the HTML comment.\n // (Unlikely, but if this did happen it would break the whole document.)\n return truncated.replace(/-/g, '_')\n}\n\nexport function insertBuildIdComment(originalHtml: string, buildId: string) {\n if (\n // Skip if the build id contains a closing comment marker.\n buildId.includes('-->') ||\n // React always inserts a doctype at the start of the document. Skip if it\n // isn't present. Shouldn't happen; suggests an issue elsewhere.\n !originalHtml.startsWith(DOCTYPE_PREFIX)\n ) {\n // Return the original HTML unchanged. This means the document will not\n // be prefetched.\n // TODO: The build id comment is currently only used during prefetches, but\n // if we eventually use this mechanism for regular navigations, we may need\n // to error during build if we fail to insert it for some reason.\n return originalHtml\n }\n // The comment must be inserted after the doctype.\n return originalHtml.replace(\n DOCTYPE_PREFIX,\n DOCTYPE_PREFIX + '<!--' + escapeBuildId(buildId) + '-->'\n )\n}\n\nexport function doesExportedHtmlMatchBuildId(\n partialHtmlDocument: string,\n buildId: string\n) {\n // Check whether the document starts with the expected buildId.\n return partialHtmlDocument.startsWith(\n DOCTYPE_PREFIX + '<!--' + escapeBuildId(buildId) + '-->'\n )\n}\n"],"names":["DOC_PREFETCH_RANGE_HEADER_VALUE","doesExportedHtmlMatchBuildId","insertBuildIdComment","DOCTYPE_PREFIX","MAX_BUILD_ID_LENGTH","escapeBuildId","buildId","truncated","slice","replace","originalHtml","includes","startsWith","partialHtmlDocument"],"mappings":"AAAA,yEAAyE;AACzE,2EAA2E;AAC3E,+EAA+E;AAC/E,6CAA6C;AAC7C,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,sEAAsE;AACtE,mBAAmB;AACnB,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,6EAA6E;AAC7E,oBAAoB;;;;;;;;;;;;;;;;;IAMPA,+BAA+B;eAA/BA;;IAiCGC,4BAA4B;eAA5BA;;IAtBAC,oBAAoB;eAApBA;;;AAfhB,MAAMC,iBAAiB,kBAAkB,WAAW;;AACpD,MAAMC,sBAAsB;AAGrB,MAAMJ,kCAAkC;AAE/C,SAASK,cAAcC,OAAe;IACpC,2EAA2E;IAC3E,+BAA+B;IAC/B,MAAMC,YAAYD,QAAQE,KAAK,CAAC,GAAGJ;IACnC,yEAAyE;IACzE,wEAAwE;IACxE,OAAOG,UAAUE,OAAO,CAAC,MAAM;AACjC;AAEO,SAASP,qBAAqBQ,YAAoB,EAAEJ,OAAe;IACxE,IACE,0DAA0D;IAC1DA,QAAQK,QAAQ,CAAC,UACjB,0EAA0E;IAC1E,gEAAgE;IAChE,CAACD,aAAaE,UAAU,CAACT,iBACzB;QACA,uEAAuE;QACvE,iBAAiB;QACjB,2EAA2E;QAC3E,2EAA2E;QAC3E,iEAAiE;QACjE,OAAOO;IACT;IACA,kDAAkD;IAClD,OAAOA,aAAaD,OAAO,CACzBN,gBACAA,iBAAiB,SAASE,cAAcC,WAAW;AAEvD;AAEO,SAASL,6BACdY,mBAA2B,EAC3BP,OAAe;IAEf,+DAA+D;IAC/D,OAAOO,oBAAoBD,UAAU,CACnCT,iBAAiB,SAASE,cAAcC,WAAW;AAEvD","ignoreList":[0]}

View File

@@ -0,0 +1,16 @@
import type { Segment as FlightRouterStateSegment } from '../../../server/app-render/types';
type Opaque<K, T> = T & {
__brand: K;
};
export type SegmentRequestKeyPart = Opaque<'SegmentRequestKeyPart', string>;
export type SegmentRequestKey = Opaque<'SegmentRequestKey', string>;
export type SegmentCacheKeyPart = Opaque<'SegmentCacheKeyPart', string>;
export type SegmentCacheKey = Opaque<'SegmentCacheKey', string>;
export declare const ROOT_SEGMENT_REQUEST_KEY: SegmentRequestKey;
export declare const ROOT_SEGMENT_CACHE_KEY: SegmentCacheKey;
export declare function createSegmentRequestKeyPart(segment: FlightRouterStateSegment): SegmentRequestKeyPart;
export declare function appendSegmentRequestKeyPart(parentRequestKey: SegmentRequestKey, parallelRouteKey: string, childRequestKeyPart: SegmentRequestKeyPart): SegmentRequestKey;
export declare function createSegmentCacheKeyPart(requestKeyPart: SegmentRequestKeyPart, segment: FlightRouterStateSegment): SegmentCacheKeyPart;
export declare function appendSegmentCacheKeyPart(parentSegmentKey: SegmentCacheKey, parallelRouteKey: string, childCacheKeyPart: SegmentCacheKeyPart): SegmentCacheKey;
export declare function convertSegmentPathToStaticExportFilename(segmentPath: string): string;
export {};

View File

@@ -0,0 +1,119 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
ROOT_SEGMENT_CACHE_KEY: null,
ROOT_SEGMENT_REQUEST_KEY: null,
appendSegmentCacheKeyPart: null,
appendSegmentRequestKeyPart: null,
convertSegmentPathToStaticExportFilename: null,
createSegmentCacheKeyPart: null,
createSegmentRequestKeyPart: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
ROOT_SEGMENT_CACHE_KEY: function() {
return ROOT_SEGMENT_CACHE_KEY;
},
ROOT_SEGMENT_REQUEST_KEY: function() {
return ROOT_SEGMENT_REQUEST_KEY;
},
appendSegmentCacheKeyPart: function() {
return appendSegmentCacheKeyPart;
},
appendSegmentRequestKeyPart: function() {
return appendSegmentRequestKeyPart;
},
convertSegmentPathToStaticExportFilename: function() {
return convertSegmentPathToStaticExportFilename;
},
createSegmentCacheKeyPart: function() {
return createSegmentCacheKeyPart;
},
createSegmentRequestKeyPart: function() {
return createSegmentRequestKeyPart;
}
});
const _segment = require("../segment");
const ROOT_SEGMENT_REQUEST_KEY = '';
const ROOT_SEGMENT_CACHE_KEY = '';
function createSegmentRequestKeyPart(segment) {
if (typeof segment === 'string') {
if (segment.startsWith(_segment.PAGE_SEGMENT_KEY)) {
// The Flight Router State type sometimes includes the search params in
// the page segment. However, the Segment Cache tracks this as a separate
// key. So, we strip the search params here, and then add them back when
// the cache entry is turned back into a FlightRouterState. This is an
// unfortunate consequence of the FlightRouteState being used both as a
// transport type and as a cache key; we'll address this once more of the
// Segment Cache implementation has settled.
// TODO: We should hoist the search params out of the FlightRouterState
// type entirely, This is our plan for dynamic route params, too.
return _segment.PAGE_SEGMENT_KEY;
}
const safeName = // TODO: FlightRouterState encodes Not Found routes as "/_not-found".
// But params typically don't include the leading slash. We should use
// a different encoding to avoid this special case.
segment === '/_not-found' ? '_not-found' : encodeToFilesystemAndURLSafeString(segment);
// Since this is not a dynamic segment, it's fully encoded. It does not
// need to be "hydrated" with a param value.
return safeName;
}
const name = segment[0];
const paramType = segment[2];
const safeName = encodeToFilesystemAndURLSafeString(name);
const encodedName = '$' + paramType + '$' + safeName;
return encodedName;
}
function appendSegmentRequestKeyPart(parentRequestKey, parallelRouteKey, childRequestKeyPart) {
// Aside from being filesystem safe, segment keys are also designed so that
// each segment and parallel route creates its own subdirectory. Roughly in
// the same shape as the source app directory. This is mostly just for easier
// debugging (you can open up the build folder and navigate the output); if
// we wanted to do we could just use a flat structure.
// Omit the parallel route key for children, since this is the most
// common case. Saves some bytes (and it's what the app directory does).
const slotKey = parallelRouteKey === 'children' ? childRequestKeyPart : "@" + encodeToFilesystemAndURLSafeString(parallelRouteKey) + "/" + childRequestKeyPart;
return parentRequestKey + '/' + slotKey;
}
function createSegmentCacheKeyPart(requestKeyPart, segment) {
if (typeof segment === 'string') {
return requestKeyPart;
}
const paramValue = segment[1];
const safeValue = encodeToFilesystemAndURLSafeString(paramValue);
return requestKeyPart + '$' + safeValue;
}
function appendSegmentCacheKeyPart(parentSegmentKey, parallelRouteKey, childCacheKeyPart) {
const slotKey = parallelRouteKey === 'children' ? childCacheKeyPart : "@" + encodeToFilesystemAndURLSafeString(parallelRouteKey) + "/" + childCacheKeyPart;
return parentSegmentKey + '/' + slotKey;
}
// Define a regex pattern to match the most common characters found in a route
// param. It excludes anything that might not be cross-platform filesystem
// compatible, like |. It does not need to be precise because the fallback is to
// just base64url-encode the whole parameter, which is fine; we just don't do it
// by default for compactness, and for easier debugging.
const simpleParamValueRegex = /^[a-zA-Z0-9\-_@]+$/;
function encodeToFilesystemAndURLSafeString(value) {
if (simpleParamValueRegex.test(value)) {
return value;
}
// If there are any unsafe characters, base64url-encode the entire value.
// We also add a ! prefix so it doesn't collide with the simple case.
const base64url = btoa(value).replace(/\+/g, '-') // Replace '+' with '-'
.replace(/\//g, '_') // Replace '/' with '_'
.replace(/=+$/, '') // Remove trailing '='
;
return '!' + base64url;
}
function convertSegmentPathToStaticExportFilename(segmentPath) {
return "__next" + segmentPath.replace(/\//g, '.') + ".txt";
}
//# sourceMappingURL=segment-value-encoding.js.map

File diff suppressed because one or more lines are too long