vinext 1.0.0 → 1.1.0
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 +72 -1
- package/THIRD_PARTY_LICENSES.md +26 -0
- package/dist/build/app-page-static-eligibility.d.ts +15 -0
- package/dist/build/app-page-static-eligibility.js +82 -0
- package/dist/build/lifecycle.js +10 -10
- package/dist/build/pages-client-assets-module.js +6 -1
- package/dist/build/precompress.js +1 -1
- package/dist/build/prerender-paths.js +6 -85
- package/dist/build/prerender.d.ts +8 -1
- package/dist/build/prerender.js +167 -46
- package/dist/build/run-prerender.js +4 -3
- package/dist/build/server-entry.d.ts +2 -0
- package/dist/build/server-entry.js +7 -1
- package/dist/build/server-manifest.d.ts +7 -0
- package/dist/build/server-manifest.js +11 -1
- package/dist/cache/cache-adapters-virtual.d.ts +7 -1
- package/dist/cache/cache-adapters-virtual.js +3 -3
- package/dist/client/browser-globals.d.ts +4 -0
- package/dist/client/browser-globals.js +5 -0
- package/dist/client/navigation-runtime.d.ts +11 -1
- package/dist/client/navigation-runtime.js +3 -3
- package/dist/config/config-matchers.js +3 -1
- package/dist/config/dotenv.js +14 -3
- package/dist/config/next-config.d.ts +7 -0
- package/dist/config/next-config.js +18 -3
- package/dist/entries/app-rsc-entry.d.ts +2 -0
- package/dist/entries/app-rsc-entry.js +67 -45
- package/dist/index.d.ts +4 -1
- package/dist/index.js +200 -100
- package/dist/init-cloudflare.d.ts +1 -1
- package/dist/init-cloudflare.js +6 -6
- package/dist/init-platform.js +1 -1
- package/dist/init.js +4 -2
- package/dist/plugins/cache-flight-codec.d.ts +19 -0
- package/dist/plugins/cache-flight-codec.js +68 -0
- package/dist/plugins/commonjs-esm-facade.d.ts +35 -0
- package/dist/plugins/commonjs-esm-facade.js +80 -0
- package/dist/plugins/commonjs-syntax.d.ts +16 -0
- package/dist/plugins/commonjs-syntax.js +22 -0
- package/dist/plugins/fonts.d.ts +8 -11
- package/dist/plugins/fonts.js +71 -34
- package/dist/plugins/ignore-dynamic-requests.d.ts +11 -0
- package/dist/plugins/ignore-dynamic-requests.js +54 -3
- package/dist/plugins/import-meta-url.js +6 -6
- package/dist/plugins/og-asset-ownership.d.ts +2 -0
- package/dist/plugins/og-asset-ownership.js +24 -13
- package/dist/plugins/og-assets.d.ts +18 -0
- package/dist/plugins/og-assets.js +50 -8
- package/dist/plugins/og-harfbuzz.js +38 -27
- package/dist/plugins/optimize-imports.js +2 -1
- package/dist/plugins/require-context.d.ts +1 -0
- package/dist/plugins/require-context.js +28 -1
- package/dist/plugins/scan-build-css.d.ts +12 -0
- package/dist/plugins/scan-build-css.js +39 -0
- package/dist/plugins/strip-server-exports.d.ts +2 -0
- package/dist/plugins/strip-server-exports.js +3 -1
- package/dist/plugins/transform-result.d.ts +18 -0
- package/dist/plugins/transform-result.js +23 -2
- package/dist/plugins/typeof-window.d.ts +10 -0
- package/dist/plugins/typeof-window.js +33 -9
- package/dist/routing/app-route-graph.d.ts +0 -1
- package/dist/routing/app-route-graph.js +2 -4
- package/dist/routing/app-route-ids.d.ts +3 -0
- package/dist/routing/app-route-ids.js +6 -0
- package/dist/routing/file-matcher.d.ts +1 -0
- package/dist/routing/file-matcher.js +1 -1
- package/dist/server/api-handler.js +17 -7
- package/dist/server/app-bfcache-id.d.ts +1 -0
- package/dist/server/app-bfcache-id.js +7 -1
- package/dist/server/app-bfcache-identity.js +1 -2
- package/dist/server/app-browser-entry.js +15 -9
- package/dist/server/app-browser-history-controller.d.ts +2 -0
- package/dist/server/app-browser-history-controller.js +8 -0
- package/dist/server/app-browser-hydration.d.ts +1 -1
- package/dist/server/app-browser-hydration.js +2 -2
- package/dist/server/app-browser-interception-context.js +26 -2
- package/dist/server/app-browser-navigation-controller.d.ts +7 -1
- package/dist/server/app-browser-navigation-controller.js +94 -11
- package/dist/server/app-browser-state.js +1 -1
- package/dist/server/app-browser-stream.d.ts +1 -1
- package/dist/server/app-fallback-renderer.d.ts +2 -0
- package/dist/server/app-fallback-renderer.js +1 -0
- package/dist/server/app-history-state.d.ts +3 -1
- package/dist/server/app-history-state.js +9 -5
- package/dist/server/app-middleware.js +2 -1
- package/dist/server/app-optimistic-routing.js +1 -1
- package/dist/server/app-page-boundary-render.d.ts +5 -0
- package/dist/server/app-page-boundary-render.js +6 -4
- package/dist/server/app-page-cache-finalizer.d.ts +11 -0
- package/dist/server/app-page-cache-finalizer.js +41 -7
- package/dist/server/app-page-cache-render.d.ts +18 -0
- package/dist/server/app-page-cache-render.js +65 -44
- package/dist/server/app-page-cache.d.ts +5 -0
- package/dist/server/app-page-cache.js +27 -7
- package/dist/server/app-page-dispatch.d.ts +9 -1
- package/dist/server/app-page-dispatch.js +47 -30
- package/dist/server/app-page-element-builder.d.ts +7 -0
- package/dist/server/app-page-element-builder.js +27 -42
- package/dist/server/app-page-execution.d.ts +15 -12
- package/dist/server/app-page-execution.js +30 -40
- package/dist/server/app-page-head.d.ts +5 -0
- package/dist/server/app-page-head.js +9 -3
- package/dist/server/app-page-probe.d.ts +0 -109
- package/dist/server/app-page-probe.js +3 -136
- package/dist/server/app-page-render-identity.d.ts +0 -1
- package/dist/server/app-page-render-identity.js +1 -1
- package/dist/server/app-page-render-observation.js +2 -2
- package/dist/server/app-page-render.d.ts +13 -5
- package/dist/server/app-page-render.js +219 -118
- package/dist/server/app-page-request.d.ts +0 -1
- package/dist/server/app-page-request.js +1 -2
- package/dist/server/app-page-response.d.ts +4 -3
- package/dist/server/app-page-response.js +4 -3
- package/dist/server/app-page-route-wiring.d.ts +17 -5
- package/dist/server/app-page-route-wiring.js +55 -33
- package/dist/server/app-page-rsc-render-status.d.ts +48 -0
- package/dist/server/app-page-rsc-render-status.js +54 -0
- package/dist/server/app-page-search-params-observation.js +1 -1
- package/dist/server/app-page-stream.d.ts +15 -8
- package/dist/server/app-page-stream.js +7 -1
- package/dist/server/app-page-tracing.js +2 -1
- package/dist/server/app-pages-bridge.js +3 -2
- package/dist/server/app-render-abort-error.d.ts +3 -0
- package/dist/server/app-render-abort-error.js +8 -0
- package/dist/server/app-request-stage-dispatch.js +1 -1
- package/dist/server/app-request-stage-independent-entry.js +8 -11
- package/dist/server/app-response-header-provenance.d.ts +2 -0
- package/dist/server/app-response-header-provenance.js +10 -4
- package/dist/server/app-route-handler-cache.d.ts +2 -1
- package/dist/server/app-route-handler-cache.js +11 -7
- package/dist/server/app-route-handler-dispatch.js +11 -4
- package/dist/server/app-route-handler-execution.d.ts +9 -1
- package/dist/server/app-route-handler-execution.js +28 -13
- package/dist/server/app-route-handler-policy.js +2 -2
- package/dist/server/app-route-handler-response.d.ts +1 -1
- package/dist/server/app-route-handler-response.js +14 -5
- package/dist/server/app-router-entry.d.ts +2 -1
- package/dist/server/app-router-entry.js +8 -12
- package/dist/server/app-rsc-errors.d.ts +0 -1
- package/dist/server/app-rsc-errors.js +2 -6
- package/dist/server/app-rsc-handler.d.ts +5 -0
- package/dist/server/app-rsc-handler.js +229 -102
- package/dist/server/app-rsc-request-normalization.d.ts +4 -2
- package/dist/server/app-rsc-request-normalization.js +7 -4
- package/dist/server/app-rsc-response-finalizer.js +4 -1
- package/dist/server/app-rsc-response-stage.d.ts +1 -1
- package/dist/server/app-rsc-response-stage.js +6 -4
- package/dist/server/app-rsc-route-matching.d.ts +9 -1
- package/dist/server/app-rsc-route-matching.js +37 -20
- package/dist/server/app-server-action-execution.d.ts +2 -1
- package/dist/server/app-server-action-execution.js +1 -1
- package/dist/server/app-ssr-entry.d.ts +3 -2
- package/dist/server/app-ssr-entry.js +9 -7
- package/dist/server/app-ssr-search-params-gate.js +1 -1
- package/dist/server/app-ssr-stream.d.ts +6 -1
- package/dist/server/app-ssr-stream.js +35 -16
- package/dist/server/app-worker-stages.d.ts +2 -2
- package/dist/server/app-worker-stages.js +3 -3
- package/dist/server/cache-control.js +3 -11
- package/dist/server/cacheability-request.js +56 -52
- package/dist/server/config-headers.js +3 -1
- package/dist/server/dev-server.js +23 -5
- package/dist/server/headers.d.ts +16 -0
- package/dist/server/headers.js +19 -1
- package/dist/server/http-error-responses.js +1 -0
- package/dist/server/image-optimization.d.ts +16 -0
- package/dist/server/image-optimization.js +26 -6
- package/dist/server/isr-cache.d.ts +2 -0
- package/dist/server/isr-cache.js +8 -3
- package/dist/server/metadata-route-response.js +94 -37
- package/dist/server/middleware-matcher-pattern.js +8 -4
- package/dist/server/middleware-response-headers.js +2 -0
- package/dist/server/multi-stage.d.ts +8 -0
- package/dist/server/navigation-planner.js +1 -1
- package/dist/server/node-response-signal.d.ts +16 -0
- package/dist/server/node-response-signal.js +26 -0
- package/dist/server/open-redirect.d.ts +47 -0
- package/dist/server/open-redirect.js +74 -1
- package/dist/server/pages-api-route.js +8 -2
- package/dist/server/pages-asset-tags.d.ts +1 -0
- package/dist/server/pages-asset-tags.js +73 -22
- package/dist/server/pages-client-assets.d.ts +2 -0
- package/dist/server/pages-page-data.d.ts +6 -1
- package/dist/server/pages-page-data.js +71 -65
- package/dist/server/pages-page-handler.js +50 -15
- package/dist/server/pages-page-response.js +3 -1
- package/dist/server/pages-request-pipeline.d.ts +2 -1
- package/dist/server/pages-request-pipeline.js +20 -5
- package/dist/server/pages-request-stage-entry.js +14 -8
- package/dist/server/prerender-manifest.d.ts +6 -0
- package/dist/server/prerender-manifest.js +1 -1
- package/dist/server/prod-server.d.ts +8 -7
- package/dist/server/prod-server.js +152 -78
- package/dist/server/request-pipeline.d.ts +20 -8
- package/dist/server/request-pipeline.js +21 -7
- package/dist/server/response-compression.d.ts +43 -0
- package/dist/server/response-compression.js +111 -0
- package/dist/server/response-stage-cacheability.js +1 -1
- package/dist/server/seed-cache.js +10 -7
- package/dist/server/socket-error-backstop.d.ts +14 -0
- package/dist/server/socket-error-backstop.js +27 -4
- package/dist/server/worker-revalidation-context.d.ts +2 -1
- package/dist/server/worker-revalidation-context.js +1 -0
- package/dist/server/workers-tracing.d.ts +17 -0
- package/dist/server/workers-tracing.js +48 -8
- package/dist/shims/cache-adapter-instantiate.d.ts +50 -0
- package/dist/shims/cache-adapter-instantiate.js +118 -0
- package/dist/shims/cache-callable-runtime.d.ts +2 -40
- package/dist/shims/cache-callable-runtime.js +50 -168
- package/dist/shims/cache-flight-arguments.d.ts +22 -0
- package/dist/shims/cache-flight-arguments.js +56 -0
- package/dist/shims/cache-request-state.d.ts +18 -14
- package/dist/shims/cache-request-state.js +33 -21
- package/dist/shims/cache-runtime.d.ts +6 -19
- package/dist/shims/cache-runtime.js +168 -226
- package/dist/shims/cache.d.ts +3 -2
- package/dist/shims/cache.js +8 -5
- package/dist/shims/cacheability-classification.d.ts +8 -1
- package/dist/shims/cacheability-classification.js +2 -2
- package/dist/shims/cdn-cache.d.ts +10 -1
- package/dist/shims/cdn-cache.js +16 -9
- package/dist/shims/error-boundary-navigation.d.ts +7 -0
- package/dist/shims/error-boundary-navigation.js +19 -1
- package/dist/shims/error-boundary.d.ts +10 -10
- package/dist/shims/error-boundary.js +26 -7
- package/dist/shims/error.d.ts +4 -18
- package/dist/shims/error.js +17 -10
- package/dist/shims/error.react-server.d.ts +4 -6
- package/dist/shims/error.react-server.js +4 -1
- package/dist/shims/fetch-cache.d.ts +6 -0
- package/dist/shims/fetch-cache.js +23 -4
- package/dist/shims/form.js +1 -1
- package/dist/shims/headers.d.ts +19 -69
- package/dist/shims/headers.js +82 -183
- package/dist/shims/internal/app-page-props-cache-key.d.ts +5 -2
- package/dist/shims/internal/app-page-props-cache-key.js +24 -15
- package/dist/shims/internal/cache-context-accessor.d.ts +16 -0
- package/dist/shims/internal/cache-context-accessor.js +15 -0
- package/dist/shims/internal/headers-state.d.ts +73 -0
- package/dist/shims/internal/headers-state.js +157 -0
- package/dist/shims/metadata.js +3 -3
- package/dist/shims/navigation-errors.d.ts +6 -0
- package/dist/shims/navigation-errors.js +11 -1
- package/dist/shims/navigation.d.ts +7 -0
- package/dist/shims/navigation.js +16 -6
- package/dist/shims/request-context.d.ts +9 -0
- package/dist/shims/request-state-types.d.ts +2 -1
- package/dist/shims/root-params.js +1 -1
- package/dist/shims/server.js +52 -2
- package/dist/shims/thenable-params.d.ts +4 -0
- package/dist/shims/thenable-params.js +25 -3
- package/dist/shims/unified-request-context.d.ts +1 -1
- package/dist/shims/unified-request-context.js +1 -0
- package/dist/utils/encode-cache-tag.d.ts +1 -0
- package/dist/utils/encode-cache-tag.js +11 -1
- package/dist/utils/mdx-scan.d.ts +5 -0
- package/dist/utils/mdx-scan.js +8 -1
- package/dist/utils/regex-safety.d.ts +10 -0
- package/dist/utils/regex-safety.js +70 -18
- package/dist/utils/vite-cli-invocation.js +8 -1
- package/package.json +8 -4
- package/dist/server/metadata-route-cache-policy.d.ts +0 -5
- package/dist/server/metadata-route-cache-policy.js +0 -6
|
@@ -3,7 +3,7 @@ import { getUnconsumedMiddlewareRequestHeaders } from "../utils/middleware-reque
|
|
|
3
3
|
import { hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-path.js";
|
|
4
4
|
import { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS } from "./headers.js";
|
|
5
5
|
import { forbiddenResponse, methodNotAllowedResponse, notFoundResponse } from "./http-error-responses.js";
|
|
6
|
-
import { isOpenRedirectShaped } from "./open-redirect.js";
|
|
6
|
+
import { getRepeatedSlashRedirect, isOpenRedirectShaped, repeatedSlashRedirectResponse, sendRepeatedSlashRedirect } from "./open-redirect.js";
|
|
7
7
|
import { createStaticFileSignal } from "./static-file-signal.js";
|
|
8
8
|
//#region src/server/request-pipeline.ts
|
|
9
9
|
const PATHNAME_CANONICALIZATION_BASE = new URL("http://vinext.invalid/");
|
|
@@ -52,10 +52,21 @@ function canonicalizeRequestUrlPathname(url) {
|
|
|
52
52
|
* Backslashes are equivalent to forward slashes in the URL spec
|
|
53
53
|
* (e.g. `/\evil.com` is treated as `//evil.com` by browsers).
|
|
54
54
|
*
|
|
55
|
-
* Next.js
|
|
56
|
-
*
|
|
55
|
+
* Like Next.js, any raw path containing a literal backslash or a repeated
|
|
56
|
+
* slash first gets a 308 to the collapsed path (`//evil.com` → `/evil.com`,
|
|
57
|
+
* `//` → `/`); see `getRepeatedSlashRedirect`. We check the RAW
|
|
58
|
+
* pathname before normalization so this fires before normalizePath collapses
|
|
59
|
+
* `//`.
|
|
57
60
|
*
|
|
58
|
-
*
|
|
61
|
+
* Request-based (Worker) entries only see `request.url` after WHATWG URL
|
|
62
|
+
* parsing, which has already turned `\` into `/`; the raw request target is
|
|
63
|
+
* not available there. `/\evil.com` still redirects (it arrives as
|
|
64
|
+
* `//evil.com`), but a lone backslash such as `/foo\bar` arrives as
|
|
65
|
+
* `/foo/bar` and is routed without the redirect. On Cloudflare, requests that
|
|
66
|
+
* match a static asset are also answered by the asset layer before the
|
|
67
|
+
* Worker runs. Node entries (dev and `vinext start`) check the raw target.
|
|
68
|
+
*
|
|
69
|
+
* Percent-encoded variants get a 404, as in Next.js, because:
|
|
59
70
|
* - `%5C` decodes to `\` (browsers treat `/\evil.com` as `//evil.com`).
|
|
60
71
|
* - `%2F` decodes to `/` (so `/%2F/evil.com` effectively becomes `//evil.com`).
|
|
61
72
|
* These forms survive segment-wise decoding that re-encodes path delimiters
|
|
@@ -64,9 +75,12 @@ function canonicalizeRequestUrlPathname(url) {
|
|
|
64
75
|
* `isOpenRedirectShaped` for the full list of rejected leading-segment forms.
|
|
65
76
|
*
|
|
66
77
|
* @param rawPathname - The raw pathname from the URL, before any normalization
|
|
67
|
-
* @
|
|
78
|
+
* @param search - The raw query string (including `?`), kept in the 308 Location
|
|
79
|
+
* @returns A 308 or 404 Response, or null to continue
|
|
68
80
|
*/
|
|
69
|
-
function guardProtocolRelativeUrl(rawPathname) {
|
|
81
|
+
function guardProtocolRelativeUrl(rawPathname, search = "") {
|
|
82
|
+
const slashRedirect = repeatedSlashRedirectResponse(rawPathname + search);
|
|
83
|
+
if (slashRedirect) return slashRedirect;
|
|
70
84
|
if (isOpenRedirectShaped(rawPathname)) return notFoundResponse();
|
|
71
85
|
return null;
|
|
72
86
|
}
|
|
@@ -418,4 +432,4 @@ function cloneRequestWithUrl(request, url) {
|
|
|
418
432
|
return attachRequestCfMetadata(cloned, request);
|
|
419
433
|
}
|
|
420
434
|
//#endregion
|
|
421
|
-
export { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, attachRequestCfMetadata, canonicalizeRequestPathname, canonicalizeRequestUrlPathname, cloneRequestWithHeaders, cloneRequestWithUrl, createStaticFileSignal, filterInternalHeaders, guardProtocolRelativeUrl, hasBasePath, isOpenRedirectShaped, isOriginAllowed, normalizeTrailingSlash, normalizeTrailingSlashPathname, processMiddlewareHeaders, resolvePublicFileRoute, stripBasePath, validateCsrfOrigin, validateServerActionPayload };
|
|
435
|
+
export { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, attachRequestCfMetadata, canonicalizeRequestPathname, canonicalizeRequestUrlPathname, cloneRequestWithHeaders, cloneRequestWithUrl, createStaticFileSignal, filterInternalHeaders, getRepeatedSlashRedirect, guardProtocolRelativeUrl, hasBasePath, isOpenRedirectShaped, isOriginAllowed, normalizeTrailingSlash, normalizeTrailingSlashPathname, processMiddlewareHeaders, resolvePublicFileRoute, sendRepeatedSlashRedirect, stripBasePath, validateCsrfOrigin, validateServerActionPayload };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NegotiatedEncoding } from "./accept-encoding.js";
|
|
2
|
+
import { IncomingMessage } from "node:http";
|
|
3
|
+
//#region src/server/response-compression.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether a Content-Type is compressible, matching `compressible()`. This
|
|
6
|
+
* covers `text/x-component` (RSC payloads) through the `text/` rule.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isCompressibleContentType(contentType: string | null | undefined): boolean;
|
|
9
|
+
/** Whether a Cache-Control value forbids content codings (`no-transform`). */
|
|
10
|
+
export declare function hasNoTransform(cacheControl: string | null | undefined): boolean;
|
|
11
|
+
/** Responses whose known length is below this many bytes are sent uncompressed. */
|
|
12
|
+
export declare const COMPRESS_THRESHOLD = 1024;
|
|
13
|
+
export type ResponseCompressionHeaders = {
|
|
14
|
+
contentType: string | null | undefined;
|
|
15
|
+
cacheControl: string | null | undefined;
|
|
16
|
+
contentEncoding: string | null | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Body length in bytes when it is known before the headers are written (a
|
|
19
|
+
* Content-Length header, or a buffered or empty body). Undefined for
|
|
20
|
+
* streamed bodies, which are compressed regardless of size.
|
|
21
|
+
*/
|
|
22
|
+
contentLength: number | undefined;
|
|
23
|
+
};
|
|
24
|
+
export type ResponseCompression = {
|
|
25
|
+
/** Whether the response must carry `Vary: Accept-Encoding`. */
|
|
26
|
+
varyAcceptEncoding: boolean;
|
|
27
|
+
/** The coding to apply. `identity` sends the body as is. */
|
|
28
|
+
encoding: NegotiatedEncoding;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Decide how to compress a response, in the middleware's order: the
|
|
32
|
+
* Content-Type filter and `Cache-Control: no-transform` skip compression
|
|
33
|
+
* without `Vary`; every other response gets `Vary: Accept-Encoding`, then is
|
|
34
|
+
* sent uncompressed when it is under the size threshold, already encoded, or
|
|
35
|
+
* a HEAD response, or when the client accepts no supported coding.
|
|
36
|
+
*
|
|
37
|
+
* Unlike the middleware, which offers only gzip and deflate, negotiation also
|
|
38
|
+
* offers zstd and br (see negotiateEncoding).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveResponseCompression(req: IncomingMessage, compress: boolean, headers: ResponseCompressionHeaders): ResponseCompression;
|
|
41
|
+
/** Parse a Content-Length header value the way the middleware compares it. */
|
|
42
|
+
export declare function parseContentLengthHeader(value: string | null | undefined): number | undefined;
|
|
43
|
+
//#endregion
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { negotiateEncoding } from "./accept-encoding.js";
|
|
2
|
+
//#region src/server/response-compression.ts
|
|
3
|
+
/**
|
|
4
|
+
* Response compression rules for the Node production server, ported from the
|
|
5
|
+
* `compression` middleware that `next start` installs for every response
|
|
6
|
+
* unless `compress: false` (next/dist/compiled/compression, see
|
|
7
|
+
* packages/next/src/server/lib/router-server.ts).
|
|
8
|
+
*/
|
|
9
|
+
const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
|
|
10
|
+
const COMPRESSIBLE_TYPE_REGEXP = /^text\/|\+(?:json|text|xml)$/i;
|
|
11
|
+
/**
|
|
12
|
+
* mime-db entries marked `compressible: true` that COMPRESSIBLE_TYPE_REGEXP
|
|
13
|
+
* does not match, taken from the mime-db bundled with Next.js's compression
|
|
14
|
+
* middleware. That mime-db marks no type `compressible: false` that the
|
|
15
|
+
* regexp matches, so this set plus the regexp reproduces `compressible()`.
|
|
16
|
+
*/
|
|
17
|
+
const MIME_DB_COMPRESSIBLE_TYPES = /* @__PURE__ */ new Set([
|
|
18
|
+
"application/dart",
|
|
19
|
+
"application/ecmascript",
|
|
20
|
+
"application/javascript",
|
|
21
|
+
"application/json",
|
|
22
|
+
"application/postscript",
|
|
23
|
+
"application/raml+yaml",
|
|
24
|
+
"application/rtf",
|
|
25
|
+
"application/tar",
|
|
26
|
+
"application/toml",
|
|
27
|
+
"application/vnd.dart",
|
|
28
|
+
"application/vnd.ms-fontobject",
|
|
29
|
+
"application/vnd.ms-opentype",
|
|
30
|
+
"application/wasm",
|
|
31
|
+
"application/x-httpd-php",
|
|
32
|
+
"application/x-javascript",
|
|
33
|
+
"application/x-ns-proxy-autoconfig",
|
|
34
|
+
"application/x-sh",
|
|
35
|
+
"application/x-tar",
|
|
36
|
+
"application/x-virtualbox-hdd",
|
|
37
|
+
"application/x-virtualbox-ova",
|
|
38
|
+
"application/x-virtualbox-ovf",
|
|
39
|
+
"application/x-virtualbox-vbox",
|
|
40
|
+
"application/x-virtualbox-vdi",
|
|
41
|
+
"application/x-virtualbox-vhd",
|
|
42
|
+
"application/x-virtualbox-vmdk",
|
|
43
|
+
"application/x-www-form-urlencoded",
|
|
44
|
+
"application/xml",
|
|
45
|
+
"application/xml-dtd",
|
|
46
|
+
"font/otf",
|
|
47
|
+
"font/ttf",
|
|
48
|
+
"image/bmp",
|
|
49
|
+
"image/vnd.adobe.photoshop",
|
|
50
|
+
"image/x-icon",
|
|
51
|
+
"image/x-ms-bmp",
|
|
52
|
+
"message/rfc822",
|
|
53
|
+
"model/gltf-binary",
|
|
54
|
+
"x-shader/x-fragment",
|
|
55
|
+
"x-shader/x-vertex"
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Whether a Content-Type is compressible, matching `compressible()`. This
|
|
59
|
+
* covers `text/x-component` (RSC payloads) through the `text/` rule.
|
|
60
|
+
*/
|
|
61
|
+
function isCompressibleContentType(contentType) {
|
|
62
|
+
if (!contentType) return false;
|
|
63
|
+
const match = EXTRACT_TYPE_REGEXP.exec(contentType);
|
|
64
|
+
const mime = match ? match[1].toLowerCase() : "";
|
|
65
|
+
return MIME_DB_COMPRESSIBLE_TYPES.has(mime) || COMPRESSIBLE_TYPE_REGEXP.test(mime);
|
|
66
|
+
}
|
|
67
|
+
const NO_TRANSFORM_REGEXP = /(?:^|,)\s*?no-transform\s*?(?:,|$)/;
|
|
68
|
+
/** Whether a Cache-Control value forbids content codings (`no-transform`). */
|
|
69
|
+
function hasNoTransform(cacheControl) {
|
|
70
|
+
return !!cacheControl && NO_TRANSFORM_REGEXP.test(cacheControl);
|
|
71
|
+
}
|
|
72
|
+
/** Responses whose known length is below this many bytes are sent uncompressed. */
|
|
73
|
+
const COMPRESS_THRESHOLD = 1024;
|
|
74
|
+
const NO_COMPRESSION = {
|
|
75
|
+
varyAcceptEncoding: false,
|
|
76
|
+
encoding: "identity"
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Decide how to compress a response, in the middleware's order: the
|
|
80
|
+
* Content-Type filter and `Cache-Control: no-transform` skip compression
|
|
81
|
+
* without `Vary`; every other response gets `Vary: Accept-Encoding`, then is
|
|
82
|
+
* sent uncompressed when it is under the size threshold, already encoded, or
|
|
83
|
+
* a HEAD response, or when the client accepts no supported coding.
|
|
84
|
+
*
|
|
85
|
+
* Unlike the middleware, which offers only gzip and deflate, negotiation also
|
|
86
|
+
* offers zstd and br (see negotiateEncoding).
|
|
87
|
+
*/
|
|
88
|
+
function resolveResponseCompression(req, compress, headers) {
|
|
89
|
+
if (!compress) return NO_COMPRESSION;
|
|
90
|
+
if (!isCompressibleContentType(headers.contentType)) return NO_COMPRESSION;
|
|
91
|
+
if (hasNoTransform(headers.cacheControl)) return NO_COMPRESSION;
|
|
92
|
+
const identity = {
|
|
93
|
+
varyAcceptEncoding: true,
|
|
94
|
+
encoding: "identity"
|
|
95
|
+
};
|
|
96
|
+
if (headers.contentLength !== void 0 && headers.contentLength < 1024) return identity;
|
|
97
|
+
if ((headers.contentEncoding || "identity") !== "identity") return identity;
|
|
98
|
+
if (req.method === "HEAD") return identity;
|
|
99
|
+
return {
|
|
100
|
+
varyAcceptEncoding: true,
|
|
101
|
+
encoding: negotiateEncoding(req)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Parse a Content-Length header value the way the middleware compares it. */
|
|
105
|
+
function parseContentLengthHeader(value) {
|
|
106
|
+
if (value === null || value === void 0) return void 0;
|
|
107
|
+
const length = Number(value);
|
|
108
|
+
return Number.isNaN(length) ? void 0 : length;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { COMPRESS_THRESHOLD, hasNoTransform, isCompressibleContentType, parseContentLengthHeader, resolveResponseCompression };
|
|
@@ -34,7 +34,7 @@ async function withResponseStageCacheability(options, render) {
|
|
|
34
34
|
const response = options.policyHeadersAppliedBeforeRender ? rendered : cacheability.applyResponseStageCachePolicy(rendered, context, options.policyHeaders);
|
|
35
35
|
const complete = (candidate) => cacheability.finalizeWorkerCacheabilityResponse(candidate, context);
|
|
36
36
|
const route = state?.route;
|
|
37
|
-
if (!options.probeMode && state?.admission?.policy !== "manifest" && (route?.kind === "app-page" || route?.kind === "pages-page")) {
|
|
37
|
+
if (!options.probeMode && state?.admission?.policy !== "manifest" && (route?.kind === "app-page" || route?.kind === "pages-page") && !(route.kind === "pages-page" && state?.outcome?.cacheable === false)) {
|
|
38
38
|
const deferred = adapter.deferCompletedPageResponseAdmission?.(response, complete);
|
|
39
39
|
if (deferred) return deferred;
|
|
40
40
|
}
|
|
@@ -70,8 +70,11 @@ async function seedMemoryCacheFromPrerender(serverDir, options) {
|
|
|
70
70
|
const expireSeconds = typeof route.expire === "number" ? route.expire : void 0;
|
|
71
71
|
const staleSeconds = typeof route.stale === "number" && route.stale >= 0 ? route.stale : void 0;
|
|
72
72
|
const tags = buildAppPageCacheTags(cachePathname, route.tags ?? []);
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const status = route.responseStatus;
|
|
74
|
+
const location = route.headers?.location;
|
|
75
|
+
const rscHeaders = status !== void 0 && location !== void 0 ? { location } : void 0;
|
|
76
|
+
if (await seedHtml(writeAppPageEntry, prerenderDir, htmlKey, artifactPathname, trailingSlash, route.headers, status, revalidateSeconds, expireSeconds, staleSeconds, tags)) {
|
|
77
|
+
await seedRsc(writeAppPageEntry, prerenderDir, rscKey, artifactPathname, rscHeaders, status, revalidateSeconds, expireSeconds, staleSeconds, tags);
|
|
75
78
|
seeded++;
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -107,7 +110,7 @@ function createDefaultAppPageEntryWriter() {
|
|
|
107
110
|
* Seed the HTML cache entry for a single route.
|
|
108
111
|
* Returns true if the file existed and was seeded.
|
|
109
112
|
*/
|
|
110
|
-
async function seedHtml(writeAppPageEntry, prerenderDir, key, pathname, trailingSlash, headers, revalidateSeconds, expireSeconds, staleSeconds, tags) {
|
|
113
|
+
async function seedHtml(writeAppPageEntry, prerenderDir, key, pathname, trailingSlash, headers, status, revalidateSeconds, expireSeconds, staleSeconds, tags) {
|
|
111
114
|
const relPath = getOutputPath(pathname, trailingSlash);
|
|
112
115
|
const fullPath = path.join(prerenderDir, relPath);
|
|
113
116
|
if (!fs.existsSync(fullPath)) return false;
|
|
@@ -117,7 +120,7 @@ async function seedHtml(writeAppPageEntry, prerenderDir, key, pathname, trailing
|
|
|
117
120
|
rscData: void 0,
|
|
118
121
|
headers,
|
|
119
122
|
postponed: void 0,
|
|
120
|
-
status
|
|
123
|
+
status
|
|
121
124
|
}, {
|
|
122
125
|
expireSeconds,
|
|
123
126
|
revalidateSeconds,
|
|
@@ -130,7 +133,7 @@ async function seedHtml(writeAppPageEntry, prerenderDir, key, pathname, trailing
|
|
|
130
133
|
* Seed the RSC cache entry for a single route.
|
|
131
134
|
* No-op if the .rsc file doesn't exist on disk.
|
|
132
135
|
*/
|
|
133
|
-
async function seedRsc(writeAppPageEntry, prerenderDir, key, pathname, revalidateSeconds, expireSeconds, staleSeconds, tags) {
|
|
136
|
+
async function seedRsc(writeAppPageEntry, prerenderDir, key, pathname, headers, status, revalidateSeconds, expireSeconds, staleSeconds, tags) {
|
|
134
137
|
const relPath = getRscOutputPath(pathname);
|
|
135
138
|
const fullPath = path.join(prerenderDir, relPath);
|
|
136
139
|
if (!fs.existsSync(fullPath)) return;
|
|
@@ -139,9 +142,9 @@ async function seedRsc(writeAppPageEntry, prerenderDir, key, pathname, revalidat
|
|
|
139
142
|
kind: "APP_PAGE",
|
|
140
143
|
html: "",
|
|
141
144
|
rscData: rscBuffer.buffer.slice(rscBuffer.byteOffset, rscBuffer.byteOffset + rscBuffer.byteLength),
|
|
142
|
-
headers
|
|
145
|
+
headers,
|
|
143
146
|
postponed: void 0,
|
|
144
|
-
status
|
|
147
|
+
status
|
|
145
148
|
}, {
|
|
146
149
|
expireSeconds,
|
|
147
150
|
revalidateSeconds,
|
|
@@ -23,6 +23,20 @@ export declare function peerDisconnectCode(err: unknown): string | undefined;
|
|
|
23
23
|
* Exported for unit testing in isolation.
|
|
24
24
|
*/
|
|
25
25
|
export declare function isBenignAssetImportError(err: unknown): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Pure predicate: returns `true` when `err` is Node's
|
|
28
|
+
* `Readable.toWeb()` adapter enqueueing into a request body stream that was
|
|
29
|
+
* already cancelled. `pull()` calls `req.resume()`, which schedules `flow()`
|
|
30
|
+
* on `nextTick`; if the body is cancelled before that tick runs, `flow()`
|
|
31
|
+
* still emits the chunks already buffered in `req` and the adapter's
|
|
32
|
+
* `onData` throws inside the `nextTick` callback. srvx (used by
|
|
33
|
+
* `@vitejs/plugin-rsc` in dev) builds `request.body` this way, so any
|
|
34
|
+
* cancelled request body, e.g. an oversized Server Action, can crash the
|
|
35
|
+
* dev server. Node main drops that data instead (`if (wasCanceled) return;`
|
|
36
|
+
* in `onData`), so absorbing it matches upstream behaviour. Exported for
|
|
37
|
+
* unit testing in isolation.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isCancelledRequestBodyEnqueueError(err: unknown): boolean;
|
|
26
40
|
/**
|
|
27
41
|
* Test-only: returns whether the backstop has been installed in this
|
|
28
42
|
* process. Used by the unit test to assert idempotent install via the
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* `uncaughtException`, where this listener filters it.
|
|
16
16
|
*
|
|
17
17
|
* Filters strictly on peer-disconnect codes (ECONNRESET / EPIPE /
|
|
18
|
-
* ECONNABORTED)
|
|
19
|
-
* `isBenignAssetImportError`)
|
|
20
|
-
*
|
|
18
|
+
* ECONNABORTED), benign static-asset `import()` rejections (see
|
|
19
|
+
* `isBenignAssetImportError`) and Node's cancelled-request-body enqueue
|
|
20
|
+
* race (see `isCancelledRequestBodyEnqueueError`), and synchronously
|
|
21
|
+
* re-throws everything else, preserving Node's default crash semantics for genuine bugs.
|
|
21
22
|
* This is more conservative than Next.js's equivalent
|
|
22
23
|
* (`router-server.ts`'s log-only handler), which silently swallows
|
|
23
24
|
* every uncaught — vinext keeps real bugs surfacing.
|
|
@@ -153,6 +154,24 @@ function isBenignAssetImportError(err) {
|
|
|
153
154
|
return false;
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
157
|
+
* Pure predicate: returns `true` when `err` is Node's
|
|
158
|
+
* `Readable.toWeb()` adapter enqueueing into a request body stream that was
|
|
159
|
+
* already cancelled. `pull()` calls `req.resume()`, which schedules `flow()`
|
|
160
|
+
* on `nextTick`; if the body is cancelled before that tick runs, `flow()`
|
|
161
|
+
* still emits the chunks already buffered in `req` and the adapter's
|
|
162
|
+
* `onData` throws inside the `nextTick` callback. srvx (used by
|
|
163
|
+
* `@vitejs/plugin-rsc` in dev) builds `request.body` this way, so any
|
|
164
|
+
* cancelled request body, e.g. an oversized Server Action, can crash the
|
|
165
|
+
* dev server. Node main drops that data instead (`if (wasCanceled) return;`
|
|
166
|
+
* in `onData`), so absorbing it matches upstream behaviour. Exported for
|
|
167
|
+
* unit testing in isolation.
|
|
168
|
+
*/
|
|
169
|
+
function isCancelledRequestBodyEnqueueError(err) {
|
|
170
|
+
if (err === null || typeof err !== "object") return false;
|
|
171
|
+
const { code, message, stack } = err;
|
|
172
|
+
return code === "ERR_INVALID_STATE" && typeof message === "string" && message.includes("Controller is already closed") && typeof stack === "string" && /\.onData \(node:internal\/webstreams\/adapters:\d+/.test(stack);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
156
175
|
* Test-only: returns whether the backstop has been installed in this
|
|
157
176
|
* process. Used by the unit test to assert idempotent install via the
|
|
158
177
|
* Symbol.for guard. Not part of the public API.
|
|
@@ -183,6 +202,10 @@ function installSocketErrorBackstop() {
|
|
|
183
202
|
if (debug) console.warn(`[vinext] absorbed uncaughtException ${code}`);
|
|
184
203
|
return;
|
|
185
204
|
}
|
|
205
|
+
if (isCancelledRequestBodyEnqueueError(err)) {
|
|
206
|
+
if (debug) console.warn("[vinext] absorbed uncaughtException ERR_INVALID_STATE (cancelled request body)");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
186
209
|
throw err;
|
|
187
210
|
});
|
|
188
211
|
process.on("unhandledRejection", (reason) => {
|
|
@@ -197,4 +220,4 @@ function installSocketErrorBackstop() {
|
|
|
197
220
|
});
|
|
198
221
|
}
|
|
199
222
|
//#endregion
|
|
200
|
-
export { installSocketErrorBackstop, isBenignAssetImportError, isSocketErrorBackstopInstalled, peerDisconnectCode };
|
|
223
|
+
export { installSocketErrorBackstop, isBenignAssetImportError, isCancelledRequestBodyEnqueueError, isSocketErrorBackstopInstalled, peerDisconnectCode };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { VinextRequestStageContext } from "./multi-stage.js";
|
|
1
2
|
import { ExecutionContextLike } from "../shims/request-context.js";
|
|
2
3
|
//#region src/server/worker-revalidation-context.d.ts
|
|
3
|
-
type PlatformExecutionContext = Partial<ExecutionContextLike
|
|
4
|
+
type PlatformExecutionContext = Partial<ExecutionContextLike> & VinextRequestStageContext;
|
|
4
5
|
/**
|
|
5
6
|
* Add a request-local, in-process Pages revalidation dispatcher to a Worker
|
|
6
7
|
* execution context. Re-entering with the derived internal context preserves
|
|
@@ -10,6 +10,7 @@ function deriveExecutionContext(base, dispatchPagesRevalidate, isInternalPagesRe
|
|
|
10
10
|
} } : {},
|
|
11
11
|
hostRuntime: base?.hostRuntime ?? defaultHostRuntime,
|
|
12
12
|
...base?.cache === void 0 ? {} : { cache: base.cache },
|
|
13
|
+
...base?.assets === void 0 ? {} : { assets: base.assets },
|
|
13
14
|
...base?.trustedRevalidateOrigin === void 0 ? {} : { trustedRevalidateOrigin: base.trustedRevalidateOrigin },
|
|
14
15
|
dispatchPagesRevalidate,
|
|
15
16
|
isInternalPagesRevalidation
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { FrameworkTracingIntegration } from "./framework-tracer.js";
|
|
2
2
|
//#region src/server/workers-tracing.d.ts
|
|
3
3
|
export type WorkersTracingException = string | {
|
|
4
|
+
code: string | number;
|
|
5
|
+
name?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
stack?: string;
|
|
8
|
+
} | {
|
|
9
|
+
code?: string | number;
|
|
4
10
|
name: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
stack?: string;
|
|
13
|
+
} | {
|
|
14
|
+
code?: string | number;
|
|
15
|
+
name?: string;
|
|
5
16
|
message: string;
|
|
6
17
|
stack?: string;
|
|
7
18
|
};
|
|
@@ -9,9 +20,15 @@ export type WorkersTracingSpan = {
|
|
|
9
20
|
readonly isTraced: boolean;
|
|
10
21
|
recordException?(exception: WorkersTracingException): void;
|
|
11
22
|
setAttribute(key: string, value: boolean | number | string): void;
|
|
23
|
+
setStatus?(status: {
|
|
24
|
+
code: "error";
|
|
25
|
+
message?: string;
|
|
26
|
+
}): void;
|
|
27
|
+
updateName?(name: string): void;
|
|
12
28
|
};
|
|
13
29
|
export type WorkersTracing = {
|
|
14
30
|
enterSpan<T>(name: string, callback: (span: WorkersTracingSpan) => T): T;
|
|
31
|
+
getActiveSpan?(): WorkersTracingSpan | undefined;
|
|
15
32
|
};
|
|
16
33
|
export declare function createWorkersTracingIntegration(tracing: WorkersTracing): FrameworkTracingIntegration;
|
|
17
34
|
//#endregion
|
|
@@ -1,20 +1,60 @@
|
|
|
1
1
|
//#region src/server/workers-tracing.ts
|
|
2
|
+
function readExceptionField(error, key) {
|
|
3
|
+
try {
|
|
4
|
+
return Reflect.get(error, key);
|
|
5
|
+
} catch {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
2
9
|
function normalizeException(error) {
|
|
3
10
|
if (typeof error === "string") return error;
|
|
4
|
-
if (error
|
|
5
|
-
|
|
6
|
-
name
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
if (error && typeof error === "object") {
|
|
12
|
+
const code = readExceptionField(error, "code");
|
|
13
|
+
const name = readExceptionField(error, "name");
|
|
14
|
+
const message = readExceptionField(error, "message");
|
|
15
|
+
const stack = readExceptionField(error, "stack");
|
|
16
|
+
const details = {
|
|
17
|
+
...typeof name === "string" ? { name } : {},
|
|
18
|
+
...typeof message === "string" ? { message } : {},
|
|
19
|
+
...typeof stack === "string" ? { stack } : {}
|
|
20
|
+
};
|
|
21
|
+
if (typeof code === "string" || typeof code === "number") return {
|
|
22
|
+
...details,
|
|
23
|
+
code
|
|
24
|
+
};
|
|
25
|
+
if (typeof name === "string") return {
|
|
26
|
+
...details,
|
|
27
|
+
name
|
|
28
|
+
};
|
|
29
|
+
if (typeof message === "string") return {
|
|
30
|
+
...details,
|
|
31
|
+
message
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return String(error);
|
|
36
|
+
} catch {
|
|
37
|
+
return "Unknown exception";
|
|
38
|
+
}
|
|
10
39
|
}
|
|
11
40
|
function createWorkersTracingIntegration(tracing) {
|
|
12
41
|
const backendSpan = (span) => ({
|
|
13
|
-
recordException: (error) =>
|
|
14
|
-
|
|
42
|
+
recordException: (error) => {
|
|
43
|
+
if (span.isTraced) span.recordException?.(normalizeException(error));
|
|
44
|
+
},
|
|
45
|
+
setAttribute: (key, value) => span.setAttribute(key, value),
|
|
46
|
+
setErrorStatus: (message) => span.setStatus?.({
|
|
47
|
+
code: "error",
|
|
48
|
+
message
|
|
49
|
+
}),
|
|
50
|
+
updateName: (name) => span.updateName?.(name)
|
|
15
51
|
});
|
|
16
52
|
return {
|
|
17
53
|
id: "cloudflare-workers",
|
|
54
|
+
getActiveSpan() {
|
|
55
|
+
const span = tracing.getActiveSpan?.();
|
|
56
|
+
return span ? backendSpan(span) : void 0;
|
|
57
|
+
},
|
|
18
58
|
enterSpan(descriptor, callback) {
|
|
19
59
|
return tracing.enterSpan(descriptor.name, (span) => {
|
|
20
60
|
for (const [key, value] of Object.entries(descriptor.attributes)) span.setAttribute(key, value);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/shims/cache-adapter-instantiate.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Instantiate a declaratively configured cache adapter from its module's
|
|
4
|
+
* default export. The generated `virtual:vinext-cache-adapters` module calls
|
|
5
|
+
* this for both the `cache.data` and `cache.cdn` slots, so every router and
|
|
6
|
+
* runtime (Node.js dev/prod server and Cloudflare Workers) uses the same rule.
|
|
7
|
+
*
|
|
8
|
+
* Contract: the default export must be a function, and it receives one
|
|
9
|
+
* `{ env, options }` argument.
|
|
10
|
+
*
|
|
11
|
+
* - A `class` (class syntax, as Vite and every current toolchain emit it) is
|
|
12
|
+
* invoked with `new`. It is recognised by the source text that
|
|
13
|
+
* `Function.prototype.toString` must return for class syntax, which starts
|
|
14
|
+
* with `class`, on a function that has [[Construct]].
|
|
15
|
+
* - Any other function is called as a factory, with plain-call semantics
|
|
16
|
+
* (`this`, `new.target` and bound receivers are what a call gives them).
|
|
17
|
+
* Existing factories therefore behave exactly as before, whatever their
|
|
18
|
+
* `prototype` looks like (frozen, decorated, or inherited).
|
|
19
|
+
* - A constructor that is not class syntax (an ES5-compiled class, or a
|
|
20
|
+
* bound or Proxy-wrapped class, whose source text is not observable) is
|
|
21
|
+
* structurally indistinguishable from a factory without invoking it, so the
|
|
22
|
+
* module states it explicitly by exporting a factory that constructs it:
|
|
23
|
+
* `export default (args) => new Adapter(args)`.
|
|
24
|
+
*
|
|
25
|
+
* Classification never invokes the export, matches no error messages and
|
|
26
|
+
* never retries, so the export runs exactly once.
|
|
27
|
+
*
|
|
28
|
+
* The produced value must be an adapter (an object or a function, not a
|
|
29
|
+
* Promise) with the slot's required members; anything else throws an error
|
|
30
|
+
* naming what is wrong.
|
|
31
|
+
*/
|
|
32
|
+
export type CacheAdapterFactoryArgs = {
|
|
33
|
+
env: unknown;
|
|
34
|
+
options: unknown;
|
|
35
|
+
};
|
|
36
|
+
export type CacheAdapterSlot = "data" | "cdn";
|
|
37
|
+
/**
|
|
38
|
+
* Whether `value` has a [[Construct]] internal method. A Proxy is
|
|
39
|
+
* constructible exactly when its target is; its no-op `construct` trap
|
|
40
|
+
* answers without running the export or reading any of its properties.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isConstructor(value: unknown): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Whether an export is class syntax and must be invoked with `new`. The
|
|
45
|
+
* [[Construct]] check excludes methods named `class`, whose source text
|
|
46
|
+
* also starts with that word.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isClassExport(value: unknown): boolean;
|
|
49
|
+
export declare function instantiateCacheAdapter<T>(exported: unknown, args: CacheAdapterFactoryArgs, slot: CacheAdapterSlot): T;
|
|
50
|
+
//#endregion
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//#region src/shims/cache-adapter-instantiate.ts
|
|
2
|
+
const REQUIREMENTS = {
|
|
3
|
+
data: {
|
|
4
|
+
methods: [
|
|
5
|
+
"get",
|
|
6
|
+
"set",
|
|
7
|
+
"revalidateTag"
|
|
8
|
+
],
|
|
9
|
+
booleans: [],
|
|
10
|
+
description: "A data cache adapter implements get, set and revalidateTag."
|
|
11
|
+
},
|
|
12
|
+
cdn: {
|
|
13
|
+
methods: [
|
|
14
|
+
"get",
|
|
15
|
+
"set",
|
|
16
|
+
"revalidateTag",
|
|
17
|
+
"buildResponseHeaders"
|
|
18
|
+
],
|
|
19
|
+
booleans: ["ownsBackgroundRevalidation"],
|
|
20
|
+
description: "A CDN cache adapter implements get, set, revalidateTag and buildResponseHeaders, and sets ownsBackgroundRevalidation to a boolean."
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Whether `value` has a [[Construct]] internal method. A Proxy is
|
|
25
|
+
* constructible exactly when its target is; its no-op `construct` trap
|
|
26
|
+
* answers without running the export or reading any of its properties.
|
|
27
|
+
*/
|
|
28
|
+
function isConstructor(value) {
|
|
29
|
+
if (typeof value !== "function") return false;
|
|
30
|
+
try {
|
|
31
|
+
new new Proxy(value, { construct: () => ({}) })();
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const CLASS_SOURCE = /^class\b/;
|
|
38
|
+
/**
|
|
39
|
+
* Whether an export is class syntax and must be invoked with `new`. The
|
|
40
|
+
* [[Construct]] check excludes methods named `class`, whose source text
|
|
41
|
+
* also starts with that word.
|
|
42
|
+
*/
|
|
43
|
+
function isClassExport(value) {
|
|
44
|
+
if (!isConstructor(value)) return false;
|
|
45
|
+
try {
|
|
46
|
+
return CLASS_SOURCE.test(Function.prototype.toString.call(value));
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function describeValue(value) {
|
|
52
|
+
if (value === null) return "null";
|
|
53
|
+
if (Array.isArray(value)) return "an array";
|
|
54
|
+
if (typeof value === "object") return "an object";
|
|
55
|
+
return typeof value === "undefined" ? "undefined" : `a ${typeof value}`;
|
|
56
|
+
}
|
|
57
|
+
const ignore = () => {};
|
|
58
|
+
/**
|
|
59
|
+
* Attach a no-op rejection handler with the intrinsic `Promise.prototype.then`,
|
|
60
|
+
* which throws on a non-Promise before reading anything from it. Returns the
|
|
61
|
+
* derived promise, which a Promise subclass's species constructor chooses.
|
|
62
|
+
*/
|
|
63
|
+
function handleRejection(promise) {
|
|
64
|
+
return Promise.prototype.then.call(promise, void 0, ignore);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether `value` is a Promise, judged by its internal slot or this realm's
|
|
68
|
+
* Promise prototype chain, never by a `then` member: adapter contracts are
|
|
69
|
+
* structural and do not reserve `then`, so an adapter's own `then` method is
|
|
70
|
+
* never read or invoked.
|
|
71
|
+
*
|
|
72
|
+
* A Promise from any realm passes the intrinsic `then`'s brand check, which
|
|
73
|
+
* also attaches a no-op rejection handler, so the discarded result cannot
|
|
74
|
+
* become an unhandled rejection. The derived promise that call returns is
|
|
75
|
+
* handled too, since a subclass's species constructor may return a rejected
|
|
76
|
+
* one.
|
|
77
|
+
*
|
|
78
|
+
* Known limitation: when a Promise subclass's `constructor` or
|
|
79
|
+
* `Symbol.species` lookup throws, that call fails after the brand check, and
|
|
80
|
+
* no handler can be attached (every way of observing a Promise goes through
|
|
81
|
+
* that lookup or its `then`). Such a Promise is still rejected when it comes
|
|
82
|
+
* from this realm, the one adapter modules are evaluated in; one from another
|
|
83
|
+
* realm is judged by its members like any other value.
|
|
84
|
+
*/
|
|
85
|
+
function markHandledIfPromise(value) {
|
|
86
|
+
let derived;
|
|
87
|
+
try {
|
|
88
|
+
derived = handleRejection(value);
|
|
89
|
+
} catch {
|
|
90
|
+
try {
|
|
91
|
+
return value instanceof Promise;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
handleRejection(derived);
|
|
98
|
+
} catch {}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function instantiateCacheAdapter(exported, args, slot) {
|
|
102
|
+
const label = `cache.${slot} adapter`;
|
|
103
|
+
const requirements = REQUIREMENTS[slot];
|
|
104
|
+
if (typeof exported !== "function") {
|
|
105
|
+
const hint = exported !== null && typeof exported === "object" ? " To use an adapter object directly, export a factory that returns it: `export default () => adapter`." : exported === void 0 ? " Check that the module has a default export." : "";
|
|
106
|
+
throw new TypeError(`${label}: the module's default export must be a factory function or a class that receives { env, options }, got ${describeValue(exported)}.${hint}`);
|
|
107
|
+
}
|
|
108
|
+
const adapter = isClassExport(exported) ? new exported(args) : exported(args);
|
|
109
|
+
const isObjectLike = adapter !== null && typeof adapter === "object" || typeof adapter === "function";
|
|
110
|
+
if (isObjectLike && markHandledIfPromise(adapter)) throw new TypeError(`${label}: the default export returned a Promise. Adapter factories must return the adapter synchronously; defer async setup to the adapter's methods.`);
|
|
111
|
+
if (!isObjectLike) throw new TypeError(`${label}: the default export must produce an adapter object, got ${describeValue(adapter)}. ${requirements.description}`);
|
|
112
|
+
const record = adapter;
|
|
113
|
+
const problems = [...requirements.methods.filter((method) => typeof record[method] !== "function").map((method) => `method ${method}`), ...requirements.booleans.filter((property) => typeof record[property] !== "boolean").map((property) => `boolean ${property}`)];
|
|
114
|
+
if (problems.length > 0) throw new TypeError(`${label}: the adapter produced by the default export is missing ${problems.join(", ")}. ${requirements.description}`);
|
|
115
|
+
return adapter;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { instantiateCacheAdapter, isClassExport, isConstructor };
|