@kasperpeulen/vitest-plugin-nextjs 0.0.0-stage → 0.1.0-alpha.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.
Files changed (250) hide show
  1. package/README.md +249 -2
  2. package/dist/_virtual/_rolldown/runtime.js +14 -0
  3. package/dist/action-ids.d.ts +18 -0
  4. package/dist/action-ids.js +44 -0
  5. package/dist/adapter.d.ts +8 -0
  6. package/dist/adapter.js +12 -0
  7. package/dist/app-requests.d.ts +63 -0
  8. package/dist/app-requests.js +253 -0
  9. package/dist/browser-context.d.ts +24 -0
  10. package/dist/browser-context.js +157 -0
  11. package/dist/build-output.d.ts +68 -0
  12. package/dist/build-output.js +216 -0
  13. package/dist/build.d.ts +26 -0
  14. package/dist/build.js +39 -0
  15. package/dist/built-files.d.ts +9 -0
  16. package/dist/built-files.js +22 -0
  17. package/dist/cache.d.ts +44 -0
  18. package/dist/cache.js +99 -0
  19. package/dist/client-components.d.ts +3 -0
  20. package/dist/client-components.js +4 -0
  21. package/dist/client-file-key.d.ts +5 -0
  22. package/dist/client-file-key.js +5 -0
  23. package/dist/client-files.d.ts +83 -0
  24. package/dist/client-files.js +186 -0
  25. package/dist/client-graph.d.ts +94 -0
  26. package/dist/client-graph.js +266 -0
  27. package/dist/client-modules.d.ts +10 -0
  28. package/dist/client-modules.js +43 -0
  29. package/dist/client.d.ts +42 -0
  30. package/dist/client.js +263 -0
  31. package/dist/commonjs.d.ts +44 -0
  32. package/dist/commonjs.js +111 -0
  33. package/dist/compat/browser/client.d.ts +19 -0
  34. package/dist/compat/browser/client.js +13 -0
  35. package/dist/compat/build.d.ts +145 -0
  36. package/dist/compat/build.js +149 -0
  37. package/dist/compat/checks.d.ts +16 -0
  38. package/dist/compat/checks.js +148 -0
  39. package/dist/compat/patches.d.ts +13 -0
  40. package/dist/compat/patches.js +30 -0
  41. package/dist/compat/paths.d.ts +74 -0
  42. package/dist/compat/paths.js +160 -0
  43. package/dist/compat/rsc/handlers.d.ts +2 -0
  44. package/dist/compat/rsc/handlers.js +2 -0
  45. package/dist/compat/rsc/prerender.d.ts +10 -0
  46. package/dist/compat/rsc/prerender.js +9 -0
  47. package/dist/compat/server/headers.d.ts +3 -0
  48. package/dist/compat/server/headers.js +3 -0
  49. package/dist/compat/ssr/build-output.d.ts +4 -0
  50. package/dist/compat/ssr/build-output.js +3 -0
  51. package/dist/compat/ssr/cache.d.ts +6 -0
  52. package/dist/compat/ssr/cache.js +6 -0
  53. package/dist/compat/ssr/handler.d.ts +8 -0
  54. package/dist/compat/ssr/handler.js +8 -0
  55. package/dist/compat/ssr/node-server.d.ts +5 -0
  56. package/dist/compat/ssr/node-server.js +4 -0
  57. package/dist/compat/ssr/render-errors.d.ts +4 -0
  58. package/dist/compat/ssr/render-errors.js +4 -0
  59. package/dist/compat/ssr/senders.d.ts +6 -0
  60. package/dist/compat/ssr/senders.js +6 -0
  61. package/dist/compat/types.d.ts +8 -0
  62. package/dist/compat/types.js +1 -0
  63. package/dist/compile.d.ts +31 -0
  64. package/dist/compile.js +140 -0
  65. package/dist/disk.d.ts +16 -0
  66. package/dist/disk.js +13 -0
  67. package/dist/document-loads.d.ts +15 -0
  68. package/dist/document-loads.js +53 -0
  69. package/dist/document.d.ts +79 -0
  70. package/dist/document.js +328 -0
  71. package/dist/downloads.d.ts +15 -0
  72. package/dist/downloads.js +71 -0
  73. package/dist/esm-require.d.ts +11 -0
  74. package/dist/esm-require.js +32 -0
  75. package/dist/fast-crypto/index.js +1093 -0
  76. package/dist/fast-crypto.d.ts +20 -0
  77. package/dist/fast-crypto.js +151 -0
  78. package/dist/flight.d.ts +19 -0
  79. package/dist/flight.js +42 -0
  80. package/dist/form-submission.d.ts +15 -0
  81. package/dist/form-submission.js +27 -0
  82. package/dist/from-page.d.ts +5 -0
  83. package/dist/from-page.js +44 -0
  84. package/dist/globals.d.ts +1 -0
  85. package/dist/globals.js +197 -0
  86. package/dist/host-document.d.ts +26 -0
  87. package/dist/host-document.js +34 -0
  88. package/dist/index.d.ts +280 -0
  89. package/dist/index.js +281 -0
  90. package/dist/internal.d.ts +2 -0
  91. package/dist/internal.js +3 -0
  92. package/dist/leftovers.d.ts +11 -0
  93. package/dist/leftovers.js +55 -0
  94. package/dist/mode.d.ts +169 -0
  95. package/dist/mode.js +31 -0
  96. package/dist/node/LICENSES.md +3540 -0
  97. package/dist/node/assert/strict.js +29 -0
  98. package/dist/node/assert.js +3044 -0
  99. package/dist/node/buffer.js +1499 -0
  100. package/dist/node/child_process.js +45 -0
  101. package/dist/node/cluster.js +85 -0
  102. package/dist/node/console.js +96 -0
  103. package/dist/node/constants.js +495 -0
  104. package/dist/node/crypto.js +1388 -0
  105. package/dist/node/dgram.js +78 -0
  106. package/dist/node/diagnostics_channel.js +122 -0
  107. package/dist/node/dns/promises.js +129 -0
  108. package/dist/node/dns.js +137 -0
  109. package/dist/node/domain.js +43 -0
  110. package/dist/node/events.js +1155 -0
  111. package/dist/node/fs/promises.js +40 -0
  112. package/dist/node/fs.js +10375 -0
  113. package/dist/node/http.js +326 -0
  114. package/dist/node/http2.js +305 -0
  115. package/dist/node/https.js +57 -0
  116. package/dist/node/inspector/promises.js +66 -0
  117. package/dist/node/inspector.js +76 -0
  118. package/dist/node/module.js +205 -0
  119. package/dist/node/net.js +392 -0
  120. package/dist/node/os.js +259 -0
  121. package/dist/node/path/posix.js +25 -0
  122. package/dist/node/path/win32.js +25 -0
  123. package/dist/node/path.js +384 -0
  124. package/dist/node/perf_hooks.js +313 -0
  125. package/dist/node/process.js +452 -0
  126. package/dist/node/punycode.js +178 -0
  127. package/dist/node/querystring.js +934 -0
  128. package/dist/node/readline/promises.js +71 -0
  129. package/dist/node/readline.js +56 -0
  130. package/dist/node/repl.js +44 -0
  131. package/dist/node/sqlite.js +22 -0
  132. package/dist/node/stream/consumers.js +47 -0
  133. package/dist/node/stream/promises.js +6 -0
  134. package/dist/node/stream/web.js +52 -0
  135. package/dist/node/stream.js +4323 -0
  136. package/dist/node/string_decoder.js +276 -0
  137. package/dist/node/sys.js +6 -0
  138. package/dist/node/timers/promises.js +71 -0
  139. package/dist/node/timers.js +133 -0
  140. package/dist/node/tls.js +122 -0
  141. package/dist/node/trace_events.js +23 -0
  142. package/dist/node/tty.js +69 -0
  143. package/dist/node/url.js +1426 -0
  144. package/dist/node/util/types.js +1456 -0
  145. package/dist/node/util.js +4807 -0
  146. package/dist/node/v8.js +160 -0
  147. package/dist/node/vm.js +88 -0
  148. package/dist/node/wasi.js +16 -0
  149. package/dist/node/worker_threads.js +123 -0
  150. package/dist/node/zlib.js +1396 -0
  151. package/dist/node-builtins.d.ts +40 -0
  152. package/dist/node-builtins.js +74 -0
  153. package/dist/node-on-page.d.ts +46 -0
  154. package/dist/node-on-page.js +101 -0
  155. package/dist/node-platform.d.ts +36 -0
  156. package/dist/node-platform.js +152 -0
  157. package/dist/node-rendered.d.ts +21 -0
  158. package/dist/node-rendered.js +26 -0
  159. package/dist/node-server.d.ts +46 -0
  160. package/dist/node-server.js +193 -0
  161. package/dist/optimizer-cache-key.d.ts +26 -0
  162. package/dist/optimizer-cache-key.js +73 -0
  163. package/dist/own-package.d.ts +1 -0
  164. package/dist/own-package.js +6 -0
  165. package/dist/page-store.d.ts +33 -0
  166. package/dist/page-store.js +80 -0
  167. package/dist/page-timers.d.ts +22 -0
  168. package/dist/page-timers.js +47 -0
  169. package/dist/page.d.ts +112 -0
  170. package/dist/page.js +549 -0
  171. package/dist/paths.d.ts +13 -0
  172. package/dist/paths.js +47 -0
  173. package/dist/plugin.d.ts +22 -0
  174. package/dist/plugin.js +995 -0
  175. package/dist/prerender-verdict.d.ts +8 -0
  176. package/dist/prerender-verdict.js +67 -0
  177. package/dist/project/context.d.ts +546 -0
  178. package/dist/project/context.js +111 -0
  179. package/dist/project/entries.d.ts +42 -0
  180. package/dist/project/entries.js +189 -0
  181. package/dist/project/env-worker.d.ts +1 -0
  182. package/dist/project/env-worker.js +9 -0
  183. package/dist/project/env.d.ts +36 -0
  184. package/dist/project/env.js +82 -0
  185. package/dist/project/layers.d.ts +31 -0
  186. package/dist/project/layers.js +150 -0
  187. package/dist/project/loaders.d.ts +71 -0
  188. package/dist/project/loaders.js +300 -0
  189. package/dist/project/metadata.d.ts +8 -0
  190. package/dist/project/metadata.js +34 -0
  191. package/dist/project/module-exports.d.ts +29 -0
  192. package/dist/project/module-exports.js +77 -0
  193. package/dist/project/routes.d.ts +19 -0
  194. package/dist/project/routes.js +92 -0
  195. package/dist/project/routing.d.ts +46 -0
  196. package/dist/project/routing.js +280 -0
  197. package/dist/project/transform.d.ts +8 -0
  198. package/dist/project/transform.js +110 -0
  199. package/dist/project/webpack.d.ts +46 -0
  200. package/dist/project/webpack.js +160 -0
  201. package/dist/project.d.ts +264 -0
  202. package/dist/project.js +58 -0
  203. package/dist/registry-global.d.ts +5 -0
  204. package/dist/registry-global.js +5 -0
  205. package/dist/registry.d.ts +241 -0
  206. package/dist/registry.js +30 -0
  207. package/dist/route-chunks.d.ts +13 -0
  208. package/dist/route-chunks.js +32 -0
  209. package/dist/route-lookup.d.ts +34 -0
  210. package/dist/route-lookup.js +207 -0
  211. package/dist/routes-ahead-shared.d.ts +17 -0
  212. package/dist/routes-ahead-shared.js +9 -0
  213. package/dist/routes-ahead.d.ts +10 -0
  214. package/dist/routes-ahead.js +54 -0
  215. package/dist/rsc.d.ts +19 -0
  216. package/dist/rsc.js +171 -0
  217. package/dist/server-action.d.ts +9 -0
  218. package/dist/server-action.js +15 -0
  219. package/dist/server-code.d.ts +176 -0
  220. package/dist/server-code.js +357 -0
  221. package/dist/server-in-front.d.ts +37 -0
  222. package/dist/server-in-front.js +90 -0
  223. package/dist/server-layers.d.ts +22 -0
  224. package/dist/server-layers.js +33 -0
  225. package/dist/setup-browser.d.ts +1 -0
  226. package/dist/setup-browser.js +8 -0
  227. package/dist/setup-node.d.ts +1 -0
  228. package/dist/setup-node.js +6 -0
  229. package/dist/setup.d.ts +8 -0
  230. package/dist/setup.js +62 -0
  231. package/dist/speed-features.d.ts +13 -0
  232. package/dist/speed-features.js +95 -0
  233. package/dist/ssr.d.ts +70 -0
  234. package/dist/ssr.js +480 -0
  235. package/dist/static-build.d.ts +36 -0
  236. package/dist/static-build.js +92 -0
  237. package/dist/storage.d.ts +31 -0
  238. package/dist/storage.js +94 -0
  239. package/dist/styles-shared.d.ts +137 -0
  240. package/dist/styles-shared.js +124 -0
  241. package/dist/styles.d.ts +102 -0
  242. package/dist/styles.js +730 -0
  243. package/dist/stylesheet-versions.d.ts +17 -0
  244. package/dist/stylesheet-versions.js +43 -0
  245. package/dist/tester.html +10 -0
  246. package/dist/use-cache.d.ts +15 -0
  247. package/dist/use-cache.js +93 -0
  248. package/dist/vitest-version.d.ts +6 -0
  249. package/dist/vitest-version.js +12 -0
  250. package/package.json +125 -3
package/dist/page.js ADDED
@@ -0,0 +1,549 @@
1
+ import { loadsNothing, registry } from "./registry.js";
2
+ import { speedFeature } from "./speed-features.js";
3
+ import { loadedSsrLayer, ssrLayer } from "./server-layers.js";
4
+ import { leftTheApp, sendRequest, setOpenPage, setRouterOutlivesPage, unlessAborted } from "./app-requests.js";
5
+ import { keepProperties, loadDocument, unloadDocument, unloadKeptApp } from "./document.js";
6
+ import { callServerWithoutPage } from "./client-graph.js";
7
+ import { download, downloadName } from "./downloads.js";
8
+ import { recordListeners, recordMessageChannels } from "./leftovers.js";
9
+ import { resetAsyncLocalStorage } from "@kasperpeulen/vitest-plugin-rsc/internal/async-local-storage";
10
+ import { routing } from "virtual:vitest-plugin-rsc/next-manifest";
11
+ import { checkFetchedModules, createEnvironmentRunner, environmentModule, forgetHeardVersions, versionOfModules } from "@kasperpeulen/vitest-plugin-rsc/internal/utils";
12
+ //#region src/page.ts
13
+ const startTimeout = 2e4;
14
+ const setTimeout = globalThis.setTimeout;
15
+ const clearTimeout = globalThis.clearTimeout;
16
+ /**
17
+ * A navigation without a page of the app, which has the router, under
18
+ * Vitest: a component that a test file with `"use client"` rendered has none.
19
+ */
20
+ function noRouter(url) {
21
+ return /* @__PURE__ */ new Error(`vitest-plugin-rsc: a link or a form navigated to ${url.href}, and there is no page of the app to navigate in: no page of renderServer() is open, and a test file with "use client" has none. Test navigation in a page of renderServer(), in a test file without "use client".`);
22
+ }
23
+ /**
24
+ * A page of the app, from the first line of its load until it is left, with
25
+ * the node of `renderServer()` on it, and where it reports its errors. Its
26
+ * load makes it part by part, and each part that would outlive the page says
27
+ * how it is released. Leaving stops the load at once, and releases the
28
+ * parts, the last first.
29
+ */
30
+ var Page = class {
31
+ errors;
32
+ nodeOnPage;
33
+ inKeptApp;
34
+ #load = new AbortController();
35
+ /** Aborts as the page is left: what the load does takes it. */
36
+ signal = this.#load.signal;
37
+ #releases = [];
38
+ #started;
39
+ #stopped = () => {};
40
+ #left;
41
+ constructor(errors, nodeOnPage, inKeptApp = false) {
42
+ this.errors = errors;
43
+ this.nodeOnPage = nodeOnPage;
44
+ this.inKeptApp = inKeptApp;
45
+ }
46
+ /** Whether the page is open: from when its client starts until it is left. */
47
+ get open() {
48
+ return this.#started !== void 0 && !this.signal.aborted;
49
+ }
50
+ /**
51
+ * Runs `release` as the page is left: before what the load made before
52
+ * it. As `defer()` of a `DisposableStack`.
53
+ */
54
+ defer(release) {
55
+ this.#releases.push(release);
56
+ }
57
+ /**
58
+ * Opens the page: its client starts. Leaving waits for `started` to
59
+ * settle, at most `startTimeout`, before it releases what the page holds.
60
+ * `stopped` runs once the start is over for the page: once `started` has
61
+ * settled, or once leaving stops waiting for it.
62
+ */
63
+ starts(started, stopped) {
64
+ this.#started = started.then(stopped, stopped);
65
+ this.#stopped = stopped;
66
+ }
67
+ /**
68
+ * Leaves the page: `by` is what leaves it, for its node. Stops its load at
69
+ * once. Resolves once what the page holds is released, or rejects with the
70
+ * first error of a release, after every release has run. A page still
71
+ * starting after `startTimeout` is released once it has stopped, and
72
+ * `reportError()` gets that error: leaving resolves without it. Once:
73
+ * leaving it again gets the same promise.
74
+ */
75
+ leave(by) {
76
+ return this.#left ??= this.#leave(by);
77
+ }
78
+ async #leave(by) {
79
+ if (this.inKeptApp && this.open) dispatchPageTransition("pagehide");
80
+ this.#load.abort(new DOMException("The page was left before it had loaded.", "AbortError"));
81
+ this.nodeOnPage?.left(by);
82
+ const released = (this.#started ?? Promise.resolve()).then(() => this.#release());
83
+ let timeout;
84
+ const stopped = await Promise.race([released.then(() => true, () => true), new Promise((resolve) => timeout = setTimeout(resolve, startTimeout, false))]);
85
+ clearTimeout(timeout);
86
+ if (stopped) return released;
87
+ console.warn(`vitest-plugin-rsc: the page that was left was still loading its client code ${startTimeout / 1e3}s later. The next page loads anyway.`);
88
+ this.#stopped();
89
+ released.catch(reportError);
90
+ }
91
+ #release() {
92
+ let failed;
93
+ for (const release of this.#releases.splice(0).reverse()) try {
94
+ release();
95
+ } catch (error) {
96
+ failed ??= { error };
97
+ }
98
+ if (failed) throw failed.error;
99
+ }
100
+ };
101
+ let current;
102
+ let kept;
103
+ /**
104
+ * Leaves the kept app, if any: its root, its module graph, what its modules
105
+ * left behind as they loaded, and what its code added to the `<head>` go.
106
+ * Once its page is left: see `leavePage()`, and `leaveKeptAppWithPage()`.
107
+ * Not `withGlobals` once the app after it has started, whose they are then.
108
+ */
109
+ function leaveKeptApp(withGlobals = true) {
110
+ const app = kept;
111
+ kept = void 0;
112
+ setRouterOutlivesPage(false);
113
+ try {
114
+ app?.leave();
115
+ } finally {
116
+ unloadKeptApp(withGlobals);
117
+ }
118
+ }
119
+ /**
120
+ * Leaves the page of the kept app that is open, as `leavePage()` does with
121
+ * `by`, and then the kept app: a test file ends with them. The app goes also
122
+ * after a page that could not be left, and the first error rejects.
123
+ */
124
+ async function leaveKeptAppWithPage(by) {
125
+ let failed;
126
+ try {
127
+ if (current?.inKeptApp) await leavePage(by);
128
+ } catch (error) {
129
+ failed = { error };
130
+ }
131
+ try {
132
+ leaveKeptApp();
133
+ } catch (error) {
134
+ failed ??= { error };
135
+ }
136
+ if (failed) throw failed.error;
137
+ }
138
+ function releaseAll(releases) {
139
+ let failed;
140
+ for (const release of releases.splice(0).reverse()) try {
141
+ release();
142
+ } catch (error) {
143
+ failed ??= { error };
144
+ }
145
+ if (failed) throw failed.error;
146
+ }
147
+ /** The page that is open: see `Page.open`. */
148
+ function currentPage() {
149
+ return current?.open ? current : void 0;
150
+ }
151
+ async function loadPage(url, request, from, opening) {
152
+ const errors = opening?.errors ?? currentPage()?.errors ?? [];
153
+ const inKeptApp = opening ? opening.inKeptApp === true : currentPage()?.inKeptApp === true;
154
+ const leaving = leavePage(opening ? "another renderServer()" : "a page load of the app");
155
+ const page = current = new Page(errors, opening?.nodeOnPage, inKeptApp);
156
+ const loading = (async () => {
157
+ await leaving;
158
+ page.signal.throwIfAborted();
159
+ if (!speedFeature("cacheDevServer")) forgetHeardVersions();
160
+ const changed = opening && inKeptApp && kept && kept.modules !== await versionOfModules("react_client");
161
+ page.signal.throwIfAborted();
162
+ if (!inKeptApp || !kept || changed) leaveKeptApp();
163
+ if (opening?.container?.hasChildNodes()) throw new Error("vitest-plugin-rsc: the container of a node has to be empty. The node is hydrated in it, and leaving the node empties it.");
164
+ const opened = registry.opened = opening?.opened;
165
+ if (opening) {
166
+ from.headers = opening.headers;
167
+ if (opening.cookie !== null) from.seed(opening.cookie);
168
+ }
169
+ try {
170
+ return await openPage(page, url, request, from, opening);
171
+ } catch (error) {
172
+ if (opened && registry.opened === opened && !page.signal.aborted) registry.opened = void 0;
173
+ throw error;
174
+ }
175
+ })();
176
+ if (!opening?.signal) return loading;
177
+ return unlessAborted(loading, opening.signal, () => {
178
+ if (current === page) leavePage("the signal of renderServer()").catch(reportError);
179
+ });
180
+ }
181
+ async function openPage(page, url, request, from, opening) {
182
+ const { signal } = page;
183
+ const superseded = () => signal.throwIfAborted();
184
+ const send = (target, init) => sendPageLoad(target, init, {
185
+ from,
186
+ navigation: "test"
187
+ }, signal);
188
+ const requestDocument = async (init) => {
189
+ try {
190
+ const response = await send(url, init);
191
+ superseded();
192
+ return await readDocument(response, (what) => `${what}, which is not a page to open. Use handleRequest() to assert on the response itself.`);
193
+ } catch (error) {
194
+ superseded();
195
+ throw error;
196
+ }
197
+ };
198
+ const byFlight = page.inKeptApp && kept !== void 0 && speedFeature("flightLoads");
199
+ const withoutHtml = !("html" in request) && (byFlight || opening?.opened.ssr === false) ? await requestWithoutHtml(url, request, byFlight, send, signal) : void 0;
200
+ superseded();
201
+ const { response, html, after, payload } = withoutHtml ?? ("html" in request ? request : await requestDocument(request));
202
+ superseded();
203
+ let container = opening?.container;
204
+ if (opening && new URL(response.url).pathname !== opening.opened.pathname) {
205
+ registry.opened = void 0;
206
+ container = void 0;
207
+ opening.nodeOnPage?.replaced();
208
+ }
209
+ if (/^\s*<!doctype/i.test(html)) {
210
+ container = void 0;
211
+ if (!opening?.opened.node?.layouts) opening?.nodeOnPage?.replaced();
212
+ }
213
+ const startsApp = !(page.inKeptApp && kept);
214
+ const modules = startsApp && page.inKeptApp ? await versionOfModules("react_client") : 0;
215
+ if (startsApp) await checkFetchedModules();
216
+ superseded();
217
+ const forgetReact = await loadDocument(html, response.url, container, after, page.inKeptApp);
218
+ superseded();
219
+ const { inKeptApp } = page;
220
+ if (inKeptApp && kept) {
221
+ await startInKeptApp(page, kept.client, payload);
222
+ superseded();
223
+ return response;
224
+ }
225
+ const appReleases = [];
226
+ const deferForApp = (release) => inKeptApp ? appReleases.push(release) : page.defer(release);
227
+ const runner = createEnvironmentRunner("react_client");
228
+ deferForApp(() => void runner.close());
229
+ const browserRequire = (id) => registry.loadBrowserModule?.(id) ?? loadsNothing();
230
+ registry.browserRequire = browserRequire;
231
+ deferForApp(() => {
232
+ if (registry.browserRequire === browserRequire) registry.browserRequire = loadsNothing;
233
+ });
234
+ const leftovers = [recordListeners(window), recordMessageChannels()];
235
+ for (const leftover of leftovers) deferForApp(() => leftover.remove());
236
+ const loaded = () => leftovers.forEach((leftover) => leftover.stop());
237
+ const started = (async () => {
238
+ try {
239
+ const client = await runner.import(environmentModule("react_client", "@kasperpeulen/vitest-plugin-nextjs/client"));
240
+ superseded();
241
+ const { unmount, leaveApp } = await client.start(loaded, container, signal, opening?.nodeOnPage, page.errors, inKeptApp);
242
+ page.defer(() => {
243
+ try {
244
+ forgetReact();
245
+ } finally {
246
+ unmount();
247
+ }
248
+ });
249
+ if (!inKeptApp) return;
250
+ appReleases.push(leaveApp);
251
+ const marks = Reflect.ownKeys(document).filter((key) => typeof key === "string" && (key.startsWith("__reactContainer$") || key.startsWith("_reactListening")));
252
+ keepProperties(document, marks);
253
+ leaveKeptApp(false);
254
+ kept = {
255
+ client,
256
+ modules,
257
+ leave() {
258
+ keepProperties(document);
259
+ try {
260
+ releaseAll(appReleases);
261
+ } finally {
262
+ for (const key of marks) Reflect.deleteProperty(document, key);
263
+ }
264
+ }
265
+ };
266
+ setRouterOutlivesPage(true);
267
+ } catch (error) {
268
+ releaseAll(appReleases);
269
+ throw error;
270
+ }
271
+ })();
272
+ page.starts(started, loaded);
273
+ setOpenPage(signal);
274
+ try {
275
+ await started;
276
+ } catch (error) {
277
+ superseded();
278
+ throw error;
279
+ }
280
+ superseded();
281
+ return response;
282
+ }
283
+ function dispatchPageTransition(type) {
284
+ window.dispatchEvent(new PageTransitionEvent(type, { persisted: false }));
285
+ }
286
+ async function startInKeptApp(page, client, payload) {
287
+ const { signal } = page;
288
+ dispatchPageTransition("pageshow");
289
+ const started = client.renderAgain(signal, page.errors, payload).then(({ unmount }) => page.defer(unmount));
290
+ page.starts(started, () => {});
291
+ setOpenPage(signal);
292
+ try {
293
+ await started;
294
+ } catch (error) {
295
+ signal.throwIfAborted();
296
+ throw error;
297
+ }
298
+ }
299
+ async function sendPageLoad(url, init, sending, signal) {
300
+ checkFetchedModules().catch(() => {});
301
+ signal.throwIfAborted();
302
+ return sendRequest(new registry.Request(url, {
303
+ ...init,
304
+ signal
305
+ }), sending);
306
+ }
307
+ const flightDocument = "<!DOCTYPE html><html id=\"__next_error__\"><head></head><body></body></html>";
308
+ async function requestWithoutHtml(url, init, byFlight, send, signal) {
309
+ const headers = new Headers(init.headers);
310
+ if (byFlight) headers.set("rsc", "1");
311
+ const request = {
312
+ ...init,
313
+ headers
314
+ };
315
+ try {
316
+ const loaded = await followPage(await send(url, request), (target) => send(target, request), request, byFlight ? readFlight : readDocumentWithoutHtml);
317
+ signal.throwIfAborted();
318
+ if ("html" in loaded) return loaded;
319
+ return await readDocument(loaded.response, (what) => `${what}, which is not a page to open. Use handleRequest() to assert on the response itself.`);
320
+ } catch (error) {
321
+ signal.throwIfAborted();
322
+ throw error;
323
+ }
324
+ }
325
+ const isFlight = (response) => /^text\/x-component\b/i.test(response.headers.get("content-type") ?? "");
326
+ async function readFlight(response) {
327
+ if (!isFlight(response)) return void 0;
328
+ const payload = new Uint8Array(await response.arrayBuffer());
329
+ return {
330
+ document: {
331
+ response,
332
+ html: flightDocument,
333
+ payload
334
+ },
335
+ payload
336
+ };
337
+ }
338
+ async function readDocumentWithoutHtml(response) {
339
+ if (!/^text\/html\b/i.test(response.headers.get("content-type") ?? "")) return void 0;
340
+ const html = await response.text();
341
+ return {
342
+ document: {
343
+ response,
344
+ html
345
+ },
346
+ payload: payloadInDocument(html)
347
+ };
348
+ }
349
+ const documentScripts = /<script\b[^>]*>([^<]*)<\/script>/g;
350
+ const pushOfPayload = "self.__next_f.push(";
351
+ /** The Flight payload in the scripts of a document of Next. */
352
+ function payloadInDocument(html) {
353
+ const encoder = new TextEncoder();
354
+ const chunks = [];
355
+ for (const [, script = ""] of html.matchAll(documentScripts)) {
356
+ if (!script.startsWith(pushOfPayload)) continue;
357
+ const [kind, data = ""] = JSON.parse(script.slice(19, -1));
358
+ if (kind === 1) chunks.push(encoder.encode(data));
359
+ else if (kind === 3) chunks.push(Uint8Array.from(atob(data), (c) => c.charCodeAt(0)));
360
+ }
361
+ const payload = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.length, 0));
362
+ chunks.reduce((offset, chunk) => (payload.set(chunk, offset), offset + chunk.length), 0);
363
+ return payload;
364
+ }
365
+ async function followPage(response, send, init, read) {
366
+ let redirected = response.redirected;
367
+ for (let redirects = 0;;) {
368
+ const page = await read(response);
369
+ if (!page) return { response: pageResponse(response, response.body, redirected) };
370
+ const { shellErrorAnswer } = await ssrLayer();
371
+ const failure = renderFailureOf(page.payload, shellErrorAnswer);
372
+ if (failure?.location === void 0) {
373
+ const { document } = page;
374
+ const status = response.status === 200 ? failure?.status : void 0;
375
+ const body = document.payload ?? document.html;
376
+ return {
377
+ ...document,
378
+ response: pageResponse(response, body, redirected, status)
379
+ };
380
+ }
381
+ const target = new URL(failure.location, response.url);
382
+ if (target.origin !== window.location.origin) throw leftTheApp(target);
383
+ if (++redirects > 20) throw new Error(`vitest-plugin-rsc: too many redirects for ${response.url}. The last one was to ${target.href}.`);
384
+ if (init.body instanceof ReadableStream) throw new TypeError("fetch failed", { cause: /* @__PURE__ */ new Error(`vitest-plugin-rsc: ${response.url} redirected to ${target.href}, and the body of its request is a stream, which cannot be sent again.`) });
385
+ redirected = true;
386
+ response = await send(target);
387
+ }
388
+ }
389
+ function pageResponse(response, body, redirected, status = response.status) {
390
+ if ((response.redirected || !redirected) && status === response.status) return response;
391
+ const { statusText, headers, url } = response;
392
+ return Object.defineProperties(new registry.Response(body, {
393
+ status,
394
+ statusText,
395
+ headers
396
+ }), {
397
+ url: { value: url },
398
+ redirected: { value: redirected || response.redirected }
399
+ });
400
+ }
401
+ const errorRows = /(?:^|\n)[0-9a-f]+:E(\{.*)/g;
402
+ /**
403
+ * How the server would have answered the document of the page of a Flight
404
+ * payload, had it rendered its HTML, by the errors in the payload (see
405
+ * `shellErrorAnswer()` in ssr.ts): the first `redirect()`, wherever it is in
406
+ * the tree, as Next's router follows it (its RedirectBoundary), else the
407
+ * status of the first other error. Undefined for a payload without an error.
408
+ * A path of the app's has the app's `basePath` before it, which `redirect()`
409
+ * takes without it, as Next's server puts it in the `Location` of the 307 of
410
+ * a document.
411
+ */
412
+ function renderFailureOf(payload, answerOf) {
413
+ let failure;
414
+ for (const [, row] of new TextDecoder().decode(payload).matchAll(errorRows)) {
415
+ let digest;
416
+ try {
417
+ digest = JSON.parse(row).digest;
418
+ } catch {
419
+ continue;
420
+ }
421
+ if (typeof digest !== "string") continue;
422
+ const answer = answerOf(digest);
423
+ const { location } = answer;
424
+ if (location !== void 0) return {
425
+ ...answer,
426
+ location: location.startsWith("/") ? routing.basePath + location : location
427
+ };
428
+ failure ??= answer;
429
+ }
430
+ return failure;
431
+ }
432
+ async function readDocument(response, notAPage) {
433
+ const contentType = response.headers.get("content-type") ?? "";
434
+ if (!/^text\/html\b/i.test(contentType)) {
435
+ await response.body?.cancel();
436
+ const what = `${response.url} responded with ${contentType || "no content type"}`;
437
+ throw new Error(`vitest-plugin-rsc: ${notAPage(what)}`);
438
+ }
439
+ return {
440
+ response,
441
+ html: await response.text()
442
+ };
443
+ }
444
+ let navigating = new AbortController();
445
+ let withoutPage = new AbortController();
446
+ /**
447
+ * A navigation of the app without its router, as a browser makes it: a
448
+ * `<form>`, an `<a>` that React does not handle, `location.assign()`. The
449
+ * page that is open stays until the answer is there, and for good when the
450
+ * answer is a download (downloads.ts). Else the answer is the next page,
451
+ * which needs a page of the app to navigate in. `downloadRequest` is the
452
+ * navigation's, and `from` the store of the browser context it is in. With
453
+ * `push` the next page gets an entry of the history of its own, as after a
454
+ * link or a form in a browser. Else it takes the entry of the page, as
455
+ * after a reload or `location.replace()`.
456
+ */
457
+ async function navigate(url, init, downloadRequest, from, push) {
458
+ const page = currentPage();
459
+ const signals = [page?.signal ?? withoutPage.signal];
460
+ if (downloadRequest === null) {
461
+ navigating.abort(new DOMException("Another navigation took the place of this one.", "AbortError"));
462
+ navigating = new AbortController();
463
+ signals.push(navigating.signal);
464
+ }
465
+ const signal = AbortSignal.any(signals);
466
+ const byFlight = downloadRequest === null && page?.inKeptApp === true && kept !== void 0 && speedFeature("flightLoads") && (init.method ?? "GET").toUpperCase() === "GET";
467
+ const send = (target, flight) => {
468
+ const headers = new Headers(init.headers);
469
+ if (flight) headers.set("rsc", "1");
470
+ return sendPageLoad(target, {
471
+ ...init,
472
+ headers
473
+ }, {
474
+ from,
475
+ navigation: "app",
476
+ opened: page?.inKeptApp ? {
477
+ pathname: target.pathname,
478
+ proxy: true,
479
+ ssr: false
480
+ } : void 0,
481
+ network: downloadRequest === null ? void 0 : () => registry.network(url, {
482
+ ...init,
483
+ signal
484
+ })
485
+ }, signal);
486
+ };
487
+ let loaded;
488
+ try {
489
+ let response = await send(url, byFlight);
490
+ signal.throwIfAborted();
491
+ if (page?.inKeptApp && downloadRequest === null) {
492
+ const followed = await followPage(response, (target) => send(target, byFlight), init, byFlight ? readFlight : readDocumentWithoutHtml);
493
+ signal.throwIfAborted();
494
+ if ("html" in followed) loaded = followed;
495
+ else response = followed.response;
496
+ }
497
+ if (!loaded) {
498
+ const name = downloadName(response, downloadRequest);
499
+ if (name !== void 0) return await download(response, name, signal);
500
+ if (!page) {
501
+ await response.body?.cancel();
502
+ throw noRouter(url);
503
+ }
504
+ loaded = await readDocument(response, (what) => `the app navigated to a URL that is not a page: ${what}. A browser would show it in place of the app, which a test cannot do. A route handler that answers with a file to save says so with Content-Disposition: attachment, or a link with download.`);
505
+ signal.throwIfAborted();
506
+ }
507
+ } catch (error) {
508
+ signal.throwIfAborted();
509
+ throw error;
510
+ }
511
+ await loadPage(url, {
512
+ ...loaded,
513
+ after: entryOf(push)
514
+ }, from);
515
+ }
516
+ const entryOf = (push) => push ? {
517
+ url: window.location.href,
518
+ state: window.history.state
519
+ } : void 0;
520
+ let leaving = Promise.resolve();
521
+ function leavePage(by) {
522
+ const left = current;
523
+ current = void 0;
524
+ withoutPage.abort(new DOMException("The test moved on before it was done.", "AbortError"));
525
+ withoutPage = new AbortController();
526
+ const pageLeft = left?.leave(by);
527
+ const gone = leaving.catch(() => {}).then(async () => {
528
+ try {
529
+ await pageLeft;
530
+ } finally {
531
+ try {
532
+ unloadDocument();
533
+ } finally {
534
+ try {
535
+ await loadedSsrLayer()?.settleRequests();
536
+ } finally {
537
+ registry.opened = void 0;
538
+ setOpenPage(void 0);
539
+ globalThis.__viteRscCallServer = callServerWithoutPage;
540
+ resetAsyncLocalStorage();
541
+ }
542
+ }
543
+ }
544
+ });
545
+ leaving = gone;
546
+ return gone;
547
+ }
548
+ //#endregion
549
+ export { Page, currentPage, leaveKeptAppWithPage, leavePage, loadPage, navigate, noRouter, payloadInDocument, renderFailureOf };
@@ -0,0 +1,13 @@
1
+ import { NextProject } from "./project.js";
2
+ import { Plugin } from "vite";
3
+
4
+ //#region src/paths.d.ts
5
+ /**
6
+ * The files that the `paths` have for a request, in order: TypeScript takes
7
+ * the pattern with the longest prefix. Not a declaration file, which is for
8
+ * the types only.
9
+ */
10
+ declare function candidatesOf(paths: NonNullable<NextProject["paths"]>, request: string): string[];
11
+ declare function createPathsPlugin(getProject: () => NextProject): Plugin;
12
+ //#endregion
13
+ export { candidatesOf, createPathsPlugin };
package/dist/paths.js ADDED
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ //#region src/paths.ts
3
+ /**
4
+ * The part of `request` that the `*` of `pattern` stands for, or nothing. A
5
+ * pattern has one `*` at most: Next leaves out one with more.
6
+ */
7
+ function matchOf(pattern, request) {
8
+ const star = pattern.indexOf("*");
9
+ if (star === -1) return pattern === request ? "" : void 0;
10
+ if (pattern.includes("*", star + 1)) return void 0;
11
+ const [before, after] = [pattern.slice(0, star), pattern.slice(star + 1)];
12
+ return request.length >= before.length + after.length && request.startsWith(before) && request.endsWith(after) ? request.slice(before.length, request.length - after.length) : void 0;
13
+ }
14
+ /**
15
+ * The files that the `paths` have for a request, in order: TypeScript takes
16
+ * the pattern with the longest prefix. Not a declaration file, which is for
17
+ * the types only.
18
+ */
19
+ function candidatesOf(paths, request) {
20
+ const prefix = (pattern) => pattern.split("*")[0].length;
21
+ const pattern = Object.keys(paths.patterns).filter((candidate) => matchOf(candidate, request) !== void 0).sort((a, b) => prefix(b) - prefix(a))[0];
22
+ if (!pattern) return [];
23
+ return paths.patterns[pattern].filter((target) => !target.endsWith(".d.ts")).map((target) => path.resolve(paths.baseUrl, target.replace("*", matchOf(pattern, request))));
24
+ }
25
+ function createPathsPlugin(getProject) {
26
+ return {
27
+ name: "vitest-plugin-rsc:next-paths",
28
+ enforce: "pre",
29
+ async resolveId(source, importer, options) {
30
+ const { paths } = getProject();
31
+ if (!paths || !importer || importer.includes("/node_modules/")) return;
32
+ if (!(!source.startsWith(".") && !source.startsWith("\0")) || path.isAbsolute(source)) return;
33
+ const resolve = (id) => this.resolve(id, importer, {
34
+ ...options,
35
+ skipSelf: true
36
+ });
37
+ for (const candidate of candidatesOf(paths, source)) {
38
+ const resolved = await resolve(candidate);
39
+ if (resolved) return resolved;
40
+ }
41
+ if (!paths.explicitBaseUrl) return;
42
+ return await resolve(source) ?? resolve(path.resolve(paths.baseUrl, source));
43
+ }
44
+ };
45
+ }
46
+ //#endregion
47
+ export { candidatesOf, createPathsPlugin };
@@ -0,0 +1,22 @@
1
+ import { Mode, ModeOptions, SpeedOptions } from "./mode.js";
2
+ import { NextBuild } from "./build.js";
3
+ import { ServerCodeOptions } from "./server-code.js";
4
+ import { Plugin } from "vite";
5
+ //#region src/plugin.d.ts
6
+ type VitestPluginNextOptions = ServerCodeOptions & ModeOptions & {
7
+ /**
8
+ * Experimental. The code that Next's runtime and React run, in all three
9
+ * layers: `"development"` as `next dev` runs it, `"production"` as
10
+ * `next start` runs it after `next build`. Production renders faster,
11
+ * without what the development code checks and reports. In a test run
12
+ * the app's `process.env.NODE_ENV` stays as it is, in a static build it is
13
+ * the build. See docs/internals/next-routes.md, "Development Or Production".
14
+ *
15
+ * @default `"development"` for a test run and a dev server, `"production"`
16
+ * for `vite build`, a static build of the app
17
+ */
18
+ build?: NextBuild;
19
+ };
20
+ declare function vitestPluginNext(options?: VitestPluginNextOptions): Plugin[];
21
+ //#endregion
22
+ export { type Mode, type NextBuild, type SpeedOptions, VitestPluginNextOptions, vitestPluginNext };