Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds a new "preview" CLI command and creates a centralized preview server infrastructure for Nitro. It introduces a new Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@src/preview.ts`:
- Line 143: The waitForPort call in src/preview.ts currently uses retries: 20
and delay: 500 (≈10s) which is too short for Wrangler/Miniflare; update the call
to use a longer timeout or make it configurable: introduce a configurable
timeout (env var or option) such as PREVIEW_PORT_TIMEOUT_MS or
PREVIEW_PORT_RETRIES and compute either retries/delay to achieve a sensible
default (e.g., 60s+) and replace await waitForPort(randomPort, { retries: 20,
delay: 500 }) with await waitForPort(randomPort, { retries: computedRetries,
delay: computedDelay }) so callers/CI can override the wait time; ensure the new
config is documented where preview startup is invoked and default fallback is
applied when the env/option is absent.
- Around line 36-46: The loaded .env entries from loadPreviewDotEnv are not
being applied to process.env for in-process previews, so modify the preview
startup to merge those entries into process.env before launching the in-process
server path: after retrieving dotEnvEntries (the variable returned by
loadPreviewDotEnv), iterate over dotEnvEntries and assign process.env[key] = val
for each pair (but avoid overriding existing platform-managed envs if needed),
ensuring this happens before calling runPreviewCommand and before the
non-Cloudflare in-process branch that loads the server entry; keep the
Cloudflare/subprocess flow unchanged since those vars are already passed to the
subprocess.
- Line 107: The current naive split of opts.command into arg0 and args (const
[arg0, ...args] = opts.command.split(" ")) will break on quoted paths/arguments
with spaces; replace it with a proper shell-style argument parser (e.g., use
shell-quote.parse or string-argv) so quoted segments stay intact: parse
opts.command into an array and then assign const [arg0, ...args] from that
parsed array (update the code around opts.command, arg0, and args in
preview.ts).
- Around line 147-150: The current fetch handler returns fetch(url + req.url),
which breaks because ServerRequest.req.url is a full URL and also discards
method/headers/body; to fix, build the target URL by taking the incoming
request's path + search (e.g., new URL(req.url).pathname + new
URL(req.url).search) and resolve that against your base `url` variable, then
create a new Request using that resolved URL and forward req.method,
req.headers, and the body (where present) before calling `fetch`; update the
async fetch(req: ServerRequest) implementation to construct and pass this new
Request so headers, method, and body are preserved.
🧹 Nitpick comments (1)
src/preview.ts (1)
131-135: Consider also handlingSIGTERM.
SIGTERMis the standard signal sent by process managers and container runtimes to request graceful shutdown, but it's not listed here. If the parent receivesSIGTERM, the child process will be orphaned.Proposed fix
- for (const sig of ["SIGINT", "SIGHUP"] as const) { + for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"] as const) { process.once(sig, () => { killChild(sig); }); }
commit: |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/unit/proxy.test.ts (1)
3-3:⚠️ Potential issue | 🟡 MinorUse
patheinstead ofnode:path.🛠️ Proposed fix
-import { join } from "node:path"; +import { join } from "pathe";As per coding guidelines: "Use
pathefor cross-platform path operations instead ofnode:path".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unit/proxy.test.ts` at line 3, Replace the node:path import with pathe: change the import of join from "node:path" to import { join } from "pathe" and keep all uses of join in this file (e.g., in test/unit/proxy.test.ts) unchanged so path operations become cross-platform via the pathe library.
🧹 Nitpick comments (1)
src/runner/proxy.ts (1)
142-155: Optional: remove the redundantduplexfield fromtoInit.Lines 80–82 of
fetchAddressunconditionally setduplex = "half"wheneverinit.bodyis truthy, regardless of howinitwas assembled. Theduplex: init.body ? "half" : undefinedinsidetoInit'sRequestbranch is therefore always either a no-op (body present → overwritten moments later) or introduces an explicitundefinedkey (no body →fetchAddresswon't set it anyway). Removing it keepstoInit's responsibility clear — normalise aRequesttoRequestInit-shaped properties; letfetchAddressownduplex.♻️ Proposed cleanup
function toInit(init?: RequestInit | Request): RequestInit | undefined { if (!init) { return undefined; } if (init instanceof Request) { return { method: init.method, headers: init.headers, body: init.body, - duplex: init.body ? "half" : undefined, } as RequestInit; } return init; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runner/proxy.ts` around lines 142 - 155, Remove the redundant duplex field from the Request branch inside toInit so it only normalizes method, headers, and body when given a Request; leave responsibility for setting duplex to fetchAddress (which already sets duplex = "half" based on init.body), ensuring fetchAddress still inspects the returned RequestInit.body to set duplex as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/preview.ts`:
- Line 112: Replace the raw console.log("") call with the project logging
utility: change the console.log("") expression to use consola (e.g.,
consola.info("") or consola.log("")) and ensure consola is imported (import
consola from 'consola') or use the existing consola instance in the module;
update the single occurrence of console.log("") to use consola so it follows the
coding guideline.
- Around line 132-136: The signal-forwarding loop in preview.ts registers
handlers for "SIGINT" and "SIGHUP" but omits "SIGTERM", so the child process may
be orphaned; update the for-loop that calls process.once(...) (the array of
signals) to include "SIGTERM" alongside "SIGINT" and "SIGHUP" so killChild(sig)
is invoked for SIGTERM as well, ensuring graceful shutdown propagation to the
child process.
---
Outside diff comments:
In `@test/unit/proxy.test.ts`:
- Line 3: Replace the node:path import with pathe: change the import of join
from "node:path" to import { join } from "pathe" and keep all uses of join in
this file (e.g., in test/unit/proxy.test.ts) unchanged so path operations become
cross-platform via the pathe library.
---
Duplicate comments:
In `@src/preview.ts`:
- Line 108: The current naive split in the preview command parsing (const [arg0,
...args] = opts.command.split(" ")) fails on quoted arguments or paths with
spaces; replace this with a proper shell-argument parser (e.g., use string-argv
or shell-quote) to parse opts.command into an argv array, then assign arg0 =
argv[0] and args = argv.slice(1); update the import to bring in the chosen
parser and adjust any downstream uses that expect separate arg0/args.
- Around line 36-47: dotEnvEntries loaded by loadPreviewDotEnv are never written
into the current process environment before the in-process preview path runs;
update process.env with those entries immediately after dotEnvEntries is
obtained (before the child-vs-in-process branch) by iterating dotEnvEntries and
assigning process.env[key] = val for each entry so the server entry loaded by
the in-process path (the module loaded at line 66) sees the variables; keep this
assignment minimal and colocated near the dotEnvEntries usage to avoid
duplicating logic with the child-process branch.
- Line 144: The current waitForPort call uses retries: 20 and delay: 500 (total
~10s) which is too short for Wrangler startup; update the
waitForPort(randomPort, { retries: 20, delay: 500, host: "localhost" })
invocation to increase the timeout (for example increase retries to ~120, or
delay to 1000 and retries to ~60, or replace with a configurable constant like
WAIT_FOR_PORT_RETRIES/WAIT_FOR_PORT_DELAY) so total wait is ~60s+; ensure
randomPort and host remain the same and consider making the timeout values
configurable or using exponential backoff to avoid hardcoding.
---
Nitpick comments:
In `@src/runner/proxy.ts`:
- Around line 142-155: Remove the redundant duplex field from the Request branch
inside toInit so it only normalizes method, headers, and body when given a
Request; leave responsibility for setting duplex to fetchAddress (which already
sets duplex = "half" based on init.body), ensuring fetchAddress still inspects
the returned RequestInit.body to set duplex as before.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/preview.ts (1)
82-88: Avoid theas anycast for thenextcallback.The
() => undefined as anysilences a real type mismatch on theserveStaticnext-handler. A typed alternative avoids suppressing TypeScript checks:♻️ Proposed refactor
- const staticRes: Response | undefined = await staticHandler(req, () => undefined as any); + const staticRes: Response | undefined = await staticHandler(req, () => + undefined as unknown as Response + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/preview.ts` around lines 82 - 88, The code passes an untyped "() => undefined as any" into staticHandler, suppressing TypeScript checks; fix by creating a properly typed no-op next callback using the actual parameter type of staticHandler (e.g., const noopNext: Parameters<typeof staticHandler>[1] = () => undefined;) and then call staticHandler(req, noopNext) inside fetchHandler (keep fetchHandler and originalFetchHandler unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/preview.ts`:
- Around line 65-69: The loop that merges dotEnvEntries into process.env uses a
falsy check (!process.env[key]) which accidentally treats empty strings as unset
and overwrites platform-set empty values; change the guard in the loop that
iterates dotEnvEntries to only set process.env[key] when process.env[key] ===
undefined (i.e., replace the falsy check with a strict undefined check) so
existing empty-string environment variables are preserved; reference the loop
iterating over dotEnvEntries and the process.env assignment to locate and update
the condition.
- Around line 153-162: The proxyFetch call is confusing because proxyFetch({
port: randomPort, host: "localhost" }, targetURL) ignores the hostname in
targetURL; update the fetch implementation to pass only the path+search as the
second argument (e.g., use reqURL.pathname + reqURL.search) or, if you prefer an
absolute string, pass a dummy hostname (e.g., "http://internal" +
reqURL.pathname + reqURL.search) so the intent is clear; change the variables
used in the fetch function (reqURL, targetURL) and the proxyFetch invocation
accordingly to remove the redundant/ignored hostname while keeping the same
port/host routing via randomPort and "localhost".
---
Duplicate comments:
In `@src/preview.ts`:
- Around line 137-141: The current signal forwarding loop in preview.ts only
registers SIGINT and SIGHUP, so SIGTERM sent by process managers won't be
forwarded; update the signal list used in the for loop to include "SIGTERM"
(i.e., for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"] as const)) so
process.once(...) calls will also invoke killChild(sig) for SIGTERM, ensuring
the child process receives the termination signal via the existing killChild
handler.
- Line 113: The code uses naive tokenization via opts.command.split(" ") which
breaks on quoted arguments and paths with spaces; replace this with a proper
shell-style parser (e.g., use a shell-words/shell-quote parser or a small
utility parseCommand) and then destructure the result into const [arg0, ...args]
so that quoted segments are preserved as single arguments; update the usage
around opts.command, the const [arg0, ...args] assignment, and any callers that
rely on this behavior (in src/preview.ts) to use the parsed array instead of
split(" ").
- Line 117: Replace the raw console.log("") call with the project's consola
logger: change the console.log("") invocation to use consola (e.g.,
consola.log("") or consola.info("")) and ensure consola is imported at the top
(import { consola } from 'consola') or the existing logger instance is used if
one exists; update the single raw call in preview.ts where console.log("")
appears to use consola instead.
- Line 149: The current waitForPort(randomPort, { retries: 20, delay: 500, host:
"localhost" }) caps waiting at ~10s which is too short for Wrangler; update the
wait call to allow a longer startup window (e.g. increase retries and/or delay
or use the library's timeout/timeoutMs option if available) so Cloudflare
startup can complete reliably; modify the options passed to waitForPort (the
call using randomPort) to a larger total timeout (e.g. ~60s+) and remove the
TODO comment referencing the wait-for-port issue.
---
Nitpick comments:
In `@src/preview.ts`:
- Around line 82-88: The code passes an untyped "() => undefined as any" into
staticHandler, suppressing TypeScript checks; fix by creating a properly typed
no-op next callback using the actual parameter type of staticHandler (e.g.,
const noopNext: Parameters<typeof staticHandler>[1] = () => undefined;) and then
call staticHandler(req, noopNext) inside fetchHandler (keep fetchHandler and
originalFetchHandler unchanged).
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/build/vite/preview.ts (1)
29-33: MissingSIGTERMin signal handlers — same issue as insrc/preview.ts.This is consistent with the pattern in
runPreviewCommand(lines 149-153 ofsrc/preview.ts), butSIGTERMis the standard signal from process managers (systemd, Kubernetes, Docker). The preview server's close handler won't fire when the parent process receivesSIGTERM.Proposed fix
- for (const sig of ["SIGINT", "SIGHUP"] as const) { + for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"] as const) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/build/vite/preview.ts` around lines 29 - 33, The signal handler loop that registers process.on for ["SIGINT", "SIGHUP"] should also include "SIGTERM" so the preview server's close logic is invoked for termination from process managers; update the array in the loop that registers process.on(...) (the one that calls await preview.close()) to include "SIGTERM" (matching the pattern used by runPreviewCommand) so preview.close() runs on SIGTERM as well.src/cli/commands/preview.ts (1)
42-46: Consider addingSIGTERMfor container/process-manager environments.Only
SIGINTis handled. In production-like preview scenarios (Docker, CI),SIGTERMis the standard graceful shutdown signal. Without it, the server and preview child processes may be orphaned.Proposed fix
- process.on("SIGINT", async () => { - await server.close(); - await preview.close(); - process.exit(0); - }); + for (const sig of ["SIGINT", "SIGTERM"] as const) { + process.on(sig, async () => { + await server.close(); + await preview.close(); + process.exit(0); + }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/commands/preview.ts` around lines 42 - 46, The current code only registers a SIGINT handler (process.on("SIGINT", ...)) so add SIGTERM handling by extracting the cleanup logic into a single async function (e.g., gracefulShutdown or handleShutdown) that awaits server.close() and preview.close() and then calls process.exit(0); register that function for both process.on("SIGINT", ...) and process.on("SIGTERM", ...); also guard the function with a simple boolean (e.g., isShuttingDown) to prevent double-invocation if both signals arrive.src/preview.ts (1)
155-161: Child crash before port is ready causes a silent 10s hang instead of a fast failure.If the spawned process exits with an error before
waitForPortsucceeds, the code waits the full retry window (20 × 500ms = 10s) before timing out. Linking the child's exit event to rejectwaitForPortearly would improve developer experience.Proposed approach
+ const childExited = new Promise<never>((_, reject) => { + child.once("exit", (code) => { + if (code && code !== 0) { + reject(new Error(`Preview server exited with code ${code}`)); + } + }); + }); + - child.once("exit", (code) => { - if (code && code !== 0) { - consola.error(`[nitro] Preview server exited with code ${code}`); - } - }); - - await waitForPort(randomPort, { retries: 20, delay: 500, host: "localhost" }); + await Promise.race([ + waitForPort(randomPort, { retries: 20, delay: 500, host: "localhost" }), + childExited, + ]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/preview.ts` around lines 155 - 161, The current logic waits the full waitForPort retry window even if the spawned process (child) already exited, so change the flow to race waitForPort(randomPort, ...) against the child's exit: create a Promise that listens to child.once('exit', (code) => { ... }) and rejects immediately when code is non-zero (or if the child was killed), then Promise.race that with waitForPort(randomPort, { retries: 20, delay: 500, host: "localhost" }); ensure the exit listener is cleaned up when waitForPort succeeds and the waitForPort promise is cleaned up if the exit handler fires, and surface the child's exit code/error (used in consola.error) on rejection for faster failure feedback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/commands/preview.ts`:
- Around line 21-34: Declare preview with let before calling serve so the fetch
closure doesn't access a const in the TDZ; e.g., move the declaration of preview
(from const preview = ...) to a prior let preview: ReturnType<typeof
startPreview> | undefined (or any), then call serve({ fetch(req) { return
preview!.fetch(req); }, ... }) and after that assign preview = await
startPreview({ rootDir, loader: { srvxServer: server } }); ensuring you update
the type annotation if needed and keep the same identifiers preview, serve,
server, and startPreview.
In `@src/preview.ts`:
- Around line 103-108: The in-process preview returned by loadServerEntry does
not expose an upgrade handler, so WebSocket upgrade events are dropped; modify
the PreviewInstance returned in src/preview.ts to include an async upgrade(req,
socket, head) method that forwards to the underlying entry.upgrade if present
(or wraps proxyUpgrade like spawned previews do), i.e., detect and expose
entry.upgrade from loadServerEntry (or call proxyUpgrade with the same semantics
used by spawned previews) so server.node?.server?.on("upgrade", ...) sees
preview.upgrade and can proxy WebSocket connections; ensure the new upgrade
handler preserves the same parameters and error handling as the existing
proxyUpgrade path.
---
Duplicate comments:
In `@src/preview.ts`:
- Around line 76-81: The loop that copies dotEnvEntries into process.env (inside
the buildInfo.serverEntry conditional) currently treats empty-string values as
unset by checking if (!process.env[key]); change that check to test for
undefined (process.env[key] === undefined) so platform-set empty-string
variables are preserved; update the for-loop handling of dotEnvEntries
accordingly (refer to buildInfo.serverEntry, dotEnvEntries, and
process.env[key]).
- Line 129: Replace the direct console call in src/preview.ts (the
console.log("") statement) with the project's consola logger: import consola if
it's not already imported and use an appropriate consola method (e.g.,
consola.info or consola.log) instead of console.log(""); ensure the import/usage
follows existing project logging conventions so build/dev logs use consola
uniformly.
- Around line 149-153: The signal-forwarding loop in src/preview.ts only listens
for "SIGINT" and "SIGHUP", so the child process isn't forwarded "SIGTERM" (used
by systemd/Kubernetes/Docker); update the array passed to the loop (the
["SIGINT","SIGHUP"] constant) to include "SIGTERM" and ensure the existing
process.once(sig, () => killChild(sig)) behavior is applied to it as well so
killChild receives and forwards "SIGTERM" to the child.
- Around line 120-125: The command parsing in runPreviewCommand currently uses
naive opts.command.split(" "), which breaks on quoted arguments or paths with
spaces; replace this with a shell-aware argument parser (e.g., use a small
helper or a library such as shell-quote or string-argv) to parse opts.command
into argv while preserving quoted segments and escapes, then continue to
destructure into [arg0, ...args] as before so subsequent code using arg0 and
args works correctly.
---
Nitpick comments:
In `@src/build/vite/preview.ts`:
- Around line 29-33: The signal handler loop that registers process.on for
["SIGINT", "SIGHUP"] should also include "SIGTERM" so the preview server's close
logic is invoked for termination from process managers; update the array in the
loop that registers process.on(...) (the one that calls await preview.close())
to include "SIGTERM" (matching the pattern used by runPreviewCommand) so
preview.close() runs on SIGTERM as well.
In `@src/cli/commands/preview.ts`:
- Around line 42-46: The current code only registers a SIGINT handler
(process.on("SIGINT", ...)) so add SIGTERM handling by extracting the cleanup
logic into a single async function (e.g., gracefulShutdown or handleShutdown)
that awaits server.close() and preview.close() and then calls process.exit(0);
register that function for both process.on("SIGINT", ...) and
process.on("SIGTERM", ...); also guard the function with a simple boolean (e.g.,
isShuttingDown) to prevent double-invocation if both signals arrive.
In `@src/preview.ts`:
- Around line 155-161: The current logic waits the full waitForPort retry window
even if the spawned process (child) already exited, so change the flow to race
waitForPort(randomPort, ...) against the child's exit: create a Promise that
listens to child.once('exit', (code) => { ... }) and rejects immediately when
code is non-zero (or if the child was killed), then Promise.race that with
waitForPort(randomPort, { retries: 20, delay: 500, host: "localhost" }); ensure
the exit listener is cleaned up when waitForPort succeeds and the waitForPort
promise is cleaned up if the exit handler fires, and surface the child's exit
code/error (used in consola.error) on rejection for faster failure feedback.
| const server = serve({ | ||
| fetch(req) { | ||
| return preview.fetch(req); | ||
| }, | ||
| middleware: [log()], | ||
| gracefulShutdown: false, | ||
| port: args.port, | ||
| hostname: args.host, | ||
| }); | ||
|
|
||
| const preview = await startPreview({ | ||
| rootDir, | ||
| loader: { srvxServer: server }, | ||
| }); |
There was a problem hiding this comment.
TDZ race: preview is accessed inside fetch before const declaration executes.
preview is declared with const on line 31 but referenced in the fetch closure on line 23. If any request arrives between serve() starting the listener and await startPreview() resolving, accessing preview throws a ReferenceError due to the Temporal Dead Zone.
Since serve() needs to be created first (to pass server into startPreview via loader), use a let variable declared before serve():
Proposed fix
+ let preview: Awaited<ReturnType<typeof startPreview>>;
+
const server = serve({
- fetch(req) {
- return preview.fetch(req);
+ async fetch(req) {
+ if (!preview) {
+ return new Response("Server is starting...", { status: 503 });
+ }
+ return preview!.fetch(req);
},
middleware: [log()],
gracefulShutdown: false,
port: args.port,
hostname: args.host,
});
- const preview = await startPreview({
+ preview = await startPreview({
rootDir,
loader: { srvxServer: server },
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const server = serve({ | |
| fetch(req) { | |
| return preview.fetch(req); | |
| }, | |
| middleware: [log()], | |
| gracefulShutdown: false, | |
| port: args.port, | |
| hostname: args.host, | |
| }); | |
| const preview = await startPreview({ | |
| rootDir, | |
| loader: { srvxServer: server }, | |
| }); | |
| let preview: Awaited<ReturnType<typeof startPreview>>; | |
| const server = serve({ | |
| async fetch(req) { | |
| if (!preview) { | |
| return new Response("Server is starting...", { status: 503 }); | |
| } | |
| return preview!.fetch(req); | |
| }, | |
| middleware: [log()], | |
| gracefulShutdown: false, | |
| port: args.port, | |
| hostname: args.host, | |
| }); | |
| preview = await startPreview({ | |
| rootDir, | |
| loader: { srvxServer: server }, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli/commands/preview.ts` around lines 21 - 34, Declare preview with let
before calling serve so the fetch closure doesn't access a const in the TDZ;
e.g., move the declaration of preview (from const preview = ...) to a prior let
preview: ReturnType<typeof startPreview> | undefined (or any), then call serve({
fetch(req) { return preview!.fetch(req); }, ... }) and after that assign preview
= await startPreview({ rootDir, loader: { srvxServer: server } }); ensuring you
update the type annotation if needed and keep the same identifiers preview,
serve, server, and startPreview.
This PR adds new
nitro previewcommand to preview the production build using srvx/loader or wrangler for cloudflare.(extracted from vite plugin to also reuse for a temporary prerendering solution in next steps)
TODO: