@thomas.f/prnext 0.1.17-alpha → 0.1.19-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -868
- package/build/actions.mjs +27 -15
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +122 -46
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +34 -10
- package/build/config.mjs +34 -3
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +18 -14
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +14 -7
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +90 -28
- package/build/instrumentation.mjs +30 -0
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +1 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +59 -10
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/standalone.mjs +21 -3
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +19 -6
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +70 -5
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +15 -3
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +152 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +69 -4
- package/compat/link.cjs +13 -10
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +248 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +93 -0
- package/docs/ai/troubleshooting.md +116 -2
- package/package.json +40 -9
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +316 -92
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +7 -2
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +13 -3
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +83 -25
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +74 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/fs.mjs +15 -8
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
package/compat/index.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface InstantSample {
|
|
|
13
13
|
}
|
|
14
14
|
export type InstantConfig = boolean | { level?: 'warning' | 'experimental-error'; unstable_samples?: InstantSample[]; unstable_disableValidation?: true; unstable_disableDevValidation?: true; unstable_disableBuildValidation?: true };
|
|
15
15
|
|
|
16
|
+
export interface ReactCompilerOptions {
|
|
17
|
+
compilationMode?: 'infer' | 'annotation';
|
|
18
|
+
panicThreshold?: 'none' | 'critical_errors' | 'all_errors';
|
|
19
|
+
/** PRNext requires matching React 19 peers. */
|
|
20
|
+
target?: '19';
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
export type TurbopackCondition = 'browser' | 'foreign' | 'development' | 'production' | 'node' | 'edge-light'
|
|
17
24
|
| {all: TurbopackCondition[]} | {any: TurbopackCondition[]} | {not: TurbopackCondition}
|
|
18
25
|
| {path: string | RegExp; content?: RegExp} | {path?: string | RegExp; content: RegExp};
|
|
@@ -117,9 +124,17 @@ export interface NextConfig {
|
|
|
117
124
|
/** PRNext only. CLI > environment > build configuration > profile.
|
|
118
125
|
* 0 disables the corresponding retirement; keepWarm retains one lane per used host.
|
|
119
126
|
*/
|
|
127
|
+
/** Soft RSS retirement after three one-second samples; active work drains. */
|
|
128
|
+
workerMemory?: false | { maxRssMb: number };
|
|
120
129
|
workerRetirement?: { idleSeconds?: number; laneIdleSeconds?: number; keepWarm?: boolean };
|
|
121
130
|
/** Import selected Node routes on startup; top-level module code executes. RSS is a soft import budget. */
|
|
122
131
|
workerPreload?: false | { routes: string[]; maxRssMb?: number; timeoutMs?: number };
|
|
132
|
+
/** Keep bounded previous public hashed assets. false disables carry-over. Defaults: one hour, two generations, 64 MiB. */
|
|
133
|
+
deploymentRetention?: false | {maxAge?: number; maxGenerations?: number; maxBytes?: number};
|
|
134
|
+
/** PRNext production prerender processes; maxMemoryMb bounds their combined observed memory. */
|
|
135
|
+
prerender?: {workers?: number; maxMemoryMb?: number};
|
|
136
|
+
/** Explicit App route patterns that must produce complete static HTML without browser JavaScript. */
|
|
137
|
+
htmlOnly?: string[];
|
|
123
138
|
/** Bounded active development routes; default retains five requested routes. */
|
|
124
139
|
onDemandEntries?: { maxInactiveAge?: number; pagesBufferLength?: number };
|
|
125
140
|
/** PRNext development menu; bottom-left by default. false hides it without disabling errors or Fast Refresh. */
|
|
@@ -130,8 +145,8 @@ export interface NextConfig {
|
|
|
130
145
|
images?: import('./image.cjs').ImageConfig;
|
|
131
146
|
pageExtensions?: string[];
|
|
132
147
|
reactStrictMode?: boolean;
|
|
133
|
-
/**
|
|
134
|
-
reactCompiler?:
|
|
148
|
+
/** Opt in to the bundled official React Compiler for application components. Disabled by default. */
|
|
149
|
+
reactCompiler?: boolean | ReactCompilerOptions;
|
|
135
150
|
/** Generate route completions and validate Link/router destinations. */
|
|
136
151
|
typedRoutes?: boolean;
|
|
137
152
|
/** Native CSS for default styles; custom PostCSS pipelines remain authoritative. */
|
|
@@ -139,6 +154,9 @@ export interface NextConfig {
|
|
|
139
154
|
/** Production checks use the project's installed TypeScript without emitting files. */
|
|
140
155
|
typescript?: { ignoreBuildErrors?: boolean; tsconfigPath?: string; devChecks?: boolean };
|
|
141
156
|
cacheComponents?: boolean;
|
|
157
|
+
partialPrefetching?: boolean;
|
|
158
|
+
/** App Router inactive layout branches. Defaults to 4 entries for 120 seconds. */
|
|
159
|
+
navigationCache?: false | {maxEntries?: number; ttlMs?: number; authCookies?: readonly string[]};
|
|
142
160
|
cacheLife?: Record<string, import('./cache.cjs').CacheLife>;
|
|
143
161
|
transpilePackages?: string[];
|
|
144
162
|
serverExternalPackages?: string[];
|
|
@@ -149,7 +167,7 @@ export interface NextConfig {
|
|
|
149
167
|
implementation?: 'sass' | 'sass-embedded';
|
|
150
168
|
[option: string]: unknown;
|
|
151
169
|
};
|
|
152
|
-
experimental?: { nextScriptWorkers?: boolean; optimizePackageImports?: string[]; proxyClientMaxBodySize?: number | string; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
170
|
+
experimental?: { nextScriptWorkers?: boolean; /** App Router development only; compile literal client imports on first use. */ lazyDynamicImports?: boolean; optimizePackageImports?: string[]; proxyClientMaxBodySize?: number | string; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
153
171
|
trailingSlash?: boolean;
|
|
154
172
|
skipTrailingSlashRedirect?: boolean;
|
|
155
173
|
skipMiddlewareUrlNormalize?: boolean;
|
|
@@ -196,7 +214,45 @@ export interface StaticBuildOptions {
|
|
|
196
214
|
distDir?: string;
|
|
197
215
|
dev?: false;
|
|
198
216
|
}
|
|
199
|
-
export interface
|
|
217
|
+
export interface ReactCompilerReport {
|
|
218
|
+
version: 1;
|
|
219
|
+
compilerVersion: string | null;
|
|
220
|
+
enabled: boolean;
|
|
221
|
+
buildId: string | null;
|
|
222
|
+
mode: 'development' | 'production';
|
|
223
|
+
/** Incremental graphs can reuse inputs without observing their transforms. */
|
|
224
|
+
scope: 'observed-transforms';
|
|
225
|
+
options: boolean | ReactCompilerOptions | null;
|
|
226
|
+
truncated: boolean;
|
|
227
|
+
summary: {files: number; functions: number; optimized: number; skipped: number; errors: number; diagnostics: number; mixed: number; transforms: number; cacheReuses: number};
|
|
228
|
+
files: Array<{
|
|
229
|
+
file: string; transforms: number; cacheReuses: number; targets: string[]; notes: string[];
|
|
230
|
+
functions: Array<{
|
|
231
|
+
name: string | null;
|
|
232
|
+
start: {line: number; column: number} | null;
|
|
233
|
+
end: {line: number; column: number} | null;
|
|
234
|
+
status: 'optimized' | 'skipped' | 'error' | 'diagnostic' | 'mixed';
|
|
235
|
+
targets: string[];
|
|
236
|
+
events: Array<{
|
|
237
|
+
kind: 'CompileSuccess' | 'CompileSkip' | 'CompileError' | 'CompileDiagnostic' | 'PipelineError';
|
|
238
|
+
status: 'optimized' | 'skipped' | 'error' | 'diagnostic' | 'mixed';
|
|
239
|
+
reason: string | null; category: string | null; targets: string[]; policy?: boolean;
|
|
240
|
+
memoSlots?: number; memoBlocks?: number; memoValues?: number;
|
|
241
|
+
}>;
|
|
242
|
+
}>;
|
|
243
|
+
}>;
|
|
244
|
+
graphs: Array<{target: string; metadataAvailable: boolean; projectJavaScriptInputs: number; observedFiles: number; unobservedInputs: number}>;
|
|
245
|
+
}
|
|
246
|
+
export interface CompilationCacheReport {
|
|
247
|
+
version: 1;
|
|
248
|
+
scope: 'eligible-production-compilation-graphs';
|
|
249
|
+
hits: number;
|
|
250
|
+
misses: number;
|
|
251
|
+
bypasses: number;
|
|
252
|
+
reusedBytes: number;
|
|
253
|
+
kinds: Array<{kind: string; hits: number; misses: number; bypasses: number; writes: number; reusedBytes: number; reasons: Record<string, number>}>;
|
|
254
|
+
}
|
|
255
|
+
export interface BuildResult { buildId: string; outputDirectory: string; reactCompilerReport?: ReactCompilerReport; compilationCache?: CompilationCacheReport; }
|
|
200
256
|
export interface ProductionBuilder {
|
|
201
257
|
build(options?: {envMode?: 'production' | 'test'}): Promise<BuildResult>;
|
|
202
258
|
stats(): {contexts: number; created: number; reused: number};
|
|
@@ -212,3 +268,12 @@ export function buildStatic(projectRoot: string, options?: StaticBuildOptions):
|
|
|
212
268
|
export function readBuildOutput(buildDirectory: string): Promise<BuildOutput>;
|
|
213
269
|
export function validateBuildOutput(value: unknown): BuildOutput;
|
|
214
270
|
export type { DocumentContext, DocumentInitialProps, DocumentProps } from './document.cjs';
|
|
271
|
+
|
|
272
|
+
/** Server instrumentation hooks, compatible with Next.js instrumentation files. */
|
|
273
|
+
export namespace Instrumentation {
|
|
274
|
+
export type onRequestError = (error: unknown, request: Readonly<{path: string; method: string; headers: Record<string,string|string[]>}>, context: {
|
|
275
|
+
routerKind: 'App Router' | 'Pages Router'; routePath: string; routeType: 'render' | 'route' | 'action' | 'proxy';
|
|
276
|
+
renderSource: 'react-server-components' | 'react-server-components-payload' | 'server-rendering';
|
|
277
|
+
revalidateReason: 'on-demand' | 'stale' | undefined; renderType: 'dynamic' | 'dynamic-resume';
|
|
278
|
+
}) => void | Promise<void>;
|
|
279
|
+
}
|
package/compat/link.cjs
CHANGED
|
@@ -7,7 +7,7 @@ const { addBasePath, hasBasePath, normalizeTrailingSlash } = require('./paths.cj
|
|
|
7
7
|
const { localizedHref } = require('./locale.cjs');
|
|
8
8
|
|
|
9
9
|
const Link = React.forwardRef(function Link({ href, as, children, replace, scroll, shallow, prefetch,
|
|
10
|
-
locale, legacyBehavior, passHref, onClick, onNavigate, onMouseEnter, onTouchStart, ...props }, ref) {
|
|
10
|
+
locale, legacyBehavior, passHref, onClick, onNavigate, onMouseEnter, onTouchStart, onFocus, ...props }, ref) {
|
|
11
11
|
const app = React.useContext(AppRouterContext);
|
|
12
12
|
const pages = React.useContext(RouterContext);
|
|
13
13
|
const anchorRef = React.useRef(null);
|
|
@@ -19,14 +19,15 @@ const Link = React.forwardRef(function Link({ href, as, children, replace, scrol
|
|
|
19
19
|
: target.startsWith('//') ? target : addBasePath(target, basePath);
|
|
20
20
|
const publicTarget = normalizeTrailingSlash(mounted, policy);
|
|
21
21
|
const source = formatUrl(href);
|
|
22
|
-
const requestPrefetch = React.useCallback(() => {
|
|
23
|
-
if (
|
|
24
|
-
if (
|
|
25
|
-
|
|
22
|
+
const requestPrefetch = React.useCallback((priority = 'intent') => {
|
|
23
|
+
if (prefetch === false || process.env.NODE_ENV !== 'production') return;
|
|
24
|
+
if (app && (app.router.partialPrefetch || prefetch === true)) app.router.prefetch(target, { priority, full: prefetch === true });
|
|
25
|
+
if (!app && pages) void pages.prefetch(source, as ? target : undefined, { locale, priority }).catch(() => {});
|
|
26
|
+
}, [app, pages, source, target, as, locale, prefetch]);
|
|
26
27
|
React.useEffect(() => {
|
|
27
|
-
if ((app ? !app.router.partialPrefetch : !pages) || prefetch === false || process.env.NODE_ENV !== 'production' || typeof IntersectionObserver === 'undefined' || !anchorRef.current) return;
|
|
28
|
+
if ((app ? !app.router.partialPrefetch && prefetch !== true : !pages) || prefetch === false || process.env.NODE_ENV !== 'production' || typeof IntersectionObserver === 'undefined' || !anchorRef.current) return;
|
|
28
29
|
const observer = new IntersectionObserver(entries => {
|
|
29
|
-
if (entries.some(entry => entry.isIntersecting)) { observer.disconnect(); requestPrefetch(); }
|
|
30
|
+
if (entries.some(entry => entry.isIntersecting)) { observer.disconnect(); requestPrefetch('viewport'); }
|
|
30
31
|
}, { rootMargin: '200px' });
|
|
31
32
|
observer.observe(anchorRef.current);
|
|
32
33
|
return () => observer.disconnect();
|
|
@@ -53,16 +54,18 @@ const Link = React.forwardRef(function Link({ href, as, children, replace, scrol
|
|
|
53
54
|
void pages[replace ? 'replace' : 'push'](href, as, { scroll, shallow, locale }).catch(error => console.error(error));
|
|
54
55
|
} else if (replace) { event.preventDefault(); window.location.replace(publicTarget); }
|
|
55
56
|
}
|
|
56
|
-
function hover(event) { onMouseEnter?.(event); requestPrefetch(); }
|
|
57
|
-
function touch(event) { onTouchStart?.(event); requestPrefetch(); }
|
|
57
|
+
function hover(event) { onMouseEnter?.(event); if (!event.defaultPrevented) requestPrefetch(); }
|
|
58
|
+
function touch(event) { onTouchStart?.(event); if (!event.defaultPrevented) requestPrefetch(); }
|
|
59
|
+
function focus(event) { onFocus?.(event); if (!event.defaultPrevented) requestPrefetch(); }
|
|
58
60
|
if (legacyBehavior) {
|
|
59
61
|
const child = React.Children.only(children);
|
|
60
62
|
return React.cloneElement(child, { ...props, href: publicTarget, ref: setRef,
|
|
61
63
|
onMouseEnter(event) { child.props.onMouseEnter?.(event); hover(event); },
|
|
62
64
|
onTouchStart(event) { child.props.onTouchStart?.(event); touch(event); },
|
|
65
|
+
onFocus(event) { child.props.onFocus?.(event); focus(event); },
|
|
63
66
|
onClick(event) { child.props.onClick?.(event); if (!event.defaultPrevented) click(event); } });
|
|
64
67
|
}
|
|
65
|
-
return React.createElement('a', { ...props, href: publicTarget, ref: setRef, onClick: click, onMouseEnter: hover, onTouchStart: touch }, children);
|
|
68
|
+
return React.createElement('a', { ...props, href: publicTarget, ref: setRef, onClick: click, onMouseEnter: hover, onTouchStart: touch, onFocus: focus }, children);
|
|
66
69
|
});
|
|
67
70
|
module.exports = Link;
|
|
68
71
|
module.exports.default = Link;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { createHmac } = require('node:crypto');
|
|
3
|
+
function navigationAuthorization(context, secret, cookieNames) {
|
|
4
|
+
if (!secret || context?.staticState) return undefined;
|
|
5
|
+
const names = cookieNames === undefined ? null : new Set(cookieNames);
|
|
6
|
+
const cookies = context.cookies.getAll().filter(cookie => !names || names.has(cookie.name)).map(({ name, value }) => [name, value]).sort((a, b) => a[0].localeCompare(b[0]));
|
|
7
|
+
return createHmac('sha256', secret).update(JSON.stringify([cookies, context.headers.get('authorization') || ''])).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
module.exports = { navigationAuthorization };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { optionalRequestContext, inCacheScope } = require('./data-cache.cjs');
|
|
3
|
+
|
|
4
|
+
function postponeStage(name) {
|
|
5
|
+
throw Object.assign(new Error(`Deferred until ${name}.`), { code: 'PRNEXT_PPR_DYNAMIC', digest: 'PRNEXT_PPR_DYNAMIC' });
|
|
6
|
+
}
|
|
7
|
+
function stage(name) {
|
|
8
|
+
const context = optionalRequestContext();
|
|
9
|
+
if (!context?.cacheComponents) throw new Error(`${name}() requires cacheComponents: true.`);
|
|
10
|
+
if (inCacheScope()) throw new Error(`${name}() cannot run inside a cached function. Call it before invoking the cached function.`);
|
|
11
|
+
// Static prerenders deliberately continue through both gates. Cacheable work
|
|
12
|
+
// remains prerenderable and is not made request-dependent by either API.
|
|
13
|
+
if (context.staticState) return;
|
|
14
|
+
if (context.navigationStage === 'shell' && (name === 'navigation' || context.partialPrefetching)) postponeStage(name);
|
|
15
|
+
if (context.navigationStage === 'prefetch' && name === 'navigation') postponeStage(name);
|
|
16
|
+
}
|
|
17
|
+
async function navigation() { stage('navigation'); }
|
|
18
|
+
async function prefetch() { stage('prefetch'); }
|
|
19
|
+
module.exports = { navigation, prefetch, postponeStage };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const waiting=new Set(),listeners=new Set();
|
|
3
|
+
const changed=()=>{for(const listener of listeners)listener();};
|
|
4
|
+
const snapshot=()=>waiting.size>0||globalThis.navigator?.onLine===false;
|
|
5
|
+
function subscribe(listener){
|
|
6
|
+
if(!listeners.size){globalThis.window?.addEventListener('online',changed);globalThis.window?.addEventListener('offline',changed);}
|
|
7
|
+
listeners.add(listener);
|
|
8
|
+
return()=>{listeners.delete(listener);if(!listeners.size){globalThis.window?.removeEventListener('online',changed);globalThis.window?.removeEventListener('offline',changed);}};
|
|
9
|
+
}
|
|
10
|
+
function beginWait(){const id={};waiting.add(id);changed();let ended=false;return()=>{if(ended)return;ended=true;waiting.delete(id);changed();};}
|
|
11
|
+
module.exports={snapshot,subscribe,beginWait};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {createHash,randomBytes}=require('node:crypto');
|
|
3
|
+
const MAX_VALUE=2*1024*1024,MAX_META=65536;
|
|
4
|
+
const hash=value=>createHash('sha256').update(value).digest('hex');
|
|
5
|
+
const ACQUIRE=`
|
|
6
|
+
if not redis.call('HGET',KEYS[2],'_incarnation') then redis.call('HSET',KEYS[2],'_incarnation',ARGV[3],'_epoch',ARGV[3]) end
|
|
7
|
+
redis.call('PEXPIRE',KEYS[2],ARGV[4])
|
|
8
|
+
local epoch=redis.call('HGET',KEYS[2],'_epoch');local incarnation=redis.call('HGET',KEYS[2],'_incarnation')
|
|
9
|
+
if redis.call('SET',KEYS[1],ARGV[1],'NX','PX',ARGV[2]) then return {1,epoch,incarnation} end
|
|
10
|
+
return {0}`;
|
|
11
|
+
const RELEASE=`if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) end return 0`;
|
|
12
|
+
const READ=`
|
|
13
|
+
local fields=redis.call('HMGET',KEYS[1],'meta','value')
|
|
14
|
+
if not fields[1] or not fields[2] or string.len(fields[1])>65536 or string.len(fields[2])>tonumber(ARGV[2]) then return nil end
|
|
15
|
+
local meta=cjson.decode(fields[1]); local extra=cjson.decode(ARGV[1]); local stale=0
|
|
16
|
+
if redis.call('HGET',KEYS[2],'_incarnation')~=meta.incarnation then return nil end
|
|
17
|
+
local t=redis.call('TIME'); local now=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
|
|
18
|
+
local seen={}
|
|
19
|
+
for _,id in ipairs(meta.tagIds) do seen[id]=true end
|
|
20
|
+
for _,id in ipairs(extra) do seen[id]=true end
|
|
21
|
+
for id,_ in pairs(seen) do
|
|
22
|
+
local value=redis.call('HGET',KEYS[2],id)
|
|
23
|
+
if value then
|
|
24
|
+
local tag=cjson.decode(value)
|
|
25
|
+
if tag.generation~=(meta.versions[id] or '0') then
|
|
26
|
+
if tag.deadline>=0 and tag.deadline<=now then return nil end
|
|
27
|
+
stale=1
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
return {fields[1],fields[2],math.max(0,now-meta.storedAt),stale}`;
|
|
32
|
+
const WRITE=`
|
|
33
|
+
if redis.call('GET',KEYS[3])~=ARGV[1] or redis.call('HGET',KEYS[2],'_epoch')~=ARGV[2] then return 0 end
|
|
34
|
+
local meta=cjson.decode(ARGV[3]); meta.versions={};meta.incarnation=ARGV[5];meta.writer=ARGV[1]
|
|
35
|
+
local t=redis.call('TIME'); local now=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
|
|
36
|
+
meta.storedAt=now-tonumber(ARGV[6])
|
|
37
|
+
for _,id in ipairs(meta.tagIds) do
|
|
38
|
+
local value=redis.call('HGET',KEYS[2],id)
|
|
39
|
+
meta.versions[id]=value and cjson.decode(value).generation or '0'
|
|
40
|
+
end
|
|
41
|
+
local encoded=cjson.encode(meta)
|
|
42
|
+
if string.len(encoded)>65536 then return 0 end
|
|
43
|
+
redis.call('HSET',KEYS[1],'meta',encoded,'value',ARGV[4])
|
|
44
|
+
redis.call('PEXPIRE',KEYS[1],ARGV[7]);redis.call('PEXPIRE',KEYS[2],ARGV[8]);return 1`;
|
|
45
|
+
const INVALIDATE=`
|
|
46
|
+
local generation=ARGV[3];local ids=cjson.decode(ARGV[4]);local added=0
|
|
47
|
+
for _,id in ipairs(ids) do if redis.call('HEXISTS',KEYS[1],id)==0 then added=added+1 end end
|
|
48
|
+
if redis.call('HLEN',KEYS[1])+added>4098 then redis.call('DEL',KEYS[1]) end
|
|
49
|
+
if not redis.call('HGET',KEYS[1],'_incarnation') then redis.call('HSET',KEYS[1],'_incarnation',generation) end
|
|
50
|
+
redis.call('HSET',KEYS[1],'_epoch',generation)
|
|
51
|
+
local t=redis.call('TIME');local now=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
|
|
52
|
+
local expiry=tonumber(ARGV[1]);local deadline=expiry<0 and -1 or now+expiry
|
|
53
|
+
for _,id in ipairs(ids) do redis.call('HSET',KEYS[1],id,cjson.encode({generation=generation,deadline=deadline})) end
|
|
54
|
+
redis.call('PEXPIRE',KEYS[1],ARGV[2])
|
|
55
|
+
return 1`;
|
|
56
|
+
const REMOVE=`local meta=redis.call('HGET',KEYS[1],'meta');if meta and cjson.decode(meta).writer==ARGV[1] then return redis.call('DEL',KEYS[1]) end return 0`;
|
|
57
|
+
|
|
58
|
+
function tags(values=[]){
|
|
59
|
+
if(!Array.isArray(values)||values.length>256||values.some(value=>typeof value!=='string'||!value||Buffer.byteLength(value)>8192))throw new TypeError('Redis cache tags must be bounded nonempty strings.');
|
|
60
|
+
return [...new Set(values)];
|
|
61
|
+
}
|
|
62
|
+
function positive(value,fallback,min,max,name){value??=fallback;if(!Number.isSafeInteger(value)||value<min||value>max)throw new TypeError(`Redis cache ${name} must be between ${min} and ${max}.`);return value;}
|
|
63
|
+
function pause(ms,signal){return new Promise((resolve,reject)=>{const done=error=>{clearTimeout(timer);signal?.removeEventListener('abort',abort);error?reject(error):resolve();};const abort=()=>done(signal.reason);const timer=setTimeout(done,ms);signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();});}
|
|
64
|
+
function boundedRead(reader,ms){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('Redis cache value read timed out.')),ms);reader.read().then(value=>{clearTimeout(timer);resolve(value);},error=>{clearTimeout(timer);reject(error);});});}
|
|
65
|
+
|
|
66
|
+
/** First-party Cache Components handler; Redis is loaded only when configured. */
|
|
67
|
+
function createRedisCache(options={},maxValueBytes=MAX_VALUE){
|
|
68
|
+
if(!options||typeof options!=='object'||Array.isArray(options))throw new TypeError('Redis cache options must be an object.');
|
|
69
|
+
const known=new Set(['namespace','urlEnv','client','leaseMs','waitMs','commandTimeoutMs','maxAgeSeconds']);
|
|
70
|
+
for(const key of Object.keys(options))if(!known.has(key))throw new TypeError(`Unknown Redis cache option: ${key}`);
|
|
71
|
+
if(options.client!==undefined&&typeof options.client?.withTypeMapping!=='function')throw new TypeError('Redis cache client must be a node-redis client.');
|
|
72
|
+
if(typeof options.namespace!=='string'||!options.namespace||Buffer.byteLength(options.namespace)>256)throw new TypeError('Redis cache requires an application-specific namespace.');
|
|
73
|
+
const urlEnv=options.urlEnv??'REDIS_URL';if(typeof urlEnv!=='string'||!/^[_A-Za-z][_A-Za-z0-9]*$/.test(urlEnv))throw new TypeError('Redis cache urlEnv must name an environment variable.');
|
|
74
|
+
const leaseMs=positive(options.leaseMs,30000,1000,120000,'leaseMs'),waitMs=positive(options.waitMs,2500,100,3500,'waitMs');
|
|
75
|
+
const commandTimeoutMs=positive(options.commandTimeoutMs,1000,100,2000,'commandTimeoutMs'),maxAgeSeconds=positive(options.maxAgeSeconds,604800,1,2592000,'maxAgeSeconds');
|
|
76
|
+
if(waitMs+2*commandTimeoutMs>4500)throw new TypeError('Redis cache waitMs plus twice commandTimeoutMs must not exceed 4500 ms.');
|
|
77
|
+
const prefix='prnext:v1:{'+hash(options.namespace)+'}:',epochKey=prefix+'tags',retention=String(maxAgeSeconds*1000+leaseMs+60000);
|
|
78
|
+
let owned,connecting,closed=false;
|
|
79
|
+
async function client(){
|
|
80
|
+
if(closed)throw new Error('Redis cache is closed.');
|
|
81
|
+
if(options.client){
|
|
82
|
+
if(options.client.isReady===false)throw new Error('Redis cache client is not connected.');
|
|
83
|
+
return options.client;
|
|
84
|
+
}
|
|
85
|
+
if(owned?.isReady)return owned;
|
|
86
|
+
if(!connecting){
|
|
87
|
+
connecting=(async()=>{
|
|
88
|
+
const url=process.env[urlEnv];
|
|
89
|
+
if(!url||!/^rediss?:\/\//.test(url))throw new Error(`Set ${urlEnv} to a Redis connection URL.`);
|
|
90
|
+
// Use node-redis's base client; Redis Stack command packages are not
|
|
91
|
+
// needed by this provider's bounded EVAL/HASH protocol.
|
|
92
|
+
const {createClient}=require('@redis/client');
|
|
93
|
+
owned?.destroy();owned=createClient({url,disableOfflineQueue:true,commandsQueueMaxLength:128,socket:{connectTimeout:commandTimeoutMs,reconnectStrategy:false}});
|
|
94
|
+
owned.on('error',()=>{});await owned.connect();return owned;
|
|
95
|
+
})().finally(()=>{connecting=undefined;});
|
|
96
|
+
}
|
|
97
|
+
return connecting;
|
|
98
|
+
}
|
|
99
|
+
async function command(args,signal){
|
|
100
|
+
const connection=await client();
|
|
101
|
+
// '$' (36) is RESP's binary-safe bulk-string type, as exposed by node-redis.
|
|
102
|
+
const binary=connection.withTypeMapping({36:Buffer});
|
|
103
|
+
const deadline=AbortSignal.timeout(commandTimeoutMs);
|
|
104
|
+
return binary.sendCommand(args,{abortSignal:signal?AbortSignal.any([signal,deadline]):deadline});
|
|
105
|
+
}
|
|
106
|
+
const keys=key=>{if(typeof key!=='string'||!key||Buffer.byteLength(key)>8192)throw new TypeError('Invalid Redis cache key.');const id=hash(key);return {entry:prefix+'entry:'+id,lock:prefix+'lease:'+id};};
|
|
107
|
+
async function acquire(key,{signal}={}){
|
|
108
|
+
const {lock}=keys(key),token=randomBytes(24).toString('hex'),until=performance.now()+waitMs;
|
|
109
|
+
for(;;){
|
|
110
|
+
signal?.throwIfAborted();
|
|
111
|
+
const started=performance.now();
|
|
112
|
+
const result=await command(['EVAL',ACQUIRE,'2',lock,epochKey,token,String(leaseMs),randomBytes(24).toString('hex'),retention],signal);
|
|
113
|
+
if(Number(result[0])===1){let released=false;return {token,started,epoch:String(result[1]),incarnation:String(result[2]),async release(){if(released)return;released=true;await command(['EVAL',RELEASE,'1',lock,token]);}};}
|
|
114
|
+
if(performance.now()>=until)throw Object.assign(new Error('Redis cache fill wait exceeded its budget.'),{code:'PRNEXT_REDIS_BUSY'});
|
|
115
|
+
await pause(Math.min(50,Math.max(1,until-performance.now())),signal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function get(key,softTags=[]){
|
|
119
|
+
const result=await command(['EVAL',READ,'2',keys(key).entry,epochKey,JSON.stringify(tags(softTags).map(hash)),String(maxValueBytes)]);
|
|
120
|
+
if(!result)return undefined;
|
|
121
|
+
const meta=JSON.parse(result[0].toString('utf8')),bytes=result[1];
|
|
122
|
+
if(meta.version!==1||!Buffer.isBuffer(bytes)||bytes.length>maxValueBytes)throw new Error('Invalid Redis cache entry.');
|
|
123
|
+
const age=Number(result[2]);if(!Number.isFinite(age)||age<0)throw new Error('Invalid Redis cache age.');
|
|
124
|
+
let offset=0;
|
|
125
|
+
return {tags:meta.tags,stale:meta.stale,revalidate:Number(result[3])?0:meta.revalidate,expire:meta.expire,timestamp:Math.max(0,Date.now()-age),
|
|
126
|
+
value:new ReadableStream({pull(controller){if(offset===bytes.length){controller.close();return;}const end=Math.min(bytes.length,offset+65536);controller.enqueue(new Uint8Array(bytes.subarray(offset,end)));offset=end;}},{highWaterMark:0})};
|
|
127
|
+
}
|
|
128
|
+
async function set(key,pending,{lease,softTags=[]}={}){
|
|
129
|
+
if(!lease||!Number.isFinite(lease.started)){
|
|
130
|
+
// Observe and release a caller's value even when its lease is missing.
|
|
131
|
+
// An unfenced producer must never publish after an invalidation.
|
|
132
|
+
void Promise.resolve(pending).then(entry=>entry.value.cancel()).catch(()=>{});
|
|
133
|
+
throw new Error('Redis cache writes require the lease returned by acquire().');
|
|
134
|
+
}
|
|
135
|
+
const entry=await pending,entryTags=tags(entry.tags);
|
|
136
|
+
for(const field of ['timestamp','stale','revalidate','expire'])if(!Number.isFinite(entry[field])||entry[field]<0)throw new TypeError('Invalid Redis cache '+field);
|
|
137
|
+
const reader=entry.value.getReader(),chunks=[];let size=0,complete=false;
|
|
138
|
+
const until=performance.now()+commandTimeoutMs;
|
|
139
|
+
let reads=0;
|
|
140
|
+
try{for(;;){if(performance.now()>=until||++reads>10000)throw new Error('Redis cache value read timed out.');const part=await boundedRead(reader,Math.max(1,until-performance.now()));if(part.done){complete=true;break;}if(!(part.value instanceof Uint8Array)||(size+=part.value.byteLength)>maxValueBytes)throw new Error(`Redis cache value exceeds ${maxValueBytes/1024/1024} MiB.`);chunks.push(Buffer.from(part.value));}}
|
|
141
|
+
finally{if(!complete)void reader.cancel().catch(()=>{});reader.releaseLock();}
|
|
142
|
+
const meta={version:1,tags:entryTags,tagIds:tags([...entryTags,...softTags]).map(hash),stale:entry.stale,revalidate:entry.revalidate,expire:entry.expire};
|
|
143
|
+
const metadata=JSON.stringify(meta);if(Buffer.byteLength(metadata)>MAX_META)throw new Error('Redis cache metadata exceeds 64 KiB.');
|
|
144
|
+
const ttl=Math.ceil(Math.min(maxAgeSeconds,entry.expire)*1000);if(ttl<=0)return;
|
|
145
|
+
const {entry:entryKey,lock}=keys(key);
|
|
146
|
+
return Number(await command(['EVAL',WRITE,'3',entryKey,epochKey,lock,lease.token,lease.epoch,metadata,Buffer.concat(chunks,size),lease.incarnation,String(Math.max(0,performance.now()-lease.started)),String(ttl),retention]))===1;
|
|
147
|
+
}
|
|
148
|
+
async function updateTags(values,durations){
|
|
149
|
+
const list=tags(values);if(!list.length)return;
|
|
150
|
+
const expire=durations?.expire;if(expire!==undefined&&(!Number.isFinite(expire)||expire<0))throw new TypeError('Invalid Redis tag expiration.');
|
|
151
|
+
await command(['EVAL',INVALIDATE,'1',epochKey,String(expire===undefined?-1:Math.ceil(expire*1000)),retention,randomBytes(24).toString('hex'),JSON.stringify(list.map(hash))]);
|
|
152
|
+
}
|
|
153
|
+
return {get,set,acquire,updateTags,
|
|
154
|
+
async release(key,lease){if(lease?.token)await command(['EVAL',RELEASE,'1',keys(key).lock,lease.token]);},
|
|
155
|
+
async remove(key,lease){if(lease?.token)await command(['EVAL',REMOVE,'1',keys(key).entry,lease.token]);},
|
|
156
|
+
async refreshTags(){},async getExpiration(){return Infinity;},async close(){closed=true;await connecting?.catch(()=>{});if(owned?.isOpen)owned.destroy();}};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Legacy incremental handler for HTML/ISR, fetch and unstable_cache. */
|
|
160
|
+
function createRedisIncrementalCache(options){
|
|
161
|
+
const {serialize,deserialize}=require('node:v8');
|
|
162
|
+
const backend=createRedisCache({leaseMs:60000,...options},16*1024*1024);
|
|
163
|
+
const keyFor=key=>'incremental:v8-'+process.versions.v8.split('.')[0]+':'+key;
|
|
164
|
+
function leaseFor(lease){
|
|
165
|
+
if(!lease||!['token','epoch','incarnation'].every(field=>typeof lease[field]==='string'&&/^[a-f0-9]{48}$/.test(lease[field]))||!Number.isFinite(lease.startedAt))throw new Error('Incremental Redis writes require a valid generation lease.');
|
|
166
|
+
return {...lease,started:performance.now()-Math.max(0,Date.now()-lease.startedAt)};
|
|
167
|
+
}
|
|
168
|
+
return class RedisIncrementalCache {
|
|
169
|
+
get prnextLeaseVersion(){return 1;}
|
|
170
|
+
async acquire(key,{signal}={}){
|
|
171
|
+
const lease=await backend.acquire(keyFor(key),{signal});
|
|
172
|
+
return {token:lease.token,epoch:lease.epoch,incarnation:lease.incarnation,startedAt:Date.now()};
|
|
173
|
+
}
|
|
174
|
+
async release(key,lease){await backend.release(keyFor(key),lease);}
|
|
175
|
+
async get(key,context={}){
|
|
176
|
+
const entry=await backend.get(keyFor(key),tags([...new Set([...(context.tags||[]),...(context.softTags||[])])]));
|
|
177
|
+
if(!entry)return null;
|
|
178
|
+
const bytes=Buffer.from(await new Response(entry.value).arrayBuffer());
|
|
179
|
+
const payload=deserialize(bytes);
|
|
180
|
+
if(payload?.version!==1||!Number.isFinite(payload.lastModified)||!payload.value||typeof payload.value.kind!=='string')throw new Error('Invalid incremental Redis payload.');
|
|
181
|
+
return {value:payload.value,lastModified:payload.lastModified,isStale:entry.revalidate===0};
|
|
182
|
+
}
|
|
183
|
+
async set(key,value,context={}){
|
|
184
|
+
const lease=leaseFor(context.prnextLease);
|
|
185
|
+
if(value===null){await backend.remove(keyFor(key),lease);return true;}
|
|
186
|
+
const raw=context.cacheControl?.revalidate??value.revalidate??value._prnext?.revalidate??false;
|
|
187
|
+
if(raw!==false&&(!Number.isFinite(raw)||raw<0))throw new TypeError('Invalid incremental Redis revalidate value.');
|
|
188
|
+
if(raw===0)return true;
|
|
189
|
+
const revalidate=raw===false?31536000:raw;
|
|
190
|
+
const entryTags=tags([...new Set([...(context.tags||[]),...(context.softTags||[])])]);
|
|
191
|
+
const bytes=serialize({version:1,lastModified:Date.now(),value});
|
|
192
|
+
if(bytes.length>16*1024*1024)throw new Error('Incremental Redis entry exceeds 16 MiB.');
|
|
193
|
+
return backend.set(keyFor(key),Promise.resolve({timestamp:Date.now(),stale:0,revalidate,expire:Math.max(revalidate,options?.maxAgeSeconds??604800),tags:entryTags,
|
|
194
|
+
value:new ReadableStream({start(controller){controller.enqueue(bytes);controller.close();}})}),{lease});
|
|
195
|
+
}
|
|
196
|
+
async revalidateTag(values,durations){await backend.updateTags(Array.isArray(values)?values:[values],durations);}
|
|
197
|
+
resetRequestCache(){}
|
|
198
|
+
async close(){await backend.close();}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
module.exports={createRedisCache,createRedisIncrementalCache};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export interface RedisCacheOptions {
|
|
3
|
+
/** Unique application namespace, shared by its replicas. */
|
|
4
|
+
namespace: string;
|
|
5
|
+
/** Runtime environment variable; the URL is never embedded in a build. Default REDIS_URL. */
|
|
6
|
+
urlEnv?: string;
|
|
7
|
+
/** An already-connected node-redis client. Its lifecycle remains yours. */
|
|
8
|
+
client?: {readonly isReady?: boolean; withTypeMapping(mapping: {36: BufferConstructor}): {sendCommand(args: (string | Buffer)[], options?: {abortSignal?: AbortSignal}): Promise<unknown>}};
|
|
9
|
+
leaseMs?: number;
|
|
10
|
+
waitMs?: number;
|
|
11
|
+
commandTimeoutMs?: number;
|
|
12
|
+
maxAgeSeconds?: number;
|
|
13
|
+
}
|
|
14
|
+
interface CacheEntry {value: ReadableStream<Uint8Array>; tags: string[]; timestamp: number; stale: number; revalidate: number; expire: number}
|
|
15
|
+
interface Lease {token: string; started: number; epoch: string; incarnation: string; release(): Promise<void>}
|
|
16
|
+
export declare function createRedisCache(options: RedisCacheOptions): {
|
|
17
|
+
get(key: string, softTags?: string[]): Promise<CacheEntry | undefined>;
|
|
18
|
+
set(key: string, entry: Promise<CacheEntry>, options?: {lease?: Lease; softTags?: string[]}): Promise<void>;
|
|
19
|
+
acquire(key: string, options?: {signal?: AbortSignal}): Promise<Lease>;
|
|
20
|
+
refreshTags(): Promise<void>;
|
|
21
|
+
getExpiration(): Promise<number>;
|
|
22
|
+
updateTags(tags: string[], durations?: {expire: number}): Promise<void>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface IncrementalRedisLease {token: string; epoch: string; incarnation: string; startedAt: number}
|
|
27
|
+
/** Configure its returned constructor as cacheHandler; Cache Components use createRedisCache separately. */
|
|
28
|
+
export declare function createRedisIncrementalCache(options: RedisCacheOptions): new (context?: unknown) => {
|
|
29
|
+
readonly prnextLeaseVersion: 1;
|
|
30
|
+
acquire(key: string, options?: {signal?: AbortSignal}): Promise<IncrementalRedisLease>;
|
|
31
|
+
release(key: string, lease: IncrementalRedisLease): Promise<void>;
|
|
32
|
+
get(key: string, context?: {tags?: string[]; softTags?: string[]}): Promise<{value: unknown; lastModified: number; isStale?: boolean} | null>;
|
|
33
|
+
set(key: string, value: unknown, context?: {prnextLease?: IncrementalRedisLease; tags?: string[]; softTags?: string[]; cacheControl?: {revalidate: number | false}}): Promise<boolean>;
|
|
34
|
+
revalidateTag(tags: string | string[], durations?: {expire?: number}): Promise<void>;
|
|
35
|
+
resetRequestCache(): void;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
const {
|
|
2
|
+
const { RequestCookieStore, ResponseCookieStore } = require('./cookies.cjs');
|
|
3
3
|
const { hasBasePath, addBasePath, removeBasePath } = require('./paths.cjs');
|
|
4
4
|
const { localePath, withLocale } = require('./locale.cjs');
|
|
5
5
|
|
|
@@ -14,21 +14,37 @@ function middlewareHeaders(init, headers) {
|
|
|
14
14
|
headers.set('x-middleware-override-headers', names.join(','));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function nextDataPath(pathname) {
|
|
18
|
+
const match = /^\/_next\/data\/([^/]+)\/(.*)\.json$/.exec(pathname);
|
|
19
|
+
if (!match) return { pathname };
|
|
20
|
+
const parts = match[2].split('/');
|
|
21
|
+
return { buildId: match[1], pathname: parts[0] === 'index' ? '/' : '/' + parts.join('/') };
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
class NextURL extends URL {
|
|
18
25
|
constructor(input, baseOrOptions, options) {
|
|
19
26
|
const base = typeof baseOrOptions === 'string' || baseOrOptions instanceof URL ? baseOrOptions : undefined;
|
|
20
27
|
const config = options || (base ? {} : baseOrOptions) || {};
|
|
21
28
|
super(input, base || config.base);
|
|
22
29
|
this._nextConfig = config.nextConfig || {};
|
|
30
|
+
this._forceLocale = config.forceLocale === true;
|
|
31
|
+
this._parseData = !(this._nextConfig.skipProxyUrlNormalize ?? this._nextConfig.skipMiddlewareUrlNormalize);
|
|
23
32
|
this._trailingSlash = super.pathname === '/' ? !!this._nextConfig.trailingSlash : super.pathname.endsWith('/');
|
|
24
33
|
this._basePath = hasBasePath(super.pathname, this._nextConfig.basePath) ? this._nextConfig.basePath || '' : '';
|
|
25
|
-
|
|
34
|
+
const logical = this._parseData ? nextDataPath(removeBasePath(super.pathname, this._basePath)) : { pathname: removeBasePath(super.pathname, this._basePath) };
|
|
35
|
+
this._buildId = logical.buildId;
|
|
36
|
+
this._locale = this._nextConfig.i18n ? localePath(logical.pathname, {...this._nextConfig.i18n,defaultLocale:this.defaultLocale}).locale : '';
|
|
37
|
+
}
|
|
38
|
+
get pathname() {
|
|
39
|
+
const raw = removeBasePath(super.pathname, this._basePath);
|
|
40
|
+
return localePath(this._parseData ? nextDataPath(raw).pathname : raw, this._nextConfig.i18n).pathname;
|
|
26
41
|
}
|
|
27
|
-
get pathname() { return localePath(removeBasePath(super.pathname, this._basePath), this._nextConfig.i18n).pathname; }
|
|
28
42
|
set pathname(value) { super.pathname = addBasePath(withLocale(String(value).startsWith('/') ? value : `/${value}`, this._locale, this.defaultLocale), this._basePath); }
|
|
29
43
|
get defaultLocale() { return this.domainLocale?.defaultLocale || this._nextConfig.i18n?.defaultLocale; }
|
|
30
44
|
get domainLocale() { return this._nextConfig.i18n?.domains?.find(value=>value.domain.toLowerCase()===this.host.toLowerCase()); }
|
|
31
45
|
get locale() { return this._locale; }
|
|
46
|
+
get buildId() { return this._buildId; }
|
|
47
|
+
set buildId(value) { this._buildId = value; }
|
|
32
48
|
set locale(value) {
|
|
33
49
|
if (!this._nextConfig.i18n?.locales.includes(value)) throw new TypeError(`Unknown locale: ${value}`);
|
|
34
50
|
const pathname=this.pathname;this._locale=value;this.pathname=pathname;
|
|
@@ -43,8 +59,13 @@ class NextURL extends URL {
|
|
|
43
59
|
set trailingSlash(value) { this._trailingSlash = value; }
|
|
44
60
|
get href() {
|
|
45
61
|
const url = new URL(super.href);
|
|
46
|
-
|
|
47
|
-
if (this.
|
|
62
|
+
let pathname = withLocale(this.pathname, this._locale, this._buildId || this._forceLocale ? undefined : this.defaultLocale);
|
|
63
|
+
if (this._buildId) {
|
|
64
|
+
pathname = pathname.replace(/\/$/, '');
|
|
65
|
+
pathname = '/_next/data/' + this._buildId + pathname + (this.pathname === '/' ? '/index.json' : '.json');
|
|
66
|
+
}
|
|
67
|
+
url.pathname = addBasePath(pathname, this._basePath);
|
|
68
|
+
if (this._trailingSlash && !this._buildId) { if (!url.pathname.endsWith('/')) url.pathname += '/'; }
|
|
48
69
|
else if (url.pathname !== '/') url.pathname = url.pathname.replace(/\/$/, '');
|
|
49
70
|
return url.href;
|
|
50
71
|
}
|
|
@@ -54,19 +75,18 @@ class NextURL extends URL {
|
|
|
54
75
|
super.href = value;
|
|
55
76
|
this._trailingSlash = super.pathname === '/' ? !!this._nextConfig.trailingSlash : super.pathname.endsWith('/');
|
|
56
77
|
this._basePath = hasBasePath(super.pathname, this._nextConfig.basePath) ? this._nextConfig.basePath || '' : '';
|
|
57
|
-
|
|
78
|
+
const logical = this._parseData ? nextDataPath(removeBasePath(super.pathname, this._basePath)) : { pathname: removeBasePath(super.pathname, this._basePath) };
|
|
79
|
+
this._buildId = logical.buildId;
|
|
80
|
+
this._locale = this._nextConfig.i18n ? localePath(logical.pathname, {...this._nextConfig.i18n,defaultLocale:this.defaultLocale}).locale : '';
|
|
58
81
|
}
|
|
59
|
-
clone() { return new NextURL(this.href, { nextConfig: this._nextConfig }); }
|
|
82
|
+
clone() { return new NextURL(this.href, { nextConfig: this._nextConfig, forceLocale: this._forceLocale }); }
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
class NextRequest extends Request {
|
|
63
86
|
constructor(input, init) {
|
|
64
87
|
super(input, init);
|
|
65
88
|
this.nextUrl = new NextURL(this.url, { nextConfig: init?.nextConfig || input?.nextUrl?._nextConfig });
|
|
66
|
-
this.cookies = new
|
|
67
|
-
mutable: true,
|
|
68
|
-
onChange: () => this.headers.set('cookie', this.cookies.toString()),
|
|
69
|
-
});
|
|
89
|
+
this.cookies = new RequestCookieStore(this.headers);
|
|
70
90
|
}
|
|
71
91
|
}
|
|
72
92
|
|
package/compat/server.d.cts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { CookieStore } from './headers.cjs';
|
|
2
2
|
export interface NextURLConfig { i18n?: import('./index.js').NextConfig['i18n']; basePath?: string; trailingSlash?: boolean; skipMiddlewareUrlNormalize?: boolean; skipProxyUrlNormalize?: boolean }
|
|
3
|
-
export class NextURL extends URL { constructor(input: string | URL, options?: { base?: string | URL; nextConfig?: NextURLConfig }); constructor(input: string | URL, base: string | URL, options?: { nextConfig?: NextURLConfig }); clone(): NextURL; locale: string; readonly defaultLocale?: string; readonly domainLocale?: NonNullable<NonNullable<import('./index.js').NextConfig['i18n']>['domains']>[number]; basePath: string; trailingSlash: boolean }
|
|
4
|
-
export
|
|
3
|
+
export class NextURL extends URL { constructor(input: string | URL, options?: { base?: string | URL; nextConfig?: NextURLConfig; forceLocale?: boolean }); constructor(input: string | URL, base: string | URL, options?: { nextConfig?: NextURLConfig; forceLocale?: boolean }); clone(): NextURL; buildId: string | undefined; locale: string; readonly defaultLocale?: string; readonly domainLocale?: NonNullable<NonNullable<import('./index.js').NextConfig['i18n']>['domains']>[number]; basePath: string; trailingSlash: boolean }
|
|
4
|
+
export interface RequestCookie { name: string; value: string }
|
|
5
|
+
export interface RequestCookieStore extends Iterable<[string, RequestCookie]> {
|
|
6
|
+
readonly size: number;
|
|
7
|
+
get(name: string | {name: string}): RequestCookie | undefined;
|
|
8
|
+
getAll(name?: string | {name: string}): RequestCookie[];
|
|
9
|
+
has(name: string): boolean;
|
|
10
|
+
set(name: string, value: string): this;
|
|
11
|
+
set(cookie: RequestCookie): this;
|
|
12
|
+
delete(name: string): boolean;
|
|
13
|
+
delete(names: string[]): boolean[];
|
|
14
|
+
clear(): this;
|
|
15
|
+
toString(): string;
|
|
16
|
+
}
|
|
17
|
+
export class NextRequest extends Request { constructor(input: RequestInfo | URL, init?: RequestInit & { nextConfig?: NextURLConfig }); nextUrl: NextURL; cookies: RequestCookieStore }
|
|
5
18
|
export interface MiddlewareResponseInit extends ResponseInit { request?: { headers?: Headers } }
|
|
6
19
|
export class NextResponse<Body = unknown> extends Response {
|
|
7
20
|
cookies: CookieStore;
|
|
@@ -27,6 +27,13 @@ function dynamicUsage(reason, request = context()) {
|
|
|
27
27
|
function staticBailout(reason, request = context()) {
|
|
28
28
|
if (diagnostics.enabled) diagnostics.record('dynamic', { route: diagnostics.route(request), reason });
|
|
29
29
|
const state = request?.staticState;
|
|
30
|
+
if (!state && request?.navigationStage && ['an uncached fetch', 'connection()', 'unstable_noStore()'].includes(reason)
|
|
31
|
+
&& !require('./data-cache.cjs').inCacheScope()) require('./navigation-stages.cjs').postponeStage(reason);
|
|
32
|
+
const requirement = request?.cacheConfig?.ensureStatic;
|
|
33
|
+
if (!state && (request?.navigationStage === 'shell' && ['shell', 'prefetch', 'navigation'].includes(requirement)
|
|
34
|
+
|| request?.navigationStage === 'prefetch' && ['prefetch', 'navigation'].includes(requirement))) {
|
|
35
|
+
require('./navigation-stages.cjs').postponeStage(reason);
|
|
36
|
+
}
|
|
30
37
|
if (!state) return true;
|
|
31
38
|
if (state.partial && state.mode === 'auto') {
|
|
32
39
|
state.dynamicReasons.add(reason);
|
package/compat/use-cache.cjs
CHANGED
|
@@ -35,6 +35,9 @@ function recordCacheDependency({ tags = [], revalidate } = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
function metadata(scope, context) { return { ...scope.life, revalidate: Math.min(scope.life.revalidate, scope.life.expire), tags: [...scope.tags], paths: getCachePaths(context), ...(scope.externalCache ? { externalCache: true } : {}) }; }
|
|
37
37
|
function propagate(info, parent, context) {
|
|
38
|
+
if (context?.staticState && context.cacheConfig?.ensureStatic === 'navigation' && (info.expire < 300 || info.stale < 30 || info.revalidate === 0)) {
|
|
39
|
+
dynamicUsage('a cache lifetime with expire below 300 seconds, stale below 30 seconds, or revalidate=0', context);
|
|
40
|
+
}
|
|
38
41
|
trackStaticDependency(info, context);
|
|
39
42
|
if (!parent || parent === true) return;
|
|
40
43
|
if (info.externalCache) parent.externalCache = true;
|