@kasperpeulen/vitest-plugin-nextjs 0.0.0-stage → 0.1.0-alpha.1
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 +249 -2
- package/dist/_virtual/_rolldown/runtime.js +14 -0
- package/dist/action-ids.d.ts +18 -0
- package/dist/action-ids.js +44 -0
- package/dist/adapter.d.ts +8 -0
- package/dist/adapter.js +12 -0
- package/dist/app-requests.d.ts +63 -0
- package/dist/app-requests.js +253 -0
- package/dist/browser-context.d.ts +24 -0
- package/dist/browser-context.js +157 -0
- package/dist/build-output.d.ts +68 -0
- package/dist/build-output.js +216 -0
- package/dist/build.d.ts +26 -0
- package/dist/build.js +39 -0
- package/dist/built-files.d.ts +9 -0
- package/dist/built-files.js +22 -0
- package/dist/cache.d.ts +44 -0
- package/dist/cache.js +99 -0
- package/dist/client-components.d.ts +3 -0
- package/dist/client-components.js +4 -0
- package/dist/client-file-key.d.ts +5 -0
- package/dist/client-file-key.js +5 -0
- package/dist/client-files.d.ts +83 -0
- package/dist/client-files.js +186 -0
- package/dist/client-graph.d.ts +94 -0
- package/dist/client-graph.js +266 -0
- package/dist/client-modules.d.ts +10 -0
- package/dist/client-modules.js +43 -0
- package/dist/client.d.ts +42 -0
- package/dist/client.js +263 -0
- package/dist/commonjs.d.ts +44 -0
- package/dist/commonjs.js +111 -0
- package/dist/compat/browser/client.d.ts +19 -0
- package/dist/compat/browser/client.js +13 -0
- package/dist/compat/build.d.ts +145 -0
- package/dist/compat/build.js +149 -0
- package/dist/compat/checks.d.ts +16 -0
- package/dist/compat/checks.js +148 -0
- package/dist/compat/patches.d.ts +13 -0
- package/dist/compat/patches.js +30 -0
- package/dist/compat/paths.d.ts +74 -0
- package/dist/compat/paths.js +160 -0
- package/dist/compat/rsc/handlers.d.ts +2 -0
- package/dist/compat/rsc/handlers.js +2 -0
- package/dist/compat/rsc/prerender.d.ts +10 -0
- package/dist/compat/rsc/prerender.js +9 -0
- package/dist/compat/server/headers.d.ts +3 -0
- package/dist/compat/server/headers.js +3 -0
- package/dist/compat/ssr/build-output.d.ts +4 -0
- package/dist/compat/ssr/build-output.js +3 -0
- package/dist/compat/ssr/cache.d.ts +6 -0
- package/dist/compat/ssr/cache.js +6 -0
- package/dist/compat/ssr/handler.d.ts +8 -0
- package/dist/compat/ssr/handler.js +8 -0
- package/dist/compat/ssr/node-server.d.ts +5 -0
- package/dist/compat/ssr/node-server.js +4 -0
- package/dist/compat/ssr/render-errors.d.ts +4 -0
- package/dist/compat/ssr/render-errors.js +4 -0
- package/dist/compat/ssr/senders.d.ts +6 -0
- package/dist/compat/ssr/senders.js +6 -0
- package/dist/compat/types.d.ts +8 -0
- package/dist/compat/types.js +1 -0
- package/dist/compile.d.ts +39 -0
- package/dist/compile.js +175 -0
- package/dist/disk.d.ts +16 -0
- package/dist/disk.js +13 -0
- package/dist/document-loads.d.ts +15 -0
- package/dist/document-loads.js +53 -0
- package/dist/document.d.ts +79 -0
- package/dist/document.js +330 -0
- package/dist/downloads.d.ts +15 -0
- package/dist/downloads.js +71 -0
- package/dist/esm-require.d.ts +11 -0
- package/dist/esm-require.js +32 -0
- package/dist/fast-crypto/index.js +1093 -0
- package/dist/fast-crypto.d.ts +20 -0
- package/dist/fast-crypto.js +151 -0
- package/dist/flight.d.ts +19 -0
- package/dist/flight.js +42 -0
- package/dist/form-submission.d.ts +15 -0
- package/dist/form-submission.js +27 -0
- package/dist/from-page.d.ts +5 -0
- package/dist/from-page.js +44 -0
- package/dist/globals.d.ts +1 -0
- package/dist/globals.js +197 -0
- package/dist/host-document.d.ts +26 -0
- package/dist/host-document.js +34 -0
- package/dist/host-return-shared.d.ts +5 -0
- package/dist/host-return-shared.js +5 -0
- package/dist/host-return.d.ts +5 -0
- package/dist/host-return.js +16 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.js +281 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +3 -0
- package/dist/leftovers.d.ts +11 -0
- package/dist/leftovers.js +55 -0
- package/dist/mode.d.ts +169 -0
- package/dist/mode.js +31 -0
- package/dist/node/LICENSES.md +3540 -0
- package/dist/node/assert/strict.js +29 -0
- package/dist/node/assert.js +3044 -0
- package/dist/node/buffer.js +1499 -0
- package/dist/node/child_process.js +45 -0
- package/dist/node/cluster.js +85 -0
- package/dist/node/console.js +96 -0
- package/dist/node/constants.js +495 -0
- package/dist/node/crypto.js +1388 -0
- package/dist/node/dgram.js +78 -0
- package/dist/node/diagnostics_channel.js +122 -0
- package/dist/node/dns/promises.js +129 -0
- package/dist/node/dns.js +137 -0
- package/dist/node/domain.js +43 -0
- package/dist/node/events.js +1155 -0
- package/dist/node/fs/promises.js +40 -0
- package/dist/node/fs.js +10375 -0
- package/dist/node/http.js +326 -0
- package/dist/node/http2.js +305 -0
- package/dist/node/https.js +57 -0
- package/dist/node/inspector/promises.js +66 -0
- package/dist/node/inspector.js +76 -0
- package/dist/node/module.js +205 -0
- package/dist/node/net.js +392 -0
- package/dist/node/os.js +259 -0
- package/dist/node/path/posix.js +25 -0
- package/dist/node/path/win32.js +25 -0
- package/dist/node/path.js +384 -0
- package/dist/node/perf_hooks.js +313 -0
- package/dist/node/process.js +452 -0
- package/dist/node/punycode.js +178 -0
- package/dist/node/querystring.js +934 -0
- package/dist/node/readline/promises.js +71 -0
- package/dist/node/readline.js +56 -0
- package/dist/node/repl.js +44 -0
- package/dist/node/sqlite.js +22 -0
- package/dist/node/stream/consumers.js +47 -0
- package/dist/node/stream/promises.js +6 -0
- package/dist/node/stream/web.js +52 -0
- package/dist/node/stream.js +4323 -0
- package/dist/node/string_decoder.js +276 -0
- package/dist/node/sys.js +6 -0
- package/dist/node/timers/promises.js +71 -0
- package/dist/node/timers.js +133 -0
- package/dist/node/tls.js +122 -0
- package/dist/node/trace_events.js +23 -0
- package/dist/node/tty.js +69 -0
- package/dist/node/url.js +1426 -0
- package/dist/node/util/types.js +1456 -0
- package/dist/node/util.js +4807 -0
- package/dist/node/v8.js +160 -0
- package/dist/node/vm.js +88 -0
- package/dist/node/wasi.js +16 -0
- package/dist/node/worker_threads.js +123 -0
- package/dist/node/zlib.js +1396 -0
- package/dist/node-builtins.d.ts +40 -0
- package/dist/node-builtins.js +74 -0
- package/dist/node-on-page.d.ts +46 -0
- package/dist/node-on-page.js +101 -0
- package/dist/node-platform.d.ts +36 -0
- package/dist/node-platform.js +152 -0
- package/dist/node-rendered.d.ts +21 -0
- package/dist/node-rendered.js +26 -0
- package/dist/node-server.d.ts +46 -0
- package/dist/node-server.js +193 -0
- package/dist/not-found-page.d.ts +19 -0
- package/dist/not-found-page.js +74 -0
- package/dist/optimizer-cache-key.d.ts +26 -0
- package/dist/optimizer-cache-key.js +73 -0
- package/dist/own-package.d.ts +1 -0
- package/dist/own-package.js +6 -0
- package/dist/page-store.d.ts +33 -0
- package/dist/page-store.js +80 -0
- package/dist/page-timers.d.ts +22 -0
- package/dist/page-timers.js +47 -0
- package/dist/page.d.ts +112 -0
- package/dist/page.js +549 -0
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +47 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +1006 -0
- package/dist/prerender-verdict.d.ts +8 -0
- package/dist/prerender-verdict.js +67 -0
- package/dist/project/context.d.ts +546 -0
- package/dist/project/context.js +111 -0
- package/dist/project/entries.d.ts +42 -0
- package/dist/project/entries.js +189 -0
- package/dist/project/env-worker.d.ts +1 -0
- package/dist/project/env-worker.js +9 -0
- package/dist/project/env.d.ts +36 -0
- package/dist/project/env.js +82 -0
- package/dist/project/layers.d.ts +31 -0
- package/dist/project/layers.js +150 -0
- package/dist/project/loaders.d.ts +71 -0
- package/dist/project/loaders.js +300 -0
- package/dist/project/metadata.d.ts +8 -0
- package/dist/project/metadata.js +34 -0
- package/dist/project/module-exports.d.ts +29 -0
- package/dist/project/module-exports.js +77 -0
- package/dist/project/routes.d.ts +19 -0
- package/dist/project/routes.js +92 -0
- package/dist/project/routing.d.ts +46 -0
- package/dist/project/routing.js +280 -0
- package/dist/project/transform.d.ts +8 -0
- package/dist/project/transform.js +110 -0
- package/dist/project/webpack.d.ts +65 -0
- package/dist/project/webpack.js +191 -0
- package/dist/project.d.ts +269 -0
- package/dist/project.js +58 -0
- package/dist/registry-global.d.ts +5 -0
- package/dist/registry-global.js +5 -0
- package/dist/registry.d.ts +241 -0
- package/dist/registry.js +30 -0
- package/dist/route-chunks.d.ts +13 -0
- package/dist/route-chunks.js +32 -0
- package/dist/route-lookup.d.ts +34 -0
- package/dist/route-lookup.js +207 -0
- package/dist/routes-ahead-shared.d.ts +17 -0
- package/dist/routes-ahead-shared.js +9 -0
- package/dist/routes-ahead.d.ts +10 -0
- package/dist/routes-ahead.js +54 -0
- package/dist/rsc.d.ts +19 -0
- package/dist/rsc.js +171 -0
- package/dist/server-action.d.ts +9 -0
- package/dist/server-action.js +15 -0
- package/dist/server-code.d.ts +176 -0
- package/dist/server-code.js +357 -0
- package/dist/server-in-front.d.ts +37 -0
- package/dist/server-in-front.js +90 -0
- package/dist/server-layers.d.ts +22 -0
- package/dist/server-layers.js +33 -0
- package/dist/setup-browser.d.ts +1 -0
- package/dist/setup-browser.js +8 -0
- package/dist/setup-node.d.ts +1 -0
- package/dist/setup-node.js +6 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +62 -0
- package/dist/speed-features.d.ts +13 -0
- package/dist/speed-features.js +95 -0
- package/dist/ssr.d.ts +70 -0
- package/dist/ssr.js +480 -0
- package/dist/static-build.d.ts +36 -0
- package/dist/static-build.js +92 -0
- package/dist/storage.d.ts +31 -0
- package/dist/storage.js +94 -0
- package/dist/styles-shared.d.ts +137 -0
- package/dist/styles-shared.js +124 -0
- package/dist/styles.d.ts +102 -0
- package/dist/styles.js +730 -0
- package/dist/stylesheet-versions.d.ts +17 -0
- package/dist/stylesheet-versions.js +43 -0
- package/dist/tester.html +10 -0
- package/dist/use-cache.d.ts +15 -0
- package/dist/use-cache.js +93 -0
- package/dist/vitest-version.d.ts +6 -0
- package/dist/vitest-version.js +12 -0
- package/package.json +125 -3
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { registry } from "./registry.js";
|
|
2
|
+
import { restoreIncrementalCache } from "./cache.js";
|
|
3
|
+
import { MockedResponse, fromNodeOutgoingHttpHeaders, isDynamicServerError, toNodeOutgoingHttpHeaders } from "./compat/ssr/node-server.js";
|
|
4
|
+
import { Readable } from "@kasperpeulen/vitest-plugin-nextjs/node/stream";
|
|
5
|
+
//#region src/node-server.ts
|
|
6
|
+
const nativeSetTimeout = globalThis.setTimeout;
|
|
7
|
+
const leftBeforeSent = () => new DOMException("The page was left before the server had sent it.", "AbortError");
|
|
8
|
+
/** The `http.IncomingMessage` of a request: a stream of its body, with what Next reads of it. */
|
|
9
|
+
function createNodeRequest(request) {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
const { body } = request;
|
|
12
|
+
const stream = body instanceof Uint8Array ? Readable.from([body]) : body && Readable.fromWeb(body);
|
|
13
|
+
return Object.assign(stream || Readable.from([]), {
|
|
14
|
+
method: request.method,
|
|
15
|
+
url: url.pathname + url.search,
|
|
16
|
+
headers: {
|
|
17
|
+
host: url.host,
|
|
18
|
+
...toNodeOutgoingHttpHeaders(request.headers)
|
|
19
|
+
},
|
|
20
|
+
httpVersion: "1.1",
|
|
21
|
+
socket: { encrypted: url.protocol === "https:" }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const writings = /* @__PURE__ */ new WeakMap();
|
|
25
|
+
/** The `Writing` of a response of `handleRequest()` with a body. */
|
|
26
|
+
const writingOf = (response) => writings.get(response);
|
|
27
|
+
/**
|
|
28
|
+
* The `http.ServerResponse` of a request, which is Next's own stand-in for
|
|
29
|
+
* one, and the `Response` the browser gets of it: once its head is there, while
|
|
30
|
+
* Next goes on to write the body.
|
|
31
|
+
*
|
|
32
|
+
* A request that is left before its head has no response. It lasts until
|
|
33
|
+
* Next's handler is done with it, which can be much later, when the data it
|
|
34
|
+
* waited for comes: its stores have to be there until then.
|
|
35
|
+
*/
|
|
36
|
+
function createNodeResponse(request) {
|
|
37
|
+
let controller;
|
|
38
|
+
let headSent = false;
|
|
39
|
+
let wrote = false;
|
|
40
|
+
let finished = false;
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
let sendHead;
|
|
43
|
+
const head = new Promise((resolve) => sendHead = resolve).then(() => headSent = true);
|
|
44
|
+
const encoder = new TextEncoder();
|
|
45
|
+
let ended;
|
|
46
|
+
const bodyWritten = new Promise((resolve) => ended = resolve);
|
|
47
|
+
const body = new ReadableStream({
|
|
48
|
+
start: (c) => void (controller = c),
|
|
49
|
+
cancel() {
|
|
50
|
+
cancelled = true;
|
|
51
|
+
res.destroy();
|
|
52
|
+
ended();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const stop = () => {
|
|
56
|
+
cancelled = true;
|
|
57
|
+
controller.error(AbortSignal.abort().reason);
|
|
58
|
+
res.destroy();
|
|
59
|
+
ended();
|
|
60
|
+
};
|
|
61
|
+
const res = new MockedResponse({ resWriter(chunk) {
|
|
62
|
+
if (res.destroyed) return false;
|
|
63
|
+
sendHead();
|
|
64
|
+
wrote = headSent = res.headersSent = true;
|
|
65
|
+
controller.enqueue(typeof chunk === "string" ? encoder.encode(chunk) : new Uint8Array(chunk));
|
|
66
|
+
return true;
|
|
67
|
+
} });
|
|
68
|
+
const appendHeader = res.appendHeader.bind(res);
|
|
69
|
+
res.appendHeader = (name, value) => name.toLowerCase() === "location" && res.getHeader(name) === value ? res : appendHeader(name, value);
|
|
70
|
+
const end = res.end.bind(res);
|
|
71
|
+
res.end = ((...args) => {
|
|
72
|
+
end(...args);
|
|
73
|
+
if (res.destroyed) return res;
|
|
74
|
+
if (!wrote) finished = true;
|
|
75
|
+
headSent = res.headersSent = true;
|
|
76
|
+
sendHead();
|
|
77
|
+
return res;
|
|
78
|
+
});
|
|
79
|
+
res._writableState.autoDestroy = false;
|
|
80
|
+
res.on("finish", () => {
|
|
81
|
+
finished = true;
|
|
82
|
+
Object.defineProperty(res, "writableFinished", {
|
|
83
|
+
value: true,
|
|
84
|
+
configurable: true
|
|
85
|
+
});
|
|
86
|
+
controller.close();
|
|
87
|
+
ended();
|
|
88
|
+
nativeSetTimeout(() => res.destroy());
|
|
89
|
+
});
|
|
90
|
+
let failure;
|
|
91
|
+
res.on("error", (error) => failure ??= error);
|
|
92
|
+
res.on("close", () => {
|
|
93
|
+
if (!headSent || finished || cancelled) return;
|
|
94
|
+
controller.error(failure ?? leftBeforeSent());
|
|
95
|
+
ended();
|
|
96
|
+
});
|
|
97
|
+
res.hasStreamed.catch(() => {});
|
|
98
|
+
const { signal } = request;
|
|
99
|
+
const leave = () => res.destroy();
|
|
100
|
+
signal?.addEventListener("abort", leave, { once: true });
|
|
101
|
+
res.once("close", () => signal?.removeEventListener("abort", leave));
|
|
102
|
+
const response = (written) => {
|
|
103
|
+
const sent = head.then(() => {
|
|
104
|
+
const empty = finished && !wrote || [
|
|
105
|
+
101,
|
|
106
|
+
204,
|
|
107
|
+
205,
|
|
108
|
+
304
|
|
109
|
+
].includes(res.statusCode);
|
|
110
|
+
const answer = new registry.Response(empty ? null : body, {
|
|
111
|
+
status: res.statusCode,
|
|
112
|
+
statusText: res.statusMessage,
|
|
113
|
+
headers: fromNodeOutgoingHttpHeaders(res.getHeaders())
|
|
114
|
+
});
|
|
115
|
+
if (!empty) writings.set(answer, {
|
|
116
|
+
written: bodyWritten,
|
|
117
|
+
stop
|
|
118
|
+
});
|
|
119
|
+
return answer;
|
|
120
|
+
});
|
|
121
|
+
const settled = written.then(() => {
|
|
122
|
+
if (headSent) return sent;
|
|
123
|
+
res.destroy();
|
|
124
|
+
throw failure ?? leftBeforeSent();
|
|
125
|
+
}, (error) => {
|
|
126
|
+
if (!headSent) {
|
|
127
|
+
res.destroy();
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
res.destroy(error ?? /* @__PURE__ */ new Error("The request handler failed."));
|
|
131
|
+
return sent;
|
|
132
|
+
});
|
|
133
|
+
return Promise.race([sent, settled]);
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
res,
|
|
137
|
+
response
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const staticToDynamic = "Page changed from static to dynamic at runtime ";
|
|
141
|
+
const isPrerenderBailout = (error) => isDynamicServerError(error) || error instanceof Error && error.message.startsWith(staticToDynamic);
|
|
142
|
+
const prerenders = [];
|
|
143
|
+
const routerServerContexts = Symbol.for("@next/router-server-methods");
|
|
144
|
+
globalThis[routerServerContexts] = { "": { logErrorWithOriginalStack(error) {
|
|
145
|
+
const prerender = prerenders.at(-1);
|
|
146
|
+
if (!isPrerenderBailout(error)) console.error(error);
|
|
147
|
+
else if (prerender) prerender.bailouts++;
|
|
148
|
+
} } };
|
|
149
|
+
/**
|
|
150
|
+
* Counts the bail-outs that Next's handler reports for the prerender that
|
|
151
|
+
* starts now: also those it answers with its error page itself, where its
|
|
152
|
+
* render of a page recovers from the error (`app-page-runtime.js:1133-1224`).
|
|
153
|
+
* The function it returns ends the count and answers it, once: a count that
|
|
154
|
+
* stays would get the bail-outs of the prerenders after it.
|
|
155
|
+
*/
|
|
156
|
+
function countBailouts() {
|
|
157
|
+
const prerender = { bailouts: 0 };
|
|
158
|
+
prerenders.push(prerender);
|
|
159
|
+
return () => {
|
|
160
|
+
const at = prerenders.lastIndexOf(prerender);
|
|
161
|
+
if (at !== -1) prerenders.splice(at, 1);
|
|
162
|
+
return prerender.bailouts;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function requestMetaOf(request, routed = {}) {
|
|
166
|
+
return {
|
|
167
|
+
...routed.params && { params: routed.params },
|
|
168
|
+
...routed.query && { query: routed.query },
|
|
169
|
+
initURL: request.url,
|
|
170
|
+
initProtocol: new URL(request.url).protocol.slice(0, -1)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* One request, by the request handler Next's build makes for its route:
|
|
175
|
+
* `templates/app-page-runtime` for a page and `templates/app-route` for a
|
|
176
|
+
* route handler, as `next start` calls them.
|
|
177
|
+
*/
|
|
178
|
+
async function handleRequest(request, context, handler, routed, { statusCode, headers } = {}) {
|
|
179
|
+
const { res, response } = createNodeResponse(request);
|
|
180
|
+
if (statusCode !== void 0) res.statusCode = statusCode;
|
|
181
|
+
headers?.forEach((value, name) => {
|
|
182
|
+
if (name !== "set-cookie") res.setHeader(name, value);
|
|
183
|
+
});
|
|
184
|
+
return response(handler(createNodeRequest(request), res, {
|
|
185
|
+
waitUntil: context.waitUntil,
|
|
186
|
+
requestMeta: {
|
|
187
|
+
...requestMetaOf(request, routed),
|
|
188
|
+
incrementalCache: globalThis.__incrementalCache
|
|
189
|
+
}
|
|
190
|
+
}).finally(restoreIncrementalCache));
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
export { countBailouts, handleRequest, isPrerenderBailout, writingOf };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/not-found-page.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A script that loads the host's document of the last page of a static build
|
|
6
|
+
* that this browser opened, when the expression `when` holds: in place of a
|
|
7
|
+
* document that the static host has at the URL of that page. Nothing when no
|
|
8
|
+
* page was opened, or the host is at this URL itself.
|
|
9
|
+
*/
|
|
10
|
+
declare function returnToHostScript(when?: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* An expression of a script for `returnToHostScript()`: whether the host's
|
|
13
|
+
* document of the last page that this tab opened was the tab's own, not in
|
|
14
|
+
* a frame.
|
|
15
|
+
*/
|
|
16
|
+
declare const topLevelHost: string;
|
|
17
|
+
declare function notFoundPagePlugin(environment: string): Plugin;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { notFoundPagePlugin, returnToHostScript, topLevelHost };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { hostKey, topLevelHostKey } from "./host-return-shared.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { getPluginApi } from "@vitejs/plugin-rsc/plugin";
|
|
5
|
+
//#region src/not-found-page.ts
|
|
6
|
+
/**
|
|
7
|
+
* A script that loads the host's document of the last page of a static build
|
|
8
|
+
* that this browser opened, when the expression `when` holds: in place of a
|
|
9
|
+
* document that the static host has at the URL of that page. Nothing when no
|
|
10
|
+
* page was opened, or the host is at this URL itself.
|
|
11
|
+
*/
|
|
12
|
+
function returnToHostScript(when = "true") {
|
|
13
|
+
const key = JSON.stringify(hostKey);
|
|
14
|
+
return `(function () {
|
|
15
|
+
if (!(${when})) return;
|
|
16
|
+
var host;
|
|
17
|
+
try {
|
|
18
|
+
host = sessionStorage.getItem(${key}) || localStorage.getItem(${key});
|
|
19
|
+
} catch (error) {}
|
|
20
|
+
if (!host) return;
|
|
21
|
+
var url = new URL(host, location.href);
|
|
22
|
+
if (url.origin !== location.origin || url.pathname === location.pathname) return;
|
|
23
|
+
// What the document started to load is not needed.
|
|
24
|
+
window.stop();
|
|
25
|
+
location.replace(url.href);
|
|
26
|
+
})();`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An expression of a script for `returnToHostScript()`: whether the host's
|
|
30
|
+
* document of the last page that this tab opened was the tab's own, not in
|
|
31
|
+
* a frame.
|
|
32
|
+
*/
|
|
33
|
+
const topLevelHost = `(function () {
|
|
34
|
+
try {
|
|
35
|
+
return sessionStorage.getItem(${JSON.stringify(topLevelHostKey)}) === "1";
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
})()`;
|
|
40
|
+
const notFoundPage = `<!doctype html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8" />
|
|
44
|
+
<meta name="robots" content="noindex" />
|
|
45
|
+
<title>404: This page could not be found.</title>
|
|
46
|
+
<script>
|
|
47
|
+
${returnToHostScript()}
|
|
48
|
+
<\/script>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<p>404: This page could not be found.</p>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
`;
|
|
55
|
+
function notFoundPagePlugin(environment) {
|
|
56
|
+
return {
|
|
57
|
+
name: "vitest-plugin-rsc:next-not-found-page",
|
|
58
|
+
apply: "build",
|
|
59
|
+
generateBundle(_options, bundle) {
|
|
60
|
+
if (this.environment.name !== environment) return;
|
|
61
|
+
if (getPluginApi(this.environment.config)?.manager.isScanBuild) return;
|
|
62
|
+
const { publicDir } = this.environment.config;
|
|
63
|
+
if ("404.html" in bundle) return;
|
|
64
|
+
if (publicDir && fs.existsSync(path.join(publicDir, "404.html"))) return;
|
|
65
|
+
this.emitFile({
|
|
66
|
+
type: "asset",
|
|
67
|
+
fileName: "404.html",
|
|
68
|
+
source: notFoundPage
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { notFoundPagePlugin, returnToHostScript, topLevelHost };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextLayer, NextProject } from "./project.js";
|
|
2
|
+
|
|
3
|
+
//#region src/optimizer-cache-key.d.ts
|
|
4
|
+
type OptimizerInputs = Pick<NextProject, "aliases" | "appAliases" | "dependencyCompileOptions" | "newExports" | "version">;
|
|
5
|
+
/**
|
|
6
|
+
* What the project gives the optimizer's plugins of a layer: Next's alias
|
|
7
|
+
* table and the app's, which the resolver picks from by the importer, and the
|
|
8
|
+
* options of Next's transform for a dependency. The tables in their order,
|
|
9
|
+
* not sorted: the resolver takes the first entry that matches, and a prefix
|
|
10
|
+
* like `next` can match where a later one would. The order is the code's,
|
|
11
|
+
* the same for every run. And the stubs of the stand-ins, of the version of
|
|
12
|
+
* Next.
|
|
13
|
+
*/
|
|
14
|
+
declare function projectKey(project: OptimizerInputs, layer: NextLayer): string;
|
|
15
|
+
/**
|
|
16
|
+
* The code of a directory of the plugin: the contents of every file of it.
|
|
17
|
+
* The contents, not the version, which a build of this repository keeps, and
|
|
18
|
+
* not the times of the files, which every build changes. All of the code,
|
|
19
|
+
* not just the modules that compile a dependency: a bundle of `dist` mixes
|
|
20
|
+
* modules, and what those import is just as much the compile.
|
|
21
|
+
*/
|
|
22
|
+
declare function codeKey(dir: string): string;
|
|
23
|
+
/** For the `define` of the optimizer of a layer, which Vite keys its cache on. */
|
|
24
|
+
declare function optimizerCacheKey(project: OptimizerInputs, layer: NextLayer): Record<string, string>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { codeKey, optimizerCacheKey, projectKey };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/optimizer-cache-key.ts
|
|
7
|
+
const hash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
8
|
+
/**
|
|
9
|
+
* What the project gives the optimizer's plugins of a layer: Next's alias
|
|
10
|
+
* table and the app's, which the resolver picks from by the importer, and the
|
|
11
|
+
* options of Next's transform for a dependency. The tables in their order,
|
|
12
|
+
* not sorted: the resolver takes the first entry that matches, and a prefix
|
|
13
|
+
* like `next` can match where a later one would. The order is the code's,
|
|
14
|
+
* the same for every run. And the stubs of the stand-ins, of the version of
|
|
15
|
+
* Next.
|
|
16
|
+
*/
|
|
17
|
+
function projectKey(project, layer) {
|
|
18
|
+
return hash(JSON.stringify([
|
|
19
|
+
Object.entries(project.aliases[layer]),
|
|
20
|
+
Object.entries(project.appAliases[layer]),
|
|
21
|
+
project.dependencyCompileOptions[layer],
|
|
22
|
+
project.newExports,
|
|
23
|
+
project.version
|
|
24
|
+
]));
|
|
25
|
+
}
|
|
26
|
+
const isCode = (file) => /\.[cm]?[jt]sx?$/.test(file) && !/\.test\.[cm]?[jt]sx?$/.test(file) && !/\.d\.[cm]?ts$/.test(file);
|
|
27
|
+
/**
|
|
28
|
+
* The code of a directory of the plugin: the contents of every file of it.
|
|
29
|
+
* The contents, not the version, which a build of this repository keeps, and
|
|
30
|
+
* not the times of the files, which every build changes. All of the code,
|
|
31
|
+
* not just the modules that compile a dependency: a bundle of `dist` mixes
|
|
32
|
+
* modules, and what those import is just as much the compile.
|
|
33
|
+
*/
|
|
34
|
+
function codeKey(dir) {
|
|
35
|
+
const content = createHash("sha256");
|
|
36
|
+
const files = fs.readdirSync(dir, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
encoding: "utf8"
|
|
39
|
+
}).map((file) => file.split(path.sep).join("/")).filter(isCode).sort();
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const full = path.join(dir, file);
|
|
42
|
+
if (!fs.statSync(full).isFile()) continue;
|
|
43
|
+
content.update(`${file}\0`).update(fs.readFileSync(full)).update("\0");
|
|
44
|
+
}
|
|
45
|
+
return content.digest("hex").slice(0, 16);
|
|
46
|
+
}
|
|
47
|
+
function ownCodeDirs() {
|
|
48
|
+
const file = fileURLToPath(import.meta.url);
|
|
49
|
+
for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) {
|
|
50
|
+
const manifest = path.join(dir, "package.json");
|
|
51
|
+
if (!fs.existsSync(manifest)) continue;
|
|
52
|
+
const { name } = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
53
|
+
if (name !== "@kasperpeulen/vitest-plugin-nextjs") return [];
|
|
54
|
+
const core = path.dirname(createRequire(import.meta.url).resolve("@kasperpeulen/vitest-plugin-rsc/package.json"));
|
|
55
|
+
return [dir, core].flatMap((pkg) => ["src", "dist"].map((code) => path.join(pkg, code))).filter((code) => fs.statSync(code, { throwIfNoEntry: false })?.isDirectory());
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
let ownCodeKey;
|
|
60
|
+
/** For the `define` of the optimizer of a layer, which Vite keys its cache on. */
|
|
61
|
+
function optimizerCacheKey(project, layer) {
|
|
62
|
+
if (ownCodeKey === void 0) try {
|
|
63
|
+
ownCodeKey = hash(ownCodeDirs().map((dir) => `${path.basename(dir)}:${codeKey(dir)}`).join(","));
|
|
64
|
+
} catch {
|
|
65
|
+
ownCodeKey = "";
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
__vitest_plugin_rsc_project__: JSON.stringify(projectKey(project, layer)),
|
|
69
|
+
__vitest_plugin_rsc_code__: JSON.stringify(ownCodeKey)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { codeKey, optimizerCacheKey, projectKey };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { addOwnPackage } from "@kasperpeulen/vitest-plugin-rsc/internal/own-packages";
|
|
3
|
+
//#region src/own-package.ts
|
|
4
|
+
addOwnPackage("@kasperpeulen/vitest-plugin-nextjs", fileURLToPath(new URL("./", import.meta.url)));
|
|
5
|
+
//#endregion
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/page-store.d.ts
|
|
2
|
+
type Cookie = {
|
|
3
|
+
value: string;
|
|
4
|
+
httpOnly: boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* What the browser's requests to the app carry, besides what a browser adds
|
|
8
|
+
* itself. One for the document, from what the test opened until the test
|
|
9
|
+
* ends: `cleanup()` makes a new one.
|
|
10
|
+
*/
|
|
11
|
+
declare class PageStore {
|
|
12
|
+
#private;
|
|
13
|
+
/** The `headers` of what the test opened, until it opens something else. */
|
|
14
|
+
headers: Headers;
|
|
15
|
+
/** The app's cookies, by name, with the values the server wrote. */
|
|
16
|
+
readonly cookies: Map<string, Cookie>;
|
|
17
|
+
/** Sets the cookies of a `cookie` header, as a `Set-Cookie` of each would. */
|
|
18
|
+
seed(cookie: string): void;
|
|
19
|
+
/** Sets and deletes cookies by the `Set-Cookie` headers of a response of the app. */
|
|
20
|
+
receive(setCookies: Iterable<string>): void;
|
|
21
|
+
/**
|
|
22
|
+
* Sets or deletes a cookie as a script of the page does, with
|
|
23
|
+
* `document.cookie`. As in a browser, a script cannot write an `HttpOnly`
|
|
24
|
+
* cookie, nor change or delete one.
|
|
25
|
+
*/
|
|
26
|
+
write(setCookie: string): void;
|
|
27
|
+
/** The `cookie` header of a request: empty without cookies. */
|
|
28
|
+
cookieHeader(): string;
|
|
29
|
+
/** What `document.cookie` has: the cookies that are not `HttpOnly`. */
|
|
30
|
+
documentCookie(): string;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { PageStore };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/page-store.ts
|
|
2
|
+
/** A name and a value: of a `cookie` header, or of a `Set-Cookie` before its first `;`. */
|
|
3
|
+
function pairOf(text) {
|
|
4
|
+
const at = text.indexOf("=");
|
|
5
|
+
return at === -1 ? ["", text.trim()] : [text.slice(0, at).trim(), text.slice(at + 1).trim()];
|
|
6
|
+
}
|
|
7
|
+
/** The name of an attribute of a `Set-Cookie`, in lower case, and its value. */
|
|
8
|
+
function attributeOf(text) {
|
|
9
|
+
const at = text.indexOf("=");
|
|
10
|
+
return at === -1 ? [text.trim().toLowerCase(), ""] : [text.slice(0, at).trim().toLowerCase(), text.slice(at + 1).trim()];
|
|
11
|
+
}
|
|
12
|
+
function expiresAt(value) {
|
|
13
|
+
if (!/\d{1,2}:\d{1,2}:\d{1,2}/.test(value)) return void 0;
|
|
14
|
+
if (!/jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec/i.test(value)) return void 0;
|
|
15
|
+
const time = Date.parse(value);
|
|
16
|
+
return Number.isNaN(time) ? void 0 : time;
|
|
17
|
+
}
|
|
18
|
+
function deletes(attributes) {
|
|
19
|
+
let maxAge;
|
|
20
|
+
let expires;
|
|
21
|
+
for (const [name, value] of attributes.map(attributeOf)) {
|
|
22
|
+
if (name === "max-age" && /^-?\d+$/.test(value)) maxAge = Number(value);
|
|
23
|
+
if (name === "expires") expires = expiresAt(value) ?? expires;
|
|
24
|
+
}
|
|
25
|
+
if (maxAge !== void 0) return maxAge <= 0;
|
|
26
|
+
return expires !== void 0 && expires <= Date.now();
|
|
27
|
+
}
|
|
28
|
+
/** The cookies as a `cookie` header, or as `document.cookie` has them. */
|
|
29
|
+
function serialize(cookies) {
|
|
30
|
+
return Array.from(cookies, ([name, { value }]) => name ? `${name}=${value}` : value).join("; ");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What the browser's requests to the app carry, besides what a browser adds
|
|
34
|
+
* itself. One for the document, from what the test opened until the test
|
|
35
|
+
* ends: `cleanup()` makes a new one.
|
|
36
|
+
*/
|
|
37
|
+
var PageStore = class {
|
|
38
|
+
/** The `headers` of what the test opened, until it opens something else. */
|
|
39
|
+
headers = new Headers();
|
|
40
|
+
/** The app's cookies, by name, with the values the server wrote. */
|
|
41
|
+
cookies = /* @__PURE__ */ new Map();
|
|
42
|
+
/** Sets the cookies of a `cookie` header, as a `Set-Cookie` of each would. */
|
|
43
|
+
seed(cookie) {
|
|
44
|
+
this.receive(cookie.split(";"));
|
|
45
|
+
}
|
|
46
|
+
/** Sets and deletes cookies by the `Set-Cookie` headers of a response of the app. */
|
|
47
|
+
receive(setCookies) {
|
|
48
|
+
for (const setCookie of setCookies) this.#set(setCookie, false);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sets or deletes a cookie as a script of the page does, with
|
|
52
|
+
* `document.cookie`. As in a browser, a script cannot write an `HttpOnly`
|
|
53
|
+
* cookie, nor change or delete one.
|
|
54
|
+
*/
|
|
55
|
+
write(setCookie) {
|
|
56
|
+
this.#set(setCookie, true);
|
|
57
|
+
}
|
|
58
|
+
/** The `cookie` header of a request: empty without cookies. */
|
|
59
|
+
cookieHeader() {
|
|
60
|
+
return serialize(this.cookies);
|
|
61
|
+
}
|
|
62
|
+
/** What `document.cookie` has: the cookies that are not `HttpOnly`. */
|
|
63
|
+
documentCookie() {
|
|
64
|
+
return serialize(Array.from(this.cookies).filter(([, { httpOnly }]) => !httpOnly));
|
|
65
|
+
}
|
|
66
|
+
#set(setCookie, byScript) {
|
|
67
|
+
const [pair = "", ...attributes] = setCookie.split(";");
|
|
68
|
+
const [name, value] = pairOf(pair);
|
|
69
|
+
if (!name && !value) return;
|
|
70
|
+
const httpOnly = attributes.some((attribute) => attributeOf(attribute)[0] === "httponly");
|
|
71
|
+
if (byScript && (httpOnly || this.cookies.get(name)?.httpOnly)) return;
|
|
72
|
+
if (deletes(attributes)) this.cookies.delete(name);
|
|
73
|
+
else this.cookies.set(name, {
|
|
74
|
+
value,
|
|
75
|
+
httpOnly
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
export { PageStore };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/page-timers.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Those uses, to the module runner's. Also for the dependency optimizer of
|
|
6
|
+
* the browser layer, by its `transform.define`: the bundler replaces them in
|
|
7
|
+
* every module of the pre-bundle, natively and in parallel, and the result
|
|
8
|
+
* stays on disk, where the plugin below would compile each pre-bundled file
|
|
9
|
+
* again in every run. None of them is a file of the host, and the packages
|
|
10
|
+
* of the host are not in it (`vitest-plugin-rsc:next-host-packages`).
|
|
11
|
+
*/
|
|
12
|
+
declare const pageTimersDefine: {
|
|
13
|
+
[k: string]: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* For the modules of the browser layer, by the name of its environment, but
|
|
17
|
+
* those of a file that `isHostFile()`, and the pre-bundled dependencies,
|
|
18
|
+
* which have `pageTimersDefine`.
|
|
19
|
+
*/
|
|
20
|
+
declare function pageTimersPlugin(environment: string, isHostFile: (file: string) => boolean): Plugin;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { pageTimersDefine, pageTimersPlugin };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { fileOfModule } from "./client-files.js";
|
|
2
|
+
import { runnerGlobalNames, timersKey } from "@kasperpeulen/vitest-plugin-rsc/internal/timers";
|
|
3
|
+
import { normalizePath, transformWithOxc } from "vite";
|
|
4
|
+
import { ssrImportMetaKey } from "vite/module-runner";
|
|
5
|
+
//#region src/page-timers.ts
|
|
6
|
+
/**
|
|
7
|
+
* Those uses, to the module runner's. Also for the dependency optimizer of
|
|
8
|
+
* the browser layer, by its `transform.define`: the bundler replaces them in
|
|
9
|
+
* every module of the pre-bundle, natively and in parallel, and the result
|
|
10
|
+
* stays on disk, where the plugin below would compile each pre-bundled file
|
|
11
|
+
* again in every run. None of them is a file of the host, and the packages
|
|
12
|
+
* of the host are not in it (`vitest-plugin-rsc:next-host-packages`).
|
|
13
|
+
*/
|
|
14
|
+
const pageTimersDefine = Object.fromEntries(runnerGlobalNames.map((name) => [name, `${ssrImportMetaKey}.${timersKey}.${name}`]));
|
|
15
|
+
const mentionsTimer = new RegExp(`\\b(?:${runnerGlobalNames.join("|")})\\b`);
|
|
16
|
+
/**
|
|
17
|
+
* For the modules of the browser layer, by the name of its environment, but
|
|
18
|
+
* those of a file that `isHostFile()`, and the pre-bundled dependencies,
|
|
19
|
+
* which have `pageTimersDefine`.
|
|
20
|
+
*/
|
|
21
|
+
function pageTimersPlugin(environment, isHostFile) {
|
|
22
|
+
return {
|
|
23
|
+
name: "vitest-plugin-rsc:next-page-timers",
|
|
24
|
+
enforce: "post",
|
|
25
|
+
applyToEnvironment: ({ name }) => name === environment,
|
|
26
|
+
transform: {
|
|
27
|
+
filter: { code: mentionsTimer },
|
|
28
|
+
async handler(code, id) {
|
|
29
|
+
const { environment } = this;
|
|
30
|
+
if (environment.mode === "dev" && environment.depsOptimizer?.isOptimizedDepFile(id)) return;
|
|
31
|
+
const file = fileOfModule(id);
|
|
32
|
+
if (file !== void 0 && isHostFile(file)) return;
|
|
33
|
+
try {
|
|
34
|
+
return await transformWithOxc(code, normalizePath(id), {
|
|
35
|
+
lang: "js",
|
|
36
|
+
define: pageTimersDefine
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const reason = String(error instanceof Error ? error.message : error);
|
|
40
|
+
this.warn(`vitest-plugin-rsc: the timers of ${id} keep running after its page is left, it does not parse as JavaScript. ${reason.replace(/\s+/g, " ")}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { pageTimersDefine, pageTimersPlugin };
|