vinext 0.1.1 → 0.1.2

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.
Files changed (147) hide show
  1. package/README.md +2 -5
  2. package/dist/build/client-build-config.d.ts +7 -1
  3. package/dist/build/client-build-config.js +9 -1
  4. package/dist/check.js +4 -3
  5. package/dist/client/navigation-runtime.d.ts +3 -2
  6. package/dist/client/window-next.d.ts +6 -4
  7. package/dist/config/config-matchers.d.ts +11 -4
  8. package/dist/config/config-matchers.js +15 -2
  9. package/dist/config/next-config.d.ts +13 -0
  10. package/dist/config/next-config.js +2 -0
  11. package/dist/deploy.js +9 -2
  12. package/dist/entries/app-rsc-entry.js +7 -1
  13. package/dist/entries/pages-client-entry.js +1 -1
  14. package/dist/entries/pages-server-entry.js +7 -6
  15. package/dist/index.d.ts +0 -2
  16. package/dist/index.js +86 -78
  17. package/dist/plugins/dynamic-preload-metadata.d.ts +13 -0
  18. package/dist/plugins/dynamic-preload-metadata.js +415 -0
  19. package/dist/plugins/og-assets.js +2 -2
  20. package/dist/plugins/optimize-imports.d.ts +8 -4
  21. package/dist/plugins/optimize-imports.js +16 -12
  22. package/dist/plugins/sass.d.ts +53 -24
  23. package/dist/plugins/sass.js +249 -1
  24. package/dist/plugins/wasm-module-import.d.ts +15 -0
  25. package/dist/plugins/wasm-module-import.js +50 -0
  26. package/dist/routing/app-route-graph.d.ts +23 -1
  27. package/dist/routing/app-route-graph.js +47 -8
  28. package/dist/routing/file-matcher.js +1 -1
  29. package/dist/server/app-browser-entry.js +108 -213
  30. package/dist/server/app-browser-error.d.ts +4 -1
  31. package/dist/server/app-browser-error.js +7 -1
  32. package/dist/server/app-browser-history-controller.d.ts +104 -0
  33. package/dist/server/app-browser-history-controller.js +210 -0
  34. package/dist/server/app-browser-navigation-controller.d.ts +3 -2
  35. package/dist/server/app-browser-navigation-controller.js +10 -7
  36. package/dist/server/app-browser-rsc-redirect.d.ts +11 -2
  37. package/dist/server/app-browser-rsc-redirect.js +30 -8
  38. package/dist/server/app-browser-state.js +4 -7
  39. package/dist/server/app-browser-visible-commit.js +1 -1
  40. package/dist/server/app-fallback-renderer.d.ts +2 -1
  41. package/dist/server/app-fallback-renderer.js +3 -1
  42. package/dist/server/app-middleware.js +1 -0
  43. package/dist/server/app-optimistic-routing.js +22 -1
  44. package/dist/server/app-page-boundary-render.d.ts +2 -1
  45. package/dist/server/app-page-boundary-render.js +4 -2
  46. package/dist/server/app-page-cache.js +9 -7
  47. package/dist/server/app-page-dispatch.d.ts +8 -0
  48. package/dist/server/app-page-dispatch.js +18 -5
  49. package/dist/server/app-page-element-builder.d.ts +22 -2
  50. package/dist/server/app-page-element-builder.js +37 -8
  51. package/dist/server/app-page-execution.d.ts +1 -1
  52. package/dist/server/app-page-execution.js +32 -17
  53. package/dist/server/app-page-render.d.ts +1 -1
  54. package/dist/server/app-page-render.js +7 -14
  55. package/dist/server/app-page-request.d.ts +1 -0
  56. package/dist/server/app-page-request.js +3 -2
  57. package/dist/server/app-page-response.js +1 -1
  58. package/dist/server/app-page-route-wiring.d.ts +3 -1
  59. package/dist/server/app-page-route-wiring.js +8 -7
  60. package/dist/server/app-page-stream.d.ts +1 -6
  61. package/dist/server/app-page-stream.js +1 -4
  62. package/dist/server/app-route-handler-response.js +11 -10
  63. package/dist/server/app-route-handler-runtime.js +12 -1
  64. package/dist/server/app-rsc-handler.js +1 -1
  65. package/dist/server/app-rsc-response-finalizer.js +1 -1
  66. package/dist/server/app-server-action-execution.d.ts +11 -0
  67. package/dist/server/app-server-action-execution.js +5 -2
  68. package/dist/server/app-ssr-entry.js +2 -2
  69. package/dist/server/app-ssr-stream.js +9 -1
  70. package/dist/server/dev-lockfile.js +2 -1
  71. package/dist/server/dev-server.js +43 -12
  72. package/dist/server/headers.d.ts +8 -1
  73. package/dist/server/headers.js +8 -1
  74. package/dist/server/instrumentation-runtime.d.ts +6 -0
  75. package/dist/server/instrumentation-runtime.js +8 -0
  76. package/dist/server/isr-decision.d.ts +79 -0
  77. package/dist/server/isr-decision.js +70 -0
  78. package/dist/server/metadata-route-response.js +5 -3
  79. package/dist/server/middleware-runtime.d.ts +13 -0
  80. package/dist/server/middleware-runtime.js +11 -7
  81. package/dist/server/middleware.js +1 -0
  82. package/dist/server/navigation-planner.d.ts +62 -1
  83. package/dist/server/navigation-planner.js +188 -0
  84. package/dist/server/navigation-trace.d.ts +11 -1
  85. package/dist/server/navigation-trace.js +11 -1
  86. package/dist/server/normalize-path.d.ts +0 -8
  87. package/dist/server/normalize-path.js +3 -1
  88. package/dist/server/otel-tracer-extension.d.ts +45 -0
  89. package/dist/server/otel-tracer-extension.js +89 -0
  90. package/dist/server/pages-api-route.d.ts +14 -3
  91. package/dist/server/pages-api-route.js +6 -1
  92. package/dist/server/pages-asset-tags.d.ts +15 -4
  93. package/dist/server/pages-asset-tags.js +18 -12
  94. package/dist/server/pages-data-route.js +5 -1
  95. package/dist/server/pages-node-compat.d.ts +3 -11
  96. package/dist/server/pages-node-compat.js +174 -121
  97. package/dist/server/pages-page-data.d.ts +28 -0
  98. package/dist/server/pages-page-data.js +61 -17
  99. package/dist/server/pages-page-handler.d.ts +1 -0
  100. package/dist/server/pages-page-handler.js +22 -6
  101. package/dist/server/pages-page-response.d.ts +45 -1
  102. package/dist/server/pages-page-response.js +66 -5
  103. package/dist/server/pages-readiness.d.ts +1 -1
  104. package/dist/server/pages-request-pipeline.d.ts +15 -1
  105. package/dist/server/pages-request-pipeline.js +23 -2
  106. package/dist/server/prod-server.d.ts +39 -1
  107. package/dist/server/prod-server.js +98 -34
  108. package/dist/shims/cache-runtime.js +9 -2
  109. package/dist/shims/dynamic-preload-chunks.d.ts +8 -0
  110. package/dist/shims/dynamic-preload-chunks.js +77 -0
  111. package/dist/shims/dynamic.d.ts +4 -0
  112. package/dist/shims/dynamic.js +4 -2
  113. package/dist/shims/error-boundary.d.ts +4 -4
  114. package/dist/shims/error.js +37 -11
  115. package/dist/shims/fetch-cache.d.ts +9 -1
  116. package/dist/shims/fetch-cache.js +11 -1
  117. package/dist/shims/head.js +6 -1
  118. package/dist/shims/headers.d.ts +16 -2
  119. package/dist/shims/headers.js +37 -1
  120. package/dist/shims/image-config.js +7 -1
  121. package/dist/shims/internal/app-route-detection.d.ts +6 -3
  122. package/dist/shims/internal/app-route-detection.js +10 -6
  123. package/dist/shims/internal/app-router-context.d.ts +5 -0
  124. package/dist/shims/metadata.d.ts +6 -2
  125. package/dist/shims/metadata.js +32 -14
  126. package/dist/shims/navigation.d.ts +7 -16
  127. package/dist/shims/navigation.js +33 -16
  128. package/dist/shims/router.js +28 -1
  129. package/dist/shims/script-nonce-context.d.ts +1 -1
  130. package/dist/shims/script-nonce-context.js +11 -3
  131. package/dist/shims/server.d.ts +17 -1
  132. package/dist/shims/server.js +31 -6
  133. package/dist/shims/slot.js +1 -1
  134. package/dist/shims/unified-request-context.js +1 -0
  135. package/dist/typegen.js +1 -0
  136. package/dist/utils/client-build-manifest.js +15 -5
  137. package/dist/utils/client-runtime-metadata.d.ts +45 -0
  138. package/dist/utils/client-runtime-metadata.js +63 -0
  139. package/dist/utils/hash.d.ts +17 -1
  140. package/dist/utils/hash.js +36 -1
  141. package/dist/utils/lazy-chunks.d.ts +27 -1
  142. package/dist/utils/lazy-chunks.js +65 -1
  143. package/dist/utils/manifest-paths.d.ts +20 -2
  144. package/dist/utils/manifest-paths.js +38 -3
  145. package/dist/utils/path.d.ts +2 -1
  146. package/dist/utils/path.js +5 -1
  147. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { createNonceAttribute } from "./html.js";
2
+ import { assetServingUrlFromBaseAnchored } from "../utils/manifest-paths.js";
2
3
  //#region src/server/pages-asset-tags.ts
3
4
  /**
4
5
  * Pages Router SSR asset-tag helpers.
@@ -33,18 +34,20 @@ function getManifestFilesForModule(manifest, moduleId) {
33
34
  return null;
34
35
  }
35
36
  /**
36
- * Find the first `.js` file in the manifest for `moduleId` and return its
37
- * URL-path form (with a leading `/`). Used to resolve the hydration URL for
38
- * the matched page or the `_app` module.
37
+ * Find the first `.js` file in the manifest for `moduleId` and return the URL it
38
+ * is actually SERVED from. Used to resolve the client-navigation / hydration URL
39
+ * for the matched page or the `_app` module (it is `import()`ed on the client),
40
+ * so it must point at the served location: `assetPrefix` replaces `basePath` for
41
+ * asset URLs. SSR-manifest values are base-anchored; re-anchor under any
42
+ * configured `assetPrefix` (default `""` keeps the legacy `"/" + file`).
39
43
  */
40
- function resolveClientModuleUrl(manifest, moduleId) {
44
+ function resolveClientModuleUrl(manifest, moduleId, basePath = "", assetPrefix = "") {
41
45
  const files = getManifestFilesForModule(resolveSsrManifest(manifest), moduleId);
42
46
  if (!files) return void 0;
43
47
  for (let i = 0; i < files.length; i++) {
44
- let file = files[i];
48
+ const file = files[i];
45
49
  if (!file || !file.endsWith(".js")) continue;
46
- if (file.charAt(0) !== "/") file = "/" + file;
47
- return file;
50
+ return assetServingUrlFromBaseAnchored(file, basePath, assetPrefix);
48
51
  }
49
52
  }
50
53
  /**
@@ -67,13 +70,16 @@ function collectAssetTags(options) {
67
70
  const seen = /* @__PURE__ */ new Set();
68
71
  const nonceAttr = createNonceAttribute(options.scriptNonce);
69
72
  const deferAttr = options.disableOptimizedLoading ? "" : " defer";
73
+ const basePath = options.basePath ?? "";
74
+ const assetPrefix = options.assetPrefix ?? "";
75
+ const href = (value) => assetServingUrlFromBaseAnchored(value, basePath, assetPrefix);
70
76
  const lazyChunks = typeof globalThis !== "undefined" && globalThis.__VINEXT_LAZY_CHUNKS__ || null;
71
77
  const lazySet = lazyChunks && lazyChunks.length > 0 ? new Set(lazyChunks) : null;
72
78
  if (typeof globalThis !== "undefined" && globalThis.__VINEXT_CLIENT_ENTRY__) {
73
79
  const entry = globalThis.__VINEXT_CLIENT_ENTRY__;
74
80
  seen.add(entry);
75
- tags.push("<link rel=\"modulepreload\"" + nonceAttr + " href=\"/" + entry + "\" />");
76
- tags.push("<script type=\"module\"" + deferAttr + nonceAttr + " src=\"/" + entry + "\" crossorigin><\/script>");
81
+ tags.push("<link rel=\"modulepreload\"" + nonceAttr + " href=\"" + href(entry) + "\" />");
82
+ tags.push("<script type=\"module\"" + deferAttr + nonceAttr + " src=\"" + href(entry) + "\" crossorigin><\/script>");
77
83
  }
78
84
  if (m) {
79
85
  const allFiles = [];
@@ -102,11 +108,11 @@ function collectAssetTags(options) {
102
108
  if (tf.charAt(0) === "/") tf = tf.slice(1);
103
109
  if (seen.has(tf)) continue;
104
110
  seen.add(tf);
105
- if (tf.endsWith(".css")) tags.push("<link rel=\"stylesheet\"" + nonceAttr + " href=\"/" + tf + "\" />");
111
+ if (tf.endsWith(".css")) tags.push("<link rel=\"stylesheet\"" + nonceAttr + " href=\"" + href(tf) + "\" />");
106
112
  else if (tf.endsWith(".js")) {
107
113
  if (lazySet && lazySet.has(tf)) continue;
108
- tags.push("<link rel=\"modulepreload\"" + nonceAttr + " href=\"/" + tf + "\" />");
109
- tags.push("<script type=\"module\"" + deferAttr + nonceAttr + " src=\"/" + tf + "\" crossorigin><\/script>");
114
+ tags.push("<link rel=\"modulepreload\"" + nonceAttr + " href=\"" + href(tf) + "\" />");
115
+ tags.push("<script type=\"module\"" + deferAttr + nonceAttr + " src=\"" + href(tf) + "\" crossorigin><\/script>");
110
116
  }
111
117
  }
112
118
  }
@@ -1,3 +1,4 @@
1
+ import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
1
2
  //#region src/server/pages-data-route.ts
2
3
  /**
3
4
  * Helpers for the Pages Router `/_next/data/{buildId}/{...page}.json` endpoint.
@@ -87,9 +88,12 @@ function buildNextDataJsonResponse(pageProps, safeJsonStringify, init) {
87
88
  * before checking the status code.
88
89
  */
89
90
  function buildNextDataNotFoundResponse() {
91
+ const headers = { "Content-Type": "application/json" };
92
+ const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
93
+ if (deploymentId) headers[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
90
94
  return new Response("{}", {
91
95
  status: 404,
92
- headers: { "Content-Type": "application/json" }
96
+ headers
93
97
  });
94
98
  }
95
99
  /**
@@ -1,34 +1,26 @@
1
1
  import { PagesBodyParseError } from "./pages-media-type.js";
2
2
  import { RevalidateOptions } from "./pages-revalidate.js";
3
+ import { Readable, Writable } from "node:stream";
3
4
 
4
5
  //#region src/server/pages-node-compat.d.ts
5
6
  type PagesRequestQuery = Record<string, string | string[]>;
6
- type PagesReqResRequest = {
7
+ type PagesReqResRequest = Readable & {
7
8
  method: string;
8
9
  url: string;
9
10
  headers: Record<string, string>;
10
11
  query: PagesRequestQuery;
11
12
  body: unknown;
12
13
  cookies: Record<string, string>;
13
- /**
14
- * Async-iterator hook so handlers can `for await (const chunk of req)` —
15
- * matching Node's `IncomingMessage` contract. Critical for the
16
- * `bodyParser: false` opt-out (webhook signature verification etc.) where
17
- * `req.body` is left undefined and user code is expected to drain the raw
18
- * stream off `req` itself.
19
- */
20
- [Symbol.asyncIterator]: () => AsyncIterator<Uint8Array>;
21
14
  };
22
15
  type PagesReqResHeaders = {
23
16
  [key: string]: string | number | boolean | string[];
24
17
  };
25
- type PagesReqResResponse = {
18
+ type PagesReqResResponse = Writable & {
26
19
  statusCode: number;
27
20
  readonly headersSent: boolean;
28
21
  writeHead: (code: number, headers?: PagesReqResHeaders) => PagesReqResResponse;
29
22
  setHeader: (name: string, value: string | number | boolean | string[]) => PagesReqResResponse;
30
23
  getHeader: (name: string) => string | number | boolean | string[] | undefined;
31
- end: (data?: BodyInit | null) => void;
32
24
  status: (code: number) => PagesReqResResponse;
33
25
  json: (data: unknown) => void;
34
26
  send: (data: unknown) => void;
@@ -4,6 +4,7 @@ import { PagesBodyParseError, getMediaType, isJsonMediaType } from "./pages-medi
4
4
  import { DEFAULT_PAGES_API_BODY_SIZE_LIMIT } from "./pages-body-parser-config.js";
5
5
  import { performOnDemandRevalidate } from "./pages-revalidate.js";
6
6
  import { decode } from "node:querystring";
7
+ import { Readable, Writable } from "node:stream";
7
8
  //#region src/server/pages-node-compat.ts
8
9
  const MAX_PAGES_API_BODY_SIZE = DEFAULT_PAGES_API_BODY_SIZE_LIMIT;
9
10
  async function readPagesRequestBodyWithLimit(request, maxBytes) {
@@ -41,140 +42,192 @@ async function parsePagesApiBody(request, maxBytes = MAX_PAGES_API_BODY_SIZE) {
41
42
  if (mediaType === "application/x-www-form-urlencoded") return decode(rawBody);
42
43
  return rawBody;
43
44
  }
45
+ async function* requestBodyChunks(request) {
46
+ if (!request.body || request.bodyUsed) return;
47
+ const reader = request.body.getReader();
48
+ try {
49
+ for (;;) {
50
+ const { value, done } = await reader.read();
51
+ if (done) return;
52
+ yield Buffer.from(value.buffer, value.byteOffset, value.byteLength);
53
+ }
54
+ } finally {
55
+ try {
56
+ await reader.cancel();
57
+ } catch {}
58
+ reader.releaseLock();
59
+ }
60
+ }
61
+ function createRequestReadable(request) {
62
+ return Readable.from(requestBodyChunks(request), { objectMode: false });
63
+ }
64
+ var PagesResponseStream = class extends Writable {
65
+ resolveResponse;
66
+ rejectResponse;
67
+ requestHeaders;
68
+ resStatusCode = 200;
69
+ resHeaders = {};
70
+ setCookieHeaders = [];
71
+ resolved = false;
72
+ controller = null;
73
+ bufferedChunks = [];
74
+ streamEnded = false;
75
+ constructor(resolveResponse, rejectResponse, requestHeaders) {
76
+ super();
77
+ this.resolveResponse = resolveResponse;
78
+ this.rejectResponse = rejectResponse;
79
+ this.requestHeaders = requestHeaders;
80
+ this.once("error", (err) => {
81
+ if (!this.resolved) {
82
+ this.resolved = true;
83
+ this.rejectResponse(err);
84
+ }
85
+ });
86
+ }
87
+ get statusCode() {
88
+ return this.resStatusCode;
89
+ }
90
+ set statusCode(code) {
91
+ this.resStatusCode = code;
92
+ }
93
+ get headersSent() {
94
+ return this.writableEnded || this.resolved;
95
+ }
96
+ writeHead(code, headers) {
97
+ this.resStatusCode = code;
98
+ if (headers) for (const [key, value] of Object.entries(headers)) this.setHeaderValue(key, value, { replaceSetCookie: false });
99
+ return this;
100
+ }
101
+ setHeader(name, value) {
102
+ this.setHeaderValue(name, value, { replaceSetCookie: true });
103
+ return this;
104
+ }
105
+ getHeader(name) {
106
+ if (name.toLowerCase() === "set-cookie") return this.setCookieHeaders.length > 0 ? this.setCookieHeaders : void 0;
107
+ return this.resHeaders[name.toLowerCase()];
108
+ }
109
+ status(code) {
110
+ this.resStatusCode = code;
111
+ return this;
112
+ }
113
+ json(data) {
114
+ this.resHeaders["content-type"] = "application/json";
115
+ this.end(JSON.stringify(data));
116
+ }
117
+ send(data) {
118
+ if (Buffer.isBuffer(data)) {
119
+ if (!this.resHeaders["content-type"]) this.resHeaders["content-type"] = "application/octet-stream";
120
+ this.resHeaders["content-length"] = String(data.length);
121
+ this.end(data);
122
+ return;
123
+ }
124
+ if (typeof data === "object" && data !== null) {
125
+ this.resHeaders["content-type"] = "application/json";
126
+ this.end(JSON.stringify(data));
127
+ return;
128
+ }
129
+ if (!this.resHeaders["content-type"]) this.resHeaders["content-type"] = "text/plain";
130
+ this.end(String(data));
131
+ }
132
+ redirect(statusOrUrl, url) {
133
+ if (typeof statusOrUrl === "string") this.writeHead(307, { Location: statusOrUrl });
134
+ else this.writeHead(statusOrUrl, { Location: url ?? "" });
135
+ this.end();
136
+ }
137
+ getHeaders() {
138
+ const headers = { ...this.resHeaders };
139
+ if (this.setCookieHeaders.length > 0) headers["set-cookie"] = this.setCookieHeaders;
140
+ return headers;
141
+ }
142
+ async revalidate(urlPath, opts) {
143
+ await performOnDemandRevalidate(this.requestHeaders, urlPath, opts);
144
+ }
145
+ _write(chunk, encoding, callback) {
146
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk, encoding) : Buffer.from(chunk);
147
+ if (this.controller && !this.streamEnded) try {
148
+ this.controller.enqueue(buffer);
149
+ } catch {}
150
+ else this.bufferedChunks.push(buffer);
151
+ this.resolveOnce();
152
+ callback();
153
+ }
154
+ _final(callback) {
155
+ this.streamEnded = true;
156
+ if (this.controller) try {
157
+ this.controller.close();
158
+ } catch {}
159
+ this.resolveOnce();
160
+ callback();
161
+ }
162
+ _destroy(error, callback) {
163
+ this.streamEnded = true;
164
+ if (!this.resolved) if (error) {
165
+ this.resolved = true;
166
+ this.rejectResponse(error);
167
+ } else this.resolveOnce();
168
+ if (this.controller) try {
169
+ if (error) this.controller.error(error);
170
+ else this.controller.close();
171
+ } catch {}
172
+ callback(error);
173
+ }
174
+ setHeaderValue(name, value, options) {
175
+ if (name.toLowerCase() === "set-cookie") {
176
+ if (options.replaceSetCookie) this.setCookieHeaders.length = 0;
177
+ if (Array.isArray(value)) this.setCookieHeaders.push(...value.map(String));
178
+ else this.setCookieHeaders.push(String(value));
179
+ return;
180
+ }
181
+ this.resHeaders[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
182
+ }
183
+ resolveOnce() {
184
+ if (this.resolved) return;
185
+ this.resolved = true;
186
+ const headers = new Headers();
187
+ for (const [key, value] of Object.entries(this.resHeaders)) headers.set(key, String(value));
188
+ for (const cookie of this.setCookieHeaders) headers.append("set-cookie", cookie);
189
+ const stream = new ReadableStream({
190
+ start: (controller) => {
191
+ this.controller = controller;
192
+ for (const buffer of this.bufferedChunks) try {
193
+ controller.enqueue(buffer);
194
+ } catch {}
195
+ this.bufferedChunks.length = 0;
196
+ if (this.streamEnded) try {
197
+ controller.close();
198
+ } catch {}
199
+ },
200
+ cancel: (reason) => {
201
+ this.bufferedChunks.length = 0;
202
+ this.destroy(reason instanceof Error ? reason : /* @__PURE__ */ new Error("Response body cancelled"));
203
+ }
204
+ });
205
+ this.resolveResponse(new Response(stream, {
206
+ status: this.resStatusCode,
207
+ headers
208
+ }));
209
+ }
210
+ };
44
211
  function createPagesReqRes(options) {
45
212
  const headersObj = {};
46
213
  for (const [key, value] of options.request.headers) headersObj[key.toLowerCase()] = value;
47
- const requestBody = options.request.body;
48
- const reqAsyncIterator = () => {
49
- if (!requestBody) return { next() {
50
- return Promise.resolve({
51
- value: void 0,
52
- done: true
53
- });
54
- } };
55
- const reader = requestBody.getReader();
56
- return {
57
- async next() {
58
- const { value, done } = await reader.read();
59
- if (done) return {
60
- value: void 0,
61
- done: true
62
- };
63
- return {
64
- value,
65
- done: false
66
- };
67
- },
68
- async return() {
69
- try {
70
- await reader.cancel();
71
- } catch {}
72
- return {
73
- value: void 0,
74
- done: true
75
- };
76
- }
77
- };
78
- };
79
- const req = {
214
+ const req = Object.assign(createRequestReadable(options.request), {
80
215
  method: options.request.method,
81
216
  url: options.url,
82
217
  headers: headersObj,
83
218
  query: options.query,
84
219
  body: options.body,
85
- cookies: parseCookies(options.request.headers.get("cookie")),
86
- [Symbol.asyncIterator]: reqAsyncIterator
87
- };
88
- let resStatusCode = 200;
89
- const resHeaders = {};
90
- const setCookieHeaders = [];
91
- let resBody = null;
92
- let ended = false;
220
+ cookies: parseCookies(options.request.headers.get("cookie"))
221
+ });
93
222
  let resolveResponse;
94
- const responsePromise = new Promise((resolve) => {
223
+ let rejectResponse;
224
+ const responsePromise = new Promise((resolve, reject) => {
95
225
  resolveResponse = resolve;
226
+ rejectResponse = reject;
96
227
  });
97
- const res = {
98
- get statusCode() {
99
- return resStatusCode;
100
- },
101
- set statusCode(code) {
102
- resStatusCode = code;
103
- },
104
- get headersSent() {
105
- return ended;
106
- },
107
- writeHead(code, headers) {
108
- resStatusCode = code;
109
- if (headers) for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === "set-cookie") if (Array.isArray(value)) setCookieHeaders.push(...value.map(String));
110
- else setCookieHeaders.push(String(value));
111
- else resHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
112
- return res;
113
- },
114
- setHeader(name, value) {
115
- if (name.toLowerCase() === "set-cookie") {
116
- setCookieHeaders.length = 0;
117
- if (Array.isArray(value)) setCookieHeaders.push(...value.map(String));
118
- else setCookieHeaders.push(String(value));
119
- } else resHeaders[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
120
- return res;
121
- },
122
- getHeader(name) {
123
- if (name.toLowerCase() === "set-cookie") return setCookieHeaders.length > 0 ? setCookieHeaders : void 0;
124
- return resHeaders[name.toLowerCase()];
125
- },
126
- end(data) {
127
- if (ended) return;
128
- ended = true;
129
- if (data !== void 0 && data !== null) resBody = data;
130
- const headers = new Headers();
131
- for (const [key, value] of Object.entries(resHeaders)) headers.set(key, String(value));
132
- for (const cookie of setCookieHeaders) headers.append("set-cookie", cookie);
133
- resolveResponse(new Response(resBody, {
134
- status: resStatusCode,
135
- headers
136
- }));
137
- },
138
- status(code) {
139
- resStatusCode = code;
140
- return res;
141
- },
142
- json(data) {
143
- resHeaders["content-type"] = "application/json";
144
- res.end(JSON.stringify(data));
145
- },
146
- send(data) {
147
- if (Buffer.isBuffer(data)) {
148
- if (!resHeaders["content-type"]) resHeaders["content-type"] = "application/octet-stream";
149
- resHeaders["content-length"] = String(data.length);
150
- res.end(new Uint8Array(data));
151
- return;
152
- }
153
- if (typeof data === "object" && data !== null) {
154
- resHeaders["content-type"] = "application/json";
155
- res.end(JSON.stringify(data));
156
- return;
157
- }
158
- if (!resHeaders["content-type"]) resHeaders["content-type"] = "text/plain";
159
- res.end(String(data));
160
- },
161
- redirect(statusOrUrl, url) {
162
- if (typeof statusOrUrl === "string") res.writeHead(307, { Location: statusOrUrl });
163
- else res.writeHead(statusOrUrl, { Location: url ?? "" });
164
- res.end();
165
- },
166
- getHeaders() {
167
- const headers = { ...resHeaders };
168
- if (setCookieHeaders.length > 0) headers["set-cookie"] = setCookieHeaders;
169
- return headers;
170
- },
171
- async revalidate(urlPath, opts) {
172
- await performOnDemandRevalidate(options.request.headers, urlPath, opts);
173
- }
174
- };
175
228
  return {
176
229
  req,
177
- res,
230
+ res: new PagesResponseStream(resolveResponse, rejectResponse, options.request.headers),
178
231
  responsePromise
179
232
  };
180
233
  }
@@ -138,6 +138,13 @@ type ResolvePagesPageDataOptions = {
138
138
  * presence — see the security note in `isr-cache.ts`.
139
139
  */
140
140
  isOnDemandRevalidate?: boolean;
141
+ /**
142
+ * The deployment ID used for deployment-skew protection. When set, it is
143
+ * included as `x-nextjs-deployment-id` on all `_next/data` responses
144
+ * (success, redirect, notFound). Mirrors Next.js pages-handler.ts behavior.
145
+ * Typically sourced from `process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID`.
146
+ */
147
+ deploymentId?: string;
141
148
  pageModule: PagesPageModule;
142
149
  params: Record<string, unknown>;
143
150
  query: Record<string, unknown>;
@@ -158,6 +165,27 @@ type ResolvePagesPageDataOptions = {
158
165
  renderIsrPassToStringAsync: (element: ReactNode) => Promise<string>;
159
166
  vinext?: VinextNextData["__vinext"];
160
167
  nextData?: PagesNextDataExtras;
168
+ /**
169
+ * The request's User-Agent string. When this matches a known crawler/bot
170
+ * pattern, ISR cache-HIT and cache-STALE responses receive an ETag header
171
+ * for consistency with the fresh-MISS path (which also attaches an ETag for
172
+ * bot UAs via `renderPagesPageResponse`). See the divergence note in
173
+ * `pages-page-response.ts` for why UA-gating is used instead of Next.js's
174
+ * `isDynamic` check.
175
+ */
176
+ userAgent?: string;
177
+ /**
178
+ * The incoming request's `If-None-Match` header value. When the cached HTML
179
+ * ETag matches (weak-ETag semantics), the ISR cache-HIT or cache-STALE
180
+ * response is a `304 Not Modified` with no body.
181
+ */
182
+ ifNoneMatch?: string;
183
+ /**
184
+ * The incoming request's `Cache-Control` header value. When it contains
185
+ * `no-cache`, the 304 short-circuit is skipped and a full response is
186
+ * returned — mirroring the `fresh` package used by Next.js.
187
+ */
188
+ requestCacheControl?: string;
161
189
  };
162
190
  type ResolvePagesPageDataRenderResult = {
163
191
  kind: "render";
@@ -1,22 +1,26 @@
1
+ import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
1
2
  import { normalizeStaticPathname } from "../routing/route-pattern.js";
2
3
  import { buildCacheStateHeaders } from "./cache-headers.js";
4
+ import { applyCdnResponseHeaders } from "./cache-control.js";
5
+ import { decideIsr } from "./isr-decision.js";
3
6
  import { buildPagesCacheValue } from "./isr-cache.js";
4
7
  import { isSerializableProps } from "./pages-serializable-props.js";
5
8
  import { hasPagesGetInitialProps, isResponseSent, loadPagesGetInitialProps } from "./pages-get-initial-props.js";
6
- import { applyCdnResponseHeaders, buildCachedRevalidateCacheControl } from "./cache-control.js";
7
9
  import { buildNextDataJsonResponse } from "./pages-data-route.js";
8
- import { buildPagesNextDataScript } from "./pages-page-response.js";
10
+ import { buildPagesNextDataScript, etagMatches, generatePagesETag, isPagesStreamingBot, requestsNoCache } from "./pages-page-response.js";
9
11
  //#region src/server/pages-page-data.ts
10
- function buildPagesDataNotFoundResponse() {
12
+ function buildPagesDataNotFoundResponse(deploymentId) {
13
+ const headers = { "Content-Type": "application/json" };
14
+ if (deploymentId) headers[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
11
15
  return new Response("{}", {
12
16
  status: 404,
13
- headers: { "Content-Type": "application/json" }
17
+ headers
14
18
  });
15
19
  }
16
20
  function buildPagesNotFoundResult(options) {
17
21
  if (options.isDataReq) return {
18
22
  kind: "response",
19
- response: buildPagesDataNotFoundResponse()
23
+ response: buildPagesDataNotFoundResponse(options.deploymentId)
20
24
  };
21
25
  return { kind: "notFound" };
22
26
  }
@@ -46,10 +50,14 @@ function resolvePagesRedirectStatus(redirect) {
46
50
  */
47
51
  function buildPagesRedirectResponse(redirect, options) {
48
52
  const destination = options.sanitizeDestination(redirect.destination);
49
- if (options.isDataReq) return buildNextDataJsonResponse({
50
- __N_REDIRECT: destination,
51
- __N_REDIRECT_STATUS: resolvePagesRedirectStatus(redirect)
52
- }, options.safeJsonStringify);
53
+ if (options.isDataReq) {
54
+ const init = { headers: {} };
55
+ if (options.deploymentId) init.headers[NEXTJS_DEPLOYMENT_ID_HEADER] = options.deploymentId;
56
+ return buildNextDataJsonResponse({
57
+ __N_REDIRECT: destination,
58
+ __N_REDIRECT_STATUS: resolvePagesRedirectStatus(redirect)
59
+ }, options.safeJsonStringify, init);
60
+ }
53
61
  return new Response(null, {
54
62
  status: resolvePagesRedirectStatus(redirect),
55
63
  headers: { Location: destination }
@@ -81,19 +89,47 @@ function matchesPagesStaticPath(pathEntry, params, routeUrl) {
81
89
  });
82
90
  }
83
91
  function buildPagesCacheResponse(html, cacheState, fontLinkHeader, revalidateSeconds, expireSeconds, cacheControl, status) {
84
- const effectiveRevalidateSeconds = cacheControl?.revalidate ?? revalidateSeconds ?? 60;
85
- const effectiveExpireSeconds = cacheControl === void 0 ? void 0 : cacheControl.expire ?? expireSeconds;
92
+ const { cacheControl: cacheControlHeader } = decideIsr({
93
+ cacheState,
94
+ kind: "pages",
95
+ revalidateSeconds: revalidateSeconds ?? 60,
96
+ expireSeconds,
97
+ cacheControlMeta: cacheControl
98
+ });
86
99
  const headers = new Headers({
87
100
  "Content-Type": "text/html",
88
101
  ...buildCacheStateHeaders(cacheState)
89
102
  });
90
- applyCdnResponseHeaders(headers, { cacheControl: buildCachedRevalidateCacheControl(cacheState, effectiveRevalidateSeconds, effectiveExpireSeconds) });
103
+ applyCdnResponseHeaders(headers, { cacheControl: cacheControlHeader });
91
104
  if (fontLinkHeader) headers.set("Link", fontLinkHeader);
92
105
  return new Response(html, {
93
106
  status: status ?? 200,
94
107
  headers
95
108
  });
96
109
  }
110
+ /**
111
+ * For bot / crawler UAs, attach an ETag to a cached ISR response (HIT or
112
+ * STALE) so it is consistent with the fresh-MISS path, then check for a
113
+ * matching `If-None-Match`. When the check passes — and the request did NOT
114
+ * carry `Cache-Control: no-cache` — returns a 304 response; otherwise returns
115
+ * `null` so the caller can return the full response.
116
+ *
117
+ * Extracted to avoid duplicating the same three-line block across the HIT and
118
+ * STALE branches.
119
+ */
120
+ function applyBotETagAndCheck(cachedResponse, html, options) {
121
+ if (!options.userAgent || !isPagesStreamingBot(options.userAgent)) return null;
122
+ const etag = generatePagesETag(html);
123
+ cachedResponse.headers.set("ETag", etag);
124
+ if (!requestsNoCache(options.requestCacheControl) && options.ifNoneMatch && etagMatches(etag, options.ifNoneMatch)) return {
125
+ kind: "response",
126
+ response: new Response(null, {
127
+ status: 304,
128
+ headers: cachedResponse.headers
129
+ })
130
+ };
131
+ return null;
132
+ }
97
133
  function rewritePagesCachedHtml(cachedHtml, freshBody, nextDataScript) {
98
134
  const bodyStart = cachedHtml.indexOf("<div id=\"__next\">");
99
135
  const contentStart = bodyStart >= 0 ? bodyStart + 17 : -1;
@@ -175,10 +211,15 @@ async function resolvePagesPageData(options) {
175
211
  const cacheKey = options.isrCacheKey("pages", pathname);
176
212
  const cached = await options.isrGet(cacheKey);
177
213
  const cachedValue = cached?.value.value;
178
- if (!options.isOnDemandRevalidate && cachedValue?.kind === "PAGES" && cached && !cached.isStale && !options.scriptNonce && !options.isDataReq) return {
179
- kind: "response",
180
- response: buildPagesCacheResponse(cachedValue.html, "HIT", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status)
181
- };
214
+ if (!options.isOnDemandRevalidate && cachedValue?.kind === "PAGES" && cached && !cached.isStale && !options.scriptNonce && !options.isDataReq) {
215
+ const hitResponse = buildPagesCacheResponse(cachedValue.html, "HIT", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status);
216
+ const hitBotResult = applyBotETagAndCheck(hitResponse, cachedValue.html, options);
217
+ if (hitBotResult) return hitBotResult;
218
+ return {
219
+ kind: "response",
220
+ response: hitResponse
221
+ };
222
+ }
182
223
  if (!options.isOnDemandRevalidate && cachedValue?.kind === "PAGES" && cached && cached.isStale && !options.scriptNonce && !options.isDataReq) {
183
224
  options.triggerBackgroundRegeneration(cacheKey, async function() {
184
225
  return options.runInFreshUnifiedContext(async () => {
@@ -212,9 +253,12 @@ async function resolvePagesPageData(options) {
212
253
  routePath: options.routePattern,
213
254
  routeType: "render"
214
255
  });
256
+ const staleResponse = buildPagesCacheResponse(cachedValue.html, "STALE", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status);
257
+ const staleBotResult = applyBotETagAndCheck(staleResponse, cachedValue.html, options);
258
+ if (staleBotResult) return staleBotResult;
215
259
  return {
216
260
  kind: "response",
217
- response: buildPagesCacheResponse(cachedValue.html, "STALE", options.fontLinkHeader, void 0, options.expireSeconds, cached.value.cacheControl, cachedValue.status)
261
+ response: staleResponse
218
262
  };
219
263
  }
220
264
  const result = await options.pageModule.getStaticProps({
@@ -28,6 +28,7 @@ type I18nConfig = {
28
28
  } | null;
29
29
  type VinextConfigSubset = {
30
30
  basePath: string;
31
+ assetPrefix: string;
31
32
  trailingSlash: boolean;
32
33
  expireTime?: number;
33
34
  clientTraceMetadata?: readonly string[];