Repository: dai-shi/waku
Pull request #668 (2024-04-24 08:45:27)

SSG: high volume performance
## The problem

HTML payloads of static pages grow linearly with the number of them. This is already evident in the `03_demo` example. Each pokemon has an entry in `__WAKU_ROUTER_PREFETCH__` that points to the identical component. The `/SHOULD_SKIP` entry also grows. With a larger number of pages, the build breaks due to memory issues or node simply not being able to `JSON.stringify` the build config in:
 https://github.com/dai-shi/waku/blob/6550a0d26e7830527c767fcda8eb906aeb0f1266/packages/waku/src/lib/builder/build.ts#L692

Also, when generating the map for `__WAKU_ROUTER_PREFETCH__`, pages are rendered non-concurrently, which means blocking operations can't run in parallel.

## Solutions

1. I switched router prefetching to a pattern instead of path-based model. That way each static page needs only one entry. Not one for each static path.
2. The `shouldSkipObj` grows with each page entry. To be completely honest, I don't know enough about the rendering process to judge if each page would need skip lists for each other page component. But that growth during rendering felt weird, so i just moved the object one scope level down to reset it for each page. It's a complete shot in the dark, but it did hit something.
https://github.com/dai-shi/waku/blob/6550a0d26e7830527c767fcda8eb906aeb0f1266/packages/waku/src/router/define-router.ts#L96
3. Collection of modules for `__WAKU_ROUTER_PREFETCH__` has been changed to run in parallel. This is also necessary to enable in-component optimizations, like database request batching or connection pooling.

## Notes

* The `ssg-performance` fixture renders 10k pages in about a minute on my M1. Before it would just break.
* The build config serialization might still break at some threshold, due to `JSON.stringify` limits.
* Rendering all static paths to collect modules for prefetching feels wasteful, but thats too big of a change for this PR. But this brings us into a working state.

vercel[bot]: [vc]: #oBvF80Iaoum2pXOCX6dkg9xxoA8rDqoZqE7J9kZtjvI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3YWt1Iiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2RhaXNoaXMtcHJvamVjdHMvd2FrdS9nWmZoTUtQd1hYd2RxMkJUdHZwY2FCWWhzRHZYIiwicHJldmlld1VybCI6Indha3UtZ2l0LWZvcmstcG1lbGFiLXNzZy1wZXJmb3JtYW5jZS1kYWlzaGlzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **waku** | 🔄 Building ([Inspect](https://vercel.com/daishis-projects/waku/gZfhMKPwXXwdq2BTtvpcaBYhsDvX)) | [Visit Preview](https://waku-git-fork-pmelab-ssg-performance-daishis-projects.vercel.app) | Apr 24, 2024 8:45am |


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dai-shi/waku/pr/668/builds/497682) or the icon next to each commit SHA.
dai-shi: > The problem

Nice catch. I didn't know the exact problem, but I felt like the approach had some limits.
I haven't looked into the code, I will later. You know how CIs are failing?
Having a test is great, because eventually we would like to revisit `shouldSkip` mechanism.
pmelab: > You know how CIs are failing?

Playwright browsers fail to install there:

```
E: The repository 'https://packages.microsoft.com/ubuntu/22.04/prod jammy InRelease' is no longer signed.
```

> Having a test is great, because eventually we would like to revisit `shouldSkip` mechanism.

Absolutely. The change was more based on gut feeling rather than confident understanding.

I traced down the use of `/SHOULD_SKIP` into `getSkipList` ...

```
https://github.com/dai-shi/waku/blob/6550a0d26e7830527c767fcda8eb906aeb0f1266/packages/waku/src/router/client.ts#L213-L242
```

... which filters components of the current route path during fetching and pre-fetching of RSC payloads ...

https://github.com/dai-shi/waku/blob/6550a0d26e7830527c767fcda8eb906aeb0f1266/packages/waku/src/router/client.ts#L281-L297

https://github.com/dai-shi/waku/blob/6550a0d26e7830527c767fcda8eb906aeb0f1266/packages/waku/src/router/client.ts#L325-L332

I did not see how components of other routes could have an effect here, but I might be missing something. If you can help me to understand what it is actually supposed to **do**, I would be able to implement a test.
dai-shi: > If you can help me to understand what it is actually supposed to do, I would be able to implement a test.

The current design is the client side doesn't (shouldn't) know about the server-side route configs. What the client knows is if it should skip certain route (or maybe tell what it has in its cache to the server, and the server decides what to respond).

But, e2e tests shouldn't depend on implementation details and what should be tested is `route: 'static'` and `route: 'dynamic'` behavior.

I really wish to refactor the internal mechanism. Hopefully some time soonish.
pmelab: > But, e2e tests shouldn't depend on implementation details and what should be tested is route: 'static' and route: 'dynamic' behavior.

You mean `render: 'static'` and `render: 'dynamic'`? This is covered by a lot of integration tests already, no?
dai-shi: > You mean `render: 'static'` and `render: 'dynamic'`?

You are right. My bad.

> This is covered by a lot of integration tests already, no?

I don't remember if we have such tests that they ensure static pages are really "static". I don't think so.
pmelab: I found another issue: While generating the build config, all routes are rendered for collecting modules for prefetching:
https://github.com/dai-shi/waku/blob/6550a0d26e7830527c767fcda8eb906aeb0f1266/packages/waku/src/router/define-router.ts#L155-L163

This runs sequentially and currently blocks parallel rendering of pages. Thats probably not the best solution, since rendering all static instances of the same route to collect modules feels wrong, but fixing that would be a bigger endeavour. Parallelizing at least makes things work for now. Data requests should implement good caching though, since they run 3 times per page.
pmelab: @dai-shi : Is that what you had in mind? https://github.com/dai-shi/waku/pull/668/commits/a4d88782b02712f42ca1901d1da65f27fc4478a2
dai-shi: 



> @dai-shi : Is that what you had in mind? [a4d8878](https://github.com/dai-shi/waku/commit/a4d88782b02712f42ca1901d1da65f27fc4478a2)

Thanks. Hm, it looks good for the start, but what I meant with really "static" is that the static page has something like `Date.now()`, and if you reload the page, the date won't change.

To be honest, it's not fully implemented:
- SSR/SSG are fine
- RSC isn't static, so client routing may render static page again. (I hope to work on it after v0.21.0.)
  - Though, if client caches the rsc payload, it will skip it with `shouldSkip`. (Also, wanted to revisit in the future.)
- In DEV, static pages always rerender (I don't think it's solvable.)
dai-shi: Re: https://github.com/dai-shi/waku/pull/668#issuecomment-2077905809

See also #543. We would like to change `unstable_collectClientModue` so that it statically analyzes the code, instead of executing it. https://github.com/dai-shi/waku/pull/573#discussion_r1514091960
pmelab: > See also https://github.com/dai-shi/waku/issues/543. We would like to change unstable_collectClientModue so that it statically analyzes the code, instead of executing it. https://github.com/dai-shi/waku/pull/573#discussion_r1514091960

That would have been my next question. Thanks!

> Thanks. Hm, it looks good for the start, but what I meant with really "static" is that the static page has something like Date.now(), and if you reload the page, the date won't change.

So we want to test when components are executed. I changed the testcase to cover that (I hope 🤞 ).

Unfortunately I have no clue where the test fails come from. It only uses `tsconfig.e2e.json` for typechecks?
dai-shi: > It only uses tsconfig.e2e.json for typechecks?

You need to add new tests in "references".
pmelab: Ok, apparently using `createPages` in a fixture also leads to type errors. I switched it to file based routing instead.
dai-shi: > Ok, apparently using `createPages` in a fixture also leads to type errors.

That sounds somewhat unexpected. Can you elaborate? (Sorry, I haven't had time to look into it yet.)
pmelab: After adding the test package `tsconfig.json` files to references the type errors reduced, but some remained.

https://github.com/dai-shi/waku/actions/runs/8847605068/job/24295760378

Declaration files for `react-server-dom-webpack` could not be found. I noticed that no other fixtures use `createPages` and I wanted to know if there is a connection. The history is in my last 3 commits.
pmelab: I feel a little dumb now 😬 Thank you for fixing it!
dai-shi: > I feel a little dumb now 😬 Thank you for fixing it!

tbh, I don't know how it fixes it the problem.
`import { createPages } from 'waku'` should work in the app code.
There must be something different in e2e/fixtures setup. (cc @himself65 who should be more familiar with this.)
dai-shi: Please check ci error.

By the way, would you be able to join my discord server? We have #waku channel there and we can communicate about the development. https://discord.gg/MrQdmzd
pmelab: > tbh, I don't know how it fixes it the problem. `import { createPages } from 'waku'` should work in the app code. There must be something different in e2e/fixtures setup. (cc @himself65 who should be more familiar with this.)

I think the issues is related to this: https://github.com/dai-shi/waku/blob/c0eb2c9260a931d20d4087948ffa3ef9cd70d87e/tsconfig.json#L21-L22

The `waku` import only includes `main.ts` and the missing types are in `types.d.ts`. `waku/*` contains everything. In areal application this is not an issue because waku itself is not typechecked in this case.
pmelab: 
> Hm, isn't that types only? But, yeah something like that causes not resolving to `main.react-server.ts`.

The problem is just types. I appears as soon as a fixture imports something from `waku`, and not `waku/[whatever]`.
I will open another PR, along with another typechecking issue that I've seen.



Patch:
diff --git a/packages/waku/src/lib/builder/build.ts b/packages/waku/src/lib/builder/build.ts
index a93279f1..35630f1b 100644
--- a/packages/waku/src/lib/builder/build.ts
+++ b/packages/waku/src/lib/builder/build.ts
@@ -686,6 +686,8 @@ export async function build(options: {
   );
 
   const distEntries = await import(filePathToFileURL(distEntriesFile));
+
+  // TODO: Add progress indication for static builds.
   const buildConfig = await getBuildConfig({ config, entries: distEntries });
   await appendFile(
     distEntriesFile,
diff --git a/packages/waku/src/lib/utils/path.ts b/packages/waku/src/lib/utils/path.ts
index 872d128b..4587e973 100644
--- a/packages/waku/src/lib/utils/path.ts
+++ b/packages/waku/src/lib/utils/path.ts
@@ -89,6 +89,22 @@ export const parsePathWithSlug = (path: string): PathSpec =>
       return { type, name };
     });
 
+/**
+ * Transform a path spec to a regular expression.
+ */
+export const path2regexp = (path: PathSpec) => {
+  const parts = path.map(({ type, name }) => {
+    if (type === 'literal') {
+      return name;
+    } else if (type === 'group') {
+      return `([^/]+)`;
+    } else {
+      return `(.*)`;
+    }
+  });
+  return `^/${parts.join('/')}$`;
+};
+
 export const getPathMapping = (
   pathSpec: PathSpec,
   pathname: string,
diff --git a/packages/waku/src/router/create-pages.ts b/packages/waku/src/router/create-pages.ts
index 673ee450..5ff30019 100644
--- a/packages/waku/src/router/create-pages.ts
+++ b/packages/waku/src/router/create-pages.ts
@@ -7,6 +7,7 @@ import {
   joinPath,
   parsePathWithSlug,
   getPathMapping,
+  path2regexp,
 } from '../lib/utils/path.js';
 import type { BuildConfig } from '../server.js';
 import type { PathSpec } from '../lib/utils/path.js';
@@ -247,6 +248,7 @@ export function createPages(
     async () => {
       await configure();
       const paths: {
+        pattern: string;
         path: PathSpec;
         isStatic: boolean;
         noSsr: boolean;
@@ -258,6 +260,7 @@ export function createPages(
           ([layoutPathSpec]) => !hasPathSpecPrefix(layoutPathSpec, pathSpec),
         );
         paths.push({
+          pattern: path2regexp(parsePathWithSlug(path)),
           path: pathSpec,
           isStatic,
           noSsr,
@@ -267,6 +270,7 @@ export function createPages(
       for (const [path, [pathSpec]] of dynamicPagePathMap) {
         const noSsr = noSsrSet.has(pathSpec);
         paths.push({
+          pattern: path2regexp(parsePathWithSlug(path)),
           path: pathSpec,
           isStatic: false,
           noSsr,
@@ -276,6 +280,7 @@ export function createPages(
       for (const [path, [pathSpec]] of wildcardPagePathMap) {
         const noSsr = noSsrSet.has(pathSpec);
         paths.push({
+          pattern: path2regexp(parsePathWithSlug(path)),
           path: pathSpec,
           isStatic: false,
           noSsr,
diff --git a/packages/waku/src/router/define-router.ts b/packages/waku/src/router/define-router.ts
index fd9f9cd3..db8ba3f7 100644
--- a/packages/waku/src/router/define-router.ts
+++ b/packages/waku/src/router/define-router.ts
@@ -31,6 +31,7 @@ type ShouldSkipValue = ShouldSkip[number][1];
 export function unstable_defineRouter(
   getPathConfig: () => Promise<
     Iterable<{
+      pattern: string;
       path: PathSpec;
       isStatic?: boolean;
       noSsr?: boolean;
@@ -51,6 +52,7 @@ export function unstable_defineRouter(
   >,
 ): ReturnType<typeof defineEntries> {
   type MyPathConfig = {
+    pattern: string;
     pathname: PathSpec;
     isStatic?: boolean | undefined;
     customData: { noSsr?: boolean; is404: boolean; data: unknown };
@@ -69,6 +71,7 @@ export function unstable_defineRouter(
           item.path[0]!.type === 'literal' &&
           item.path[0]!.name === '404';
         return {
+          pattern: item.pattern,
           pathname: item.path,
           isStatic: item.isStatic,
           customData: { is404, noSsr: !!item.noSsr, data: item.data },
@@ -93,10 +96,6 @@ export function unstable_defineRouter(
         ? ['NOT_FOUND', 'HAS_404']
         : ['NOT_FOUND'];
   };
-  const shouldSkipObj: {
-    [componentId: ShouldSkip[number][0]]: ShouldSkip[number][1];
-  } = {};
-
   const renderEntries: RenderEntries = async (
     input,
     { searchParams, buildConfig },
@@ -105,6 +104,10 @@ export function unstable_defineRouter(
     if ((await existsPath(pathname, buildConfig))[0] === 'NOT_FOUND') {
       return null;
     }
+    const shouldSkipObj: {
+      [componentId: ShouldSkip[number][0]]: ShouldSkip[number][1];
+    } = {};
+
     const skip = searchParams.getAll(PARAM_KEY_SKIP) || [];
     searchParams.delete(PARAM_KEY_SKIP); // delete all
     const componentIds = getComponentIds(pathname);
@@ -152,20 +155,26 @@ export function unstable_defineRouter(
   ) => {
     const pathConfig = await getMyPathConfig();
     const path2moduleIds: Record<string, string[]> = {};
-    for (const { pathname: pathSpec } of pathConfig) {
-      if (pathSpec.some(({ type }) => type !== 'literal')) {
-        continue;
-      }
-      const pathname = '/' + pathSpec.map(({ name }) => name).join('/');
-      const input = getInputString(pathname);
-      const moduleIds = await unstable_collectClientModules(input);
-      path2moduleIds[pathname] = moduleIds;
-    }
+
+    await Promise.all(
+      pathConfig.map(async ({ pathname: pathSpec, pattern }) => {
+        if (pathSpec.some(({ type }) => type !== 'literal')) {
+          return;
+        }
+        const pathname = '/' + pathSpec.map(({ name }) => name).join('/');
+        const input = getInputString(pathname);
+        path2moduleIds[pattern] = await unstable_collectClientModules(input);
+      }),
+    );
+
     const customCode = `
 globalThis.__WAKU_ROUTER_PREFETCH__ = (path) => {
   const path2ids = ${JSON.stringify(path2moduleIds)};
-  for (const id of path2ids[path] || []) {
-    import(id);
+  const pattern = Object.keys(path2ids).find((key) => new RegExp(key).test(path));
+  if (pattern && path2ids[pattern]) {
+    for (const id of path2ids[pattern] || []) {
+      import(id);
+    }
   }
 };`;
     const buildConfig: BuildConfig = [];
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 145d7bf0..4045e292 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -72,6 +72,34 @@ importers:
         specifier: workspace:*
         version: link:packages/waku
 
+  e2e/fixtures/render-type:
+    dependencies:
+      react:
+        specifier: 19.0.0-beta-4508873393-20240430
+        version: 19.0.0-beta-4508873393-20240430
+      react-dom:
+        specifier: 19.0.0-beta-4508873393-20240430
+        version: 19.0.0-beta-4508873393-20240430(react@19.0.0-beta-4508873393-20240430)
+      react-server-dom-webpack:
+        specifier: 19.0.0-beta-4508873393-20240430
+        version: 19.0.0-beta-4508873393-20240430(react-dom@19.0.0-beta-4508873393-20240430)(react@19.0.0-beta-4508873393-20240430)(webpack@5.91.0)
+      serve:
+        specifier: ^14.2.3
+        version: 14.2.3
+      waku:
+        specifier: workspace:*
+        version: link:../../../packages/waku
+    devDependencies:
+      '@types/react':
+        specifier: 18.3.1
+        version: 18.3.1
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
+      typescript:
+        specifier: 5.4.4
+        version: 5.4.4
+
   e2e/fixtures/rsc-basic:
     dependencies:
       react:
@@ -122,6 +150,31 @@ importers:
         specifier: 5.4.4
         version: 5.4.4
 
+  e2e/fixtures/ssg-performance:
+    dependencies:
+      react:
+        specifier: 19.0.0-beta-4508873393-20240430
+        version: 19.0.0-beta-4508873393-20240430
+      react-dom:
+        specifier: 19.0.0-beta-4508873393-20240430
+        version: 19.0.0-beta-4508873393-20240430(react@19.0.0-beta-4508873393-20240430)
+      react-server-dom-webpack:
+        specifier: 19.0.0-beta-4508873393-20240430
+        version: 19.0.0-beta-4508873393-20240430(react-dom@19.0.0-beta-4508873393-20240430)(react@19.0.0-beta-4508873393-20240430)(webpack@5.91.0)
+      waku:
+        specifier: workspace:*
+        version: link:../../../packages/waku
+    devDependencies:
+      '@types/react':
+        specifier: 18.3.1
+        version: 18.3.1
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
+      typescript:
+        specifier: 5.4.4
+        version: 5.4.4
+
   e2e/fixtures/ssr-basic:
     dependencies:
       react:
@@ -2390,7 +2443,7 @@ packages:
   /@vanilla-extract/vite-plugin@4.0.7(@types/node@20.12.5)(vite@5.2.8):
     resolution: {integrity: sha512-uRAFWdq5Eq0ybpgW2P0LNOw8eW+MTg/OFo5K0Hsl2cKu/Tu2tabCsBf6vNjfhDrm4jBShHy1wJIVcYxf6sczVQ==}
     peerDependencies:
-      vite: 5.2.8
+      vite: ^4.0.3 || ^5.0.0
     dependencies:
       '@vanilla-extract/integration': 7.1.2(@types/node@20.12.5)
       vite: 5.2.8(@types/node@20.12.5)
@@ -2414,7 +2467,7 @@ packages:
     resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
-      vite: 5.2.8
+      vite: ^4.2.0 || ^5.0.0
     dependencies:
       '@babel/core': 7.24.4
       '@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.4)
@@ -2579,6 +2632,18 @@ packages:
     resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
     dev: false
 
+  /@zeit/schemas@2.36.0:
+    resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==}
+    dev: false
+
+  /accepts@1.3.8:
+    resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      mime-types: 2.1.35
+      negotiator: 0.6.3
+    dev: false
+
   /acorn-import-assertions@1.9.0(acorn@8.11.3):
     resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
     peerDependencies:
@@ -2627,15 +2692,28 @@ packages:
       json-schema-traverse: 0.4.1
       uri-js: 4.4.1
 
+  /ajv@8.12.0:
+    resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+    dependencies:
+      fast-deep-equal: 3.1.3
+      json-schema-traverse: 1.0.0
+      require-from-string: 2.0.2
+      uri-js: 4.4.1
+    dev: false
+
+  /ansi-align@3.0.1:
+    resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
+    dependencies:
+      string-width: 4.2.3
+    dev: false
+
   /ansi-regex@5.0.1:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
-    dev: true
 
   /ansi-regex@6.0.1:
     resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
     engines: {node: '>=12'}
-    dev: true
 
   /ansi-styles@3.2.1:
     resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
@@ -2657,7 +2735,6 @@ packages:
   /ansi-styles@6.2.1:
     resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
     engines: {node: '>=12'}
-    dev: true
 
   /any-promise@1.3.0:
     resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -2673,11 +2750,9 @@ packages:
 
   /arch@2.2.0:
     resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
-    dev: true
 
   /arg@5.0.2:
     resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
-    dev: true
 
   /argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -2822,7 +2897,6 @@ packages:
 
   /balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
-    dev: true
 
   /bin-check@4.1.0:
     resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==}
@@ -2854,12 +2928,25 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /boxen@7.0.0:
+    resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==}
+    engines: {node: '>=14.16'}
+    dependencies:
+      ansi-align: 3.0.1
+      camelcase: 7.0.1
+      chalk: 5.0.1
+      cli-boxes: 3.0.0
+      string-width: 5.1.2
+      type-fest: 2.19.0
+      widest-line: 4.0.1
+      wrap-ansi: 8.1.0
+    dev: false
+
   /brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
     dependencies:
       balanced-match: 1.0.2
       concat-map: 0.0.1
-    dev: true
 
   /brace-expansion@2.0.1:
     resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
@@ -2888,6 +2975,11 @@ packages:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
     dev: false
 
+  /bytes@3.0.0:
+    resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
+    engines: {node: '>= 0.8'}
+    dev: false
+
   /cac@6.7.14:
     resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
     engines: {node: '>=8'}
@@ -2932,6 +3024,11 @@ packages:
     engines: {node: '>= 6'}
     dev: true
 
+  /camelcase@7.0.1:
+    resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==}
+    engines: {node: '>=14.16'}
+    dev: false
+
   /caniuse-lite@1.0.30001606:
     resolution: {integrity: sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==}
 
@@ -2952,6 +3049,13 @@ packages:
       type-detect: 4.0.8
     dev: true
 
+  /chalk-template@0.4.0:
+    resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
+    engines: {node: '>=12'}
+    dependencies:
+      chalk: 4.1.2
+    dev: false
+
   /chalk@2.4.2:
     resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
     engines: {node: '>=4'}
@@ -2967,6 +3071,11 @@ packages:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
+  /chalk@5.0.1:
+    resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+    dev: false
+
   /character-entities-html4@2.1.0:
     resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
     dev: false
@@ -3018,10 +3127,24 @@ packages:
     resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
     dev: false
 
+  /cli-boxes@3.0.0:
+    resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
+    engines: {node: '>=10'}
+    dev: false
+
   /client-only@0.0.1:
     resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
     dev: false
 
+  /clipboardy@3.0.0:
+    resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dependencies:
+      arch: 2.2.0
+      execa: 5.1.1
+      is-wsl: 2.2.0
+    dev: false
+
   /clone-response@1.0.3:
     resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
     dependencies:
@@ -3073,9 +3196,35 @@ packages:
     engines: {node: ^12.20.0 || >=14}
     dev: true
 
+  /compressible@2.0.18:
+    resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      mime-db: 1.52.0
+    dev: false
+
+  /compression@1.7.4:
+    resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==}
+    engines: {node: '>= 0.8.0'}
+    dependencies:
+      accepts: 1.3.8
+      bytes: 3.0.0
+      compressible: 2.0.18
+      debug: 2.6.9
+      on-headers: 1.0.2
+      safe-buffer: 5.1.2
+      vary: 1.1.2
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+
   /concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
-    dev: true
+
+  /content-disposition@0.5.2:
+    resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==}
+    engines: {node: '>= 0.6'}
+    dev: false
 
   /content-disposition@0.5.4:
     resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
@@ -3107,7 +3256,6 @@ packages:
       path-key: 3.1.1
       shebang-command: 2.0.0
       which: 2.0.2
-    dev: true
 
   /css-mediaquery@0.1.2:
     resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
@@ -3158,6 +3306,17 @@ packages:
       '@babel/runtime': 7.23.9
     dev: false
 
+  /debug@2.6.9:
+    resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+    dependencies:
+      ms: 2.0.0
+    dev: false
+
   /debug@3.2.7:
     resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
     peerDependencies:
@@ -3203,7 +3362,6 @@ packages:
   /deep-extend@0.6.0:
     resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
     engines: {node: '>=4.0.0'}
-    dev: true
 
   /deep-is@0.1.4:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -3294,18 +3452,15 @@ packages:
 
   /eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
-    dev: true
 
   /electron-to-chromium@1.4.729:
     resolution: {integrity: sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==}
 
   /emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
-    dev: true
 
   /emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
-    dev: true
 
   /end-of-stream@1.4.4:
     resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
@@ -3852,7 +4007,6 @@ packages:
       onetime: 5.1.2
       signal-exit: 3.0.7
       strip-final-newline: 2.0.0
-    dev: true
 
   /execa@8.0.1:
     resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
@@ -3916,6 +4070,12 @@ packages:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
     dev: true
 
+  /fast-url-parser@1.1.3:
+    resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==}
+    dependencies:
+      punycode: 1.4.1
+    dev: false
+
   /fastq@1.17.1:
     resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
     dependencies:
@@ -4115,7 +4275,6 @@ packages:
   /get-stream@6.0.1:
     resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
     engines: {node: '>=10'}
-    dev: true
 
   /get-stream@8.0.1:
     resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
@@ -4330,7 +4489,6 @@ packages:
   /human-signals@2.1.0:
     resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
     engines: {node: '>=10.17.0'}
-    dev: true
 
   /human-signals@5.0.0:
     resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
@@ -4372,7 +4530,6 @@ packages:
 
   /ini@1.3.8:
     resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
-    dev: true
 
   /inline-style-parser@0.1.1:
     resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
@@ -4473,6 +4630,12 @@ packages:
     resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
     dev: false
 
+  /is-docker@2.2.1:
+    resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
+    engines: {node: '>=8'}
+    hasBin: true
+    dev: false
+
   /is-extglob@2.1.1:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
@@ -4487,7 +4650,6 @@ packages:
   /is-fullwidth-code-point@3.0.0:
     resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
     engines: {node: '>=8'}
-    dev: true
 
   /is-generator-function@1.0.10:
     resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==}
@@ -4544,6 +4706,11 @@ packages:
     engines: {node: '>=12'}
     dev: false
 
+  /is-port-reachable@4.0.0:
+    resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dev: false
+
   /is-reference@3.0.2:
     resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
     dependencies:
@@ -4578,7 +4745,6 @@ packages:
   /is-stream@2.0.1:
     resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
     engines: {node: '>=8'}
-    dev: true
 
   /is-stream@3.0.0:
     resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
@@ -4625,13 +4791,19 @@ packages:
       get-intrinsic: 1.2.4
     dev: true
 
+  /is-wsl@2.2.0:
+    resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
+    engines: {node: '>=8'}
+    dependencies:
+      is-docker: 2.2.1
+    dev: false
+
   /isarray@2.0.5:
     resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
     dev: true
 
   /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
-    dev: true
 
   /iterator.prototype@1.1.2:
     resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
@@ -4715,6 +4887,10 @@ packages:
   /json-schema-traverse@0.4.1:
     resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
 
+  /json-schema-traverse@1.0.0:
+    resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+    dev: false
+
   /json-stable-stringify-without-jsonify@1.0.1:
     resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
     dev: true
@@ -5288,10 +5464,22 @@ packages:
       picomatch: 2.3.1
     dev: true
 
+  /mime-db@1.33.0:
+    resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
   /mime-db@1.52.0:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
 
+  /mime-types@2.1.18:
+    resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      mime-db: 1.33.0
+    dev: false
+
   /mime-types@2.1.35:
     resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
     engines: {node: '>= 0.6'}
@@ -5302,7 +5490,6 @@ packages:
   /mimic-fn@2.1.0:
     resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
     engines: {node: '>=6'}
-    dev: true
 
   /mimic-fn@4.0.0:
     resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
@@ -5323,7 +5510,6 @@ packages:
     resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
     dependencies:
       brace-expansion: 1.1.11
-    dev: true
 
   /minimatch@9.0.3:
     resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
@@ -5341,7 +5527,6 @@ packages:
 
   /minimist@1.2.8:
     resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
-    dev: true
 
   /minipass@3.3.6:
     resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
@@ -5396,6 +5581,10 @@ packages:
     engines: {node: '>=4'}
     dev: false
 
+  /ms@2.0.0:
+    resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+    dev: false
+
   /ms@2.1.2:
     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
 
@@ -5420,6 +5609,11 @@ packages:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
     dev: true
 
+  /negotiator@0.6.3:
+    resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
   /neo-async@2.6.2:
     resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
     dev: false
@@ -5494,7 +5688,6 @@ packages:
     engines: {node: '>=8'}
     dependencies:
       path-key: 3.1.1
-    dev: true
 
   /npm-run-path@5.3.0:
     resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
@@ -5578,6 +5771,11 @@ packages:
       es-object-atoms: 1.0.0
     dev: true
 
+  /on-headers@1.0.2:
+    resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
+    engines: {node: '>= 0.8'}
+    dev: false
+
   /once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
     dependencies:
@@ -5589,7 +5787,6 @@ packages:
     engines: {node: '>=6'}
     dependencies:
       mimic-fn: 2.1.0
-    dev: true
 
   /onetime@6.0.0:
     resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
@@ -5681,6 +5878,10 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /path-is-inside@1.0.2:
+    resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
+    dev: false
+
   /path-key@2.0.1:
     resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
     engines: {node: '>=4'}
@@ -5689,7 +5890,6 @@ packages:
   /path-key@3.1.1:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
-    dev: true
 
   /path-key@4.0.0:
     resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
@@ -5708,6 +5908,10 @@ packages:
       minipass: 7.0.4
     dev: true
 
+  /path-to-regexp@2.2.1:
+    resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==}
+    dev: false
+
   /path-type@4.0.0:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
@@ -5976,6 +6180,10 @@ packages:
       once: 1.4.0
     dev: true
 
+  /punycode@1.4.1:
+    resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+    dev: false
+
   /punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
@@ -5995,6 +6203,11 @@ packages:
       safe-buffer: 5.2.1
     dev: false
 
+  /range-parser@1.2.0:
+    resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
   /rc@1.2.8:
     resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
     hasBin: true
@@ -6003,7 +6216,6 @@ packages:
       ini: 1.3.8
       minimist: 1.2.8
       strip-json-comments: 2.0.1
-    dev: true
 
   /react-dom@19.0.0-beta-4508873393-20240430(react@19.0.0-beta-4508873393-20240430):
     resolution: {integrity: sha512-/j97ai1qF3c6O3XV0nVzzExPV/0U2v8M75Sq6ThXYxePCi33kAnm+xRsCDpZOZOrIjz6nurLU/FzzPZIzXVvKQ==}
@@ -6143,14 +6355,12 @@ packages:
     dependencies:
       rc: 1.2.8
       safe-buffer: 5.2.1
-    dev: true
 
   /registry-url@3.1.0:
     resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==}
     engines: {node: '>=0.10.0'}
     dependencies:
       rc: 1.2.8
-    dev: true
 
   /remark-mdx@2.3.0:
     resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==}
@@ -6180,6 +6390,11 @@ packages:
       unified: 10.1.2
     dev: false
 
+  /require-from-string@2.0.2:
+    resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+    engines: {node: '>=0.10.0'}
+    dev: false
+
   /require-like@0.1.2:
     resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==}
     dev: true
@@ -6284,6 +6499,10 @@ packages:
       isarray: 2.0.5
     dev: true
 
+  /safe-buffer@5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+    dev: false
+
   /safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
 
@@ -6339,6 +6558,39 @@ packages:
       randombytes: 2.1.0
     dev: false
 
+  /serve-handler@6.1.5:
+    resolution: {integrity: sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==}
+    dependencies:
+      bytes: 3.0.0
+      content-disposition: 0.5.2
+      fast-url-parser: 1.1.3
+      mime-types: 2.1.18
+      minimatch: 3.1.2
+      path-is-inside: 1.0.2
+      path-to-regexp: 2.2.1
+      range-parser: 1.2.0
+    dev: false
+
+  /serve@14.2.3:
+    resolution: {integrity: sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ==}
+    engines: {node: '>= 14'}
+    hasBin: true
+    dependencies:
+      '@zeit/schemas': 2.36.0
+      ajv: 8.12.0
+      arg: 5.0.2
+      boxen: 7.0.0
+      chalk: 5.0.1
+      chalk-template: 0.4.0
+      clipboardy: 3.0.0
+      compression: 1.7.4
+      is-port-reachable: 4.0.0
+      serve-handler: 6.1.5
+      update-check: 1.5.4
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+
   /server-only@0.0.1:
     resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
     dev: true
@@ -6377,7 +6629,6 @@ packages:
     engines: {node: '>=8'}
     dependencies:
       shebang-regex: 3.0.0
-    dev: true
 
   /shebang-regex@1.0.0:
     resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
@@ -6387,7 +6638,6 @@ packages:
   /shebang-regex@3.0.0:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
-    dev: true
 
   /shiki@1.2.4:
     resolution: {integrity: sha512-Q9n9jKiOjJCRPztA9POn3/uZXNySHDNKAsPNpmtHDcFyi6ZQhx5vQKZW3Nhrwn8TWW3RudSRk66zqY603EZDeg==}
@@ -6411,7 +6661,6 @@ packages:
 
   /signal-exit@3.0.7:
     resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
-    dev: true
 
   /signal-exit@4.1.0:
     resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
@@ -6492,7 +6741,6 @@ packages:
       emoji-regex: 8.0.0
       is-fullwidth-code-point: 3.0.0
       strip-ansi: 6.0.1
-    dev: true
 
   /string-width@5.1.2:
     resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
@@ -6501,7 +6749,6 @@ packages:
       eastasianwidth: 0.2.0
       emoji-regex: 9.2.2
       strip-ansi: 7.1.0
-    dev: true
 
   /string.prototype.matchall@4.0.11:
     resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==}
@@ -6566,14 +6813,12 @@ packages:
     engines: {node: '>=8'}
     dependencies:
       ansi-regex: 5.0.1
-    dev: true
 
   /strip-ansi@7.1.0:
     resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
     engines: {node: '>=12'}
     dependencies:
       ansi-regex: 6.0.1
-    dev: true
 
   /strip-bom@3.0.0:
     resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
@@ -6588,7 +6833,6 @@ packages:
   /strip-final-newline@2.0.0:
     resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
     engines: {node: '>=6'}
-    dev: true
 
   /strip-final-newline@3.0.0:
     resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
@@ -6598,7 +6842,6 @@ packages:
   /strip-json-comments@2.0.1:
     resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
     engines: {node: '>=0.10.0'}
-    dev: true
 
   /strip-json-comments@3.1.1:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
@@ -6904,6 +7147,11 @@ packages:
     engines: {node: '>=10'}
     dev: true
 
+  /type-fest@2.19.0:
+    resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
+    engines: {node: '>=12.20'}
+    dev: false
+
   /typed-array-buffer@1.0.2:
     resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==}
     engines: {node: '>= 0.4'}
@@ -7059,7 +7307,6 @@ packages:
     dependencies:
       registry-auth-token: 3.3.2
       registry-url: 3.1.0
-    dev: true
 
   /uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -7137,6 +7384,11 @@ packages:
       sade: 1.8.1
     dev: false
 
+  /vary@1.1.2:
+    resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+    engines: {node: '>= 0.8'}
+    dev: false
+
   /vfile-matter@3.0.1:
     resolution: {integrity: sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==}
     dependencies:
@@ -7199,7 +7451,7 @@ packages:
   /vite-tsconfig-paths@4.3.2(typescript@5.4.4)(vite@5.2.8):
     resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
     peerDependencies:
-      vite: 5.2.8
+      vite: '*'
     peerDependenciesMeta:
       vite:
         optional: true
@@ -7431,7 +7683,6 @@ packages:
     hasBin: true
     dependencies:
       isexe: 2.0.0
-    dev: true
 
   /why-is-node-running@2.2.2:
     resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
@@ -7442,6 +7693,13 @@ packages:
       stackback: 0.0.2
     dev: true
 
+  /widest-line@4.0.1:
+    resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==}
+    engines: {node: '>=12'}
+    dependencies:
+      string-width: 5.1.2
+    dev: false
+
   /wrap-ansi@7.0.0:
     resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
     engines: {node: '>=10'}
@@ -7458,7 +7716,6 @@ packages:
       ansi-styles: 6.2.1
       string-width: 5.1.2
       strip-ansi: 7.1.0
-    dev: true
 
   /wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}


Test patch:
diff --git a/e2e/fixtures/render-type/README.md b/e2e/fixtures/render-type/README.md
new file mode 100644
index 00000000..1da4e63b
--- /dev/null
+++ b/e2e/fixtures/render-type/README.md
@@ -0,0 +1,3 @@
+# Render types
+
+Test different render types.
diff --git a/e2e/fixtures/render-type/package.json b/e2e/fixtures/render-type/package.json
new file mode 100644
index 00000000..df1e04eb
--- /dev/null
+++ b/e2e/fixtures/render-type/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "render-type",
+  "version": "0.1.0",
+  "type": "module",
+  "private": true,
+  "scripts": {
+    "dev": "waku dev",
+    "build": "waku build",
+    "start:dynamic": "waku start",
+    "start:static": "serve dist/public"
+  },
+  "dependencies": {
+    "react": "19.0.0-beta-4508873393-20240430",
+    "react-dom": "19.0.0-beta-4508873393-20240430",
+    "react-server-dom-webpack": "19.0.0-beta-4508873393-20240430",
+    "waku": "workspace:*",
+    "serve": "^14.2.3"
+  },
+  "devDependencies": {
+    "@types/react": "18.3.1",
+    "@types/react-dom": "18.3.0",
+    "typescript": "5.4.4"
+  }
+}
diff --git a/e2e/fixtures/render-type/src/ClientEcho.tsx b/e2e/fixtures/render-type/src/ClientEcho.tsx
new file mode 100644
index 00000000..cf0fd6af
--- /dev/null
+++ b/e2e/fixtures/render-type/src/ClientEcho.tsx
@@ -0,0 +1,13 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { Echo } from './Echo.js';
+
+export function ClientEcho({ echo }: { echo: string }) {
+  const [timeStamp, setTimestamp] = useState(() => Date.now());
+  useEffect(() => {
+    const interval = setInterval(() => setTimestamp(Date.now()), 1000);
+    return () => clearInterval(interval);
+  });
+  return <Echo echo={echo} timestamp={timeStamp} />;
+}
diff --git a/e2e/fixtures/render-type/src/Echo.tsx b/e2e/fixtures/render-type/src/Echo.tsx
new file mode 100644
index 00000000..082fb77b
--- /dev/null
+++ b/e2e/fixtures/render-type/src/Echo.tsx
@@ -0,0 +1,8 @@
+export function Echo({ echo, timestamp }: { echo: string; timestamp: number }) {
+  return (
+    <div>
+      <p data-testid="echo">{echo}</p>
+      <p data-testid="timestamp">{timestamp}</p>
+    </div>
+  );
+}
diff --git a/e2e/fixtures/render-type/src/ServerEcho.tsx b/e2e/fixtures/render-type/src/ServerEcho.tsx
new file mode 100644
index 00000000..112efccd
--- /dev/null
+++ b/e2e/fixtures/render-type/src/ServerEcho.tsx
@@ -0,0 +1,5 @@
+import { Echo } from './Echo.js';
+
+export function ServerEcho({ echo }: { echo: string }) {
+  return <Echo echo={echo} timestamp={Date.now()} />;
+}
diff --git a/e2e/fixtures/render-type/src/entries.tsx b/e2e/fixtures/render-type/src/entries.tsx
new file mode 100644
index 00000000..b4617a81
--- /dev/null
+++ b/e2e/fixtures/render-type/src/entries.tsx
@@ -0,0 +1,29 @@
+import { createPages } from 'waku/router/server';
+
+import { ServerEcho } from './ServerEcho.js';
+import { ClientEcho } from './ClientEcho.js';
+
+export default createPages(async ({ createPage }) => {
+  createPage({
+    render: 'dynamic',
+    path: '/server/dynamic/[echo]',
+    component: ServerEcho,
+  });
+  createPage({
+    render: 'static',
+    path: '/server/static/[echo]',
+    staticPaths: ['static-echo'],
+    component: ServerEcho,
+  });
+  createPage({
+    render: 'dynamic',
+    path: '/client/dynamic/[echo]',
+    component: ClientEcho,
+  });
+  createPage({
+    render: 'static',
+    path: '/client/static/[echo]',
+    staticPaths: ['static-echo'],
+    component: ClientEcho,
+  });
+});
diff --git a/e2e/fixtures/render-type/tsconfig.json b/e2e/fixtures/render-type/tsconfig.json
new file mode 100644
index 00000000..02058076
--- /dev/null
+++ b/e2e/fixtures/render-type/tsconfig.json
@@ -0,0 +1,17 @@
+{
+  "compilerOptions": {
+    "strict": true,
+    "target": "esnext",
+    "downlevelIteration": true,
+    "esModuleInterop": true,
+    "module": "nodenext",
+    "skipLibCheck": true,
+    "noUncheckedIndexedAccess": true,
+    "exactOptionalPropertyTypes": true,
+    "types": ["react/experimental"],
+    "jsx": "react-jsx",
+    "outDir": "./dist",
+    "composite": true
+  },
+  "include": ["./src", "./waku.config.ts"]
+}
diff --git a/e2e/fixtures/rsc-router/src/entries.tsx b/e2e/fixtures/rsc-router/src/entries.tsx
index d12e38ad..51e2378a 100644
--- a/e2e/fixtures/rsc-router/src/entries.tsx
+++ b/e2e/fixtures/rsc-router/src/entries.tsx
@@ -6,6 +6,7 @@ export default unstable_defineRouter(
   // getPathConfig
   async () =>
     STATIC_PATHS.map((path) => ({
+      pattern: `^${path}$`,
       path: path
         .split('/')
         .filter(Boolean)
diff --git a/e2e/fixtures/ssg-performance/README.md b/e2e/fixtures/ssg-performance/README.md
new file mode 100644
index 00000000..37685dbf
--- /dev/null
+++ b/e2e/fixtures/ssg-performance/README.md
@@ -0,0 +1,3 @@
+# SSG Performance
+
+Test high volume SSG performance.
diff --git a/e2e/fixtures/ssg-performance/package.json b/e2e/fixtures/ssg-performance/package.json
new file mode 100644
index 00000000..93c77223
--- /dev/null
+++ b/e2e/fixtures/ssg-performance/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "ssg-performance",
+  "version": "0.1.0",
+  "type": "module",
+  "private": true,
+  "scripts": {
+    "dev": "waku dev",
+    "build": "waku build",
+    "start": "waku start"
+  },
+  "dependencies": {
+    "react": "19.0.0-beta-4508873393-20240430",
+    "react-dom": "19.0.0-beta-4508873393-20240430",
+    "react-server-dom-webpack": "19.0.0-beta-4508873393-20240430",
+    "waku": "workspace:*"
+  },
+  "devDependencies": {
+    "@types/react": "18.3.1",
+    "@types/react-dom": "18.3.0",
+    "typescript": "5.4.4"
+  }
+}
diff --git a/e2e/fixtures/ssg-performance/src/Path.tsx b/e2e/fixtures/ssg-performance/src/Path.tsx
new file mode 100644
index 00000000..20a5119f
--- /dev/null
+++ b/e2e/fixtures/ssg-performance/src/Path.tsx
@@ -0,0 +1,5 @@
+import { getPath } from './context.js';
+
+export function Path() {
+  return <h1>{getPath()}</h1>;
+}
diff --git a/e2e/fixtures/ssg-performance/src/context.ts b/e2e/fixtures/ssg-performance/src/context.ts
new file mode 100644
index 00000000..e7954cef
--- /dev/null
+++ b/e2e/fixtures/ssg-performance/src/context.ts
@@ -0,0 +1,17 @@
+import { cache } from 'react';
+
+// Straight copy of https://github.com/manvalls/server-only-context/tree/main
+// Inlined for demonstration purposes.
+function createContext<T>(defaultValue: T): [() => T, (v: T) => void] {
+  const getRef = cache(() => ({ current: defaultValue }));
+
+  const getValue = (): T => getRef().current;
+
+  const setValue = (value: T) => {
+    getRef().current = value;
+  };
+
+  return [getValue, setValue];
+}
+
+export const [getPath, setPath] = createContext('/');
diff --git a/e2e/fixtures/ssg-performance/src/pages/[path].tsx b/e2e/fixtures/ssg-performance/src/pages/[path].tsx
new file mode 100644
index 00000000..7f4dc3ec
--- /dev/null
+++ b/e2e/fixtures/ssg-performance/src/pages/[path].tsx
@@ -0,0 +1,13 @@
+import { Path } from '../Path.js';
+
+export default async function Test() {
+  await new Promise((resolve) => setTimeout(resolve, 1000));
+  return <Path />;
+}
+
+export async function getConfig() {
+  return {
+    render: 'static',
+    staticPaths: new Array(5000).fill(null).map((_, i) => `path-${i}`),
+  };
+}
diff --git a/e2e/fixtures/ssg-performance/src/pages/_layout.tsx b/e2e/fixtures/ssg-performance/src/pages/_layout.tsx
new file mode 100644
index 00000000..712e6f75
--- /dev/null
+++ b/e2e/fixtures/ssg-performance/src/pages/_layout.tsx
@@ -0,0 +1,10 @@
+import type { PropsWithChildren } from 'react';
+import { setPath } from '../context.js';
+
+export default function Layout({
+  children,
+  path,
+}: PropsWithChildren<{ path: string }>) {
+  setPath(path);
+  return children;
+}
diff --git a/e2e/fixtures/ssg-performance/tsconfig.json b/e2e/fixtures/ssg-performance/tsconfig.json
new file mode 100644
index 00000000..02058076
--- /dev/null
+++ b/e2e/fixtures/ssg-performance/tsconfig.json
@@ -0,0 +1,17 @@
+{
+  "compilerOptions": {
+    "strict": true,
+    "target": "esnext",
+    "downlevelIteration": true,
+    "esModuleInterop": true,
+    "module": "nodenext",
+    "skipLibCheck": true,
+    "noUncheckedIndexedAccess": true,
+    "exactOptionalPropertyTypes": true,
+    "types": ["react/experimental"],
+    "jsx": "react-jsx",
+    "outDir": "./dist",
+    "composite": true
+  },
+  "include": ["./src", "./waku.config.ts"]
+}
diff --git a/e2e/render-type.spec.ts b/e2e/render-type.spec.ts
new file mode 100644
index 00000000..2ad63e07
--- /dev/null
+++ b/e2e/render-type.spec.ts
@@ -0,0 +1,125 @@
+import { execSync, exec, ChildProcess } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import waitPort from 'wait-port';
+import { debugChildProcess, getFreePort, terminate, test } from './utils.js';
+import { rm } from 'node:fs/promises';
+import { expect } from '@playwright/test';
+
+const waku = fileURLToPath(
+  new URL('../packages/waku/dist/cli.js', import.meta.url),
+);
+
+const cwd = fileURLToPath(new URL('./fixtures/render-type', import.meta.url));
+
+test.describe(`render type`, () => {
+  test.skip(
+    ({ browserName }) => browserName !== 'chromium',
+    'Browsers are not relevant for this test. One is enough.',
+  );
+
+  let cp: ChildProcess;
+  let port: number;
+
+  test.beforeAll('remove cache', async () => {
+    await rm(`${cwd}/dist`, {
+      recursive: true,
+      force: true,
+    });
+    execSync(`node ${waku} build`, { cwd });
+  });
+
+  test.describe('static', () => {
+    test.beforeEach(async () => {
+      port = await getFreePort();
+      // Use a static http server to make sure its not accidentally SSR.
+      cp = exec(`pnpm serve -l ${port} dist/public`, { cwd });
+      await waitPort({ port });
+    });
+
+    test('renders static content', async ({ page }) => {
+      await page.goto(`http://localhost:${port}/server/static/static-echo`);
+      expect(await page.getByTestId('echo').innerText()).toEqual('static-echo');
+    });
+
+    test('does not hydrate server components', async ({ page }) => {
+      await page.goto(`http://localhost:${port}/server/static/static-echo`);
+      const timestamp = await page.getByTestId('timestamp').innerText();
+      await page.waitForTimeout(100);
+      await page.reload();
+      // Timestamp should remain the same, because its build time.
+      expect(await page.getByTestId('timestamp').innerText()).toEqual(
+        timestamp,
+      );
+      await page.waitForTimeout(100);
+    });
+
+    test('hydrates client components', async ({ page }) => {
+      await page.goto(`http://localhost:${port}/client/static/static-echo`);
+      expect(await page.getByTestId('echo').innerText()).toEqual('static-echo');
+      const timestamp = await page.getByTestId('timestamp').innerText();
+      await page.waitForTimeout(100);
+      await page.reload();
+      // Timestamp should update with each refresh, because its client rendered.
+      expect(await page.getByTestId('timestamp').innerText()).not.toEqual(
+        timestamp,
+      );
+      await page.waitForTimeout(100);
+      // Timestamp should update in the browser because its hydrated.
+      expect(await page.getByTestId('timestamp').innerText()).not.toEqual(
+        timestamp,
+      );
+    });
+  });
+
+  test.describe('dynamic', () => {
+    test.beforeEach(async () => {
+      port = await getFreePort();
+      cp = exec(`node ${waku} start --port ${port}`, { cwd });
+      debugChildProcess(cp, fileURLToPath(import.meta.url), [
+        /ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time/,
+      ]);
+      await waitPort({ port });
+    });
+
+    test('renders dynamic content', async ({ page }) => {
+      await page.goto(`http://localhost:${port}/server/dynamic/dynamic-echo`);
+      expect(await page.getByTestId('echo').innerText()).toEqual(
+        'dynamic-echo',
+      );
+    });
+
+    test('does not hydrate server components', async ({ page }) => {
+      await page.goto(`http://localhost:${port}/server/dynamic/dynamic-echo`);
+      const timestamp = await page.getByTestId('timestamp').innerText();
+      await page.waitForTimeout(100);
+      await page.reload();
+      // Timestamp should update with each refresh, because its server rendered.
+      expect(await page.getByTestId('timestamp').innerText()).not.toBe(
+        timestamp,
+      );
+    });
+    test('hydrates client components', async ({ page }) => {
+      await page.goto(`http://localhost:${port}/client/dynamic/dynamic-echo`);
+      expect(await page.getByTestId('echo').innerText()).toEqual(
+        'dynamic-echo',
+      );
+      const timestamp = await page.getByTestId('timestamp').innerText();
+      await page.waitForTimeout(100);
+      await page.reload();
+      // Timestamp should update with each refresh, because its server rendered.
+      expect(await page.getByTestId('timestamp').innerText()).not.toEqual(
+        timestamp,
+      );
+      await page.waitForTimeout(100);
+      // Timestamp should update in the browser because its hydrated.
+      expect(await page.getByTestId('timestamp').innerText()).not.toEqual(
+        timestamp,
+      );
+    });
+    // TODO: Add test case for cached RSC payload that should not re-render.
+  });
+
+  test.afterEach(async () => {
+    await terminate(cp.pid!);
+  });
+});
diff --git a/e2e/ssg-performance.spec.ts b/e2e/ssg-performance.spec.ts
new file mode 100644
index 00000000..7b50f516
--- /dev/null
+++ b/e2e/ssg-performance.spec.ts
@@ -0,0 +1,45 @@
+import { expect } from '@playwright/test';
+import { execSync, exec, ChildProcess } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import waitPort from 'wait-port';
+import { debugChildProcess, getFreePort, terminate, test } from './utils.js';
+import { rm } from 'node:fs/promises';
+
+const waku = fileURLToPath(
+  new URL('../packages/waku/dist/cli.js', import.meta.url),
+);
+
+const cwd = fileURLToPath(
+  new URL('./fixtures/ssg-performance', import.meta.url),
+);
+
+test.describe(`high volume static site generation`, () => {
+  test.skip(
+    ({ browserName }) => browserName !== 'chromium',
+    'Browsers are not relevant for this test. One is enough.',
+  );
+
+  let cp: ChildProcess;
+  let port: number;
+
+  test.afterAll(async () => {
+    await terminate(cp.pid!);
+  });
+
+  test('build and verify', async ({ page }) => {
+    test.setTimeout(60000);
+    await rm(`${cwd}/dist`, {
+      recursive: true,
+      force: true,
+    });
+    execSync(`node ${waku} build`, { cwd });
+    port = await getFreePort();
+    cp = exec(`node ${waku} start --port ${port}`, { cwd });
+    debugChildProcess(cp, fileURLToPath(import.meta.url), [
+      /ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time/,
+    ]);
+    await waitPort({ port });
+    await page.goto(`http://localhost:${port}/path-3`);
+    await expect(page.getByRole('heading')).toHaveText('/path-3');
+  });
+});
diff --git a/packages/waku/tests/path.test.ts b/packages/waku/tests/path.test.ts
new file mode 100644
index 00000000..ca7925c2
--- /dev/null
+++ b/packages/waku/tests/path.test.ts
@@ -0,0 +1,34 @@
+import { test, describe, expect } from 'vitest';
+import { parsePathWithSlug, path2regexp } from '../src/lib/utils/path.js';
+
+function matchPath(path: string, input: string) {
+  return new RegExp(path2regexp(parsePathWithSlug(path))).test(input);
+}
+
+describe('path2regexp', () => {
+  test('handles paths without slugs', () => {
+    expect(matchPath('/foo/bar', '/foo/bar')).toBe(true);
+    expect(matchPath('/foo/baz', '/foo/bar')).toBe(false);
+  });
+
+  test('handles paths with groups', () => {
+    expect(matchPath('/foo/[x]/[y]', '/foo/bar/baz')).toBe(true);
+    expect(matchPath('/foo/[x]/[y]', '/foo/baz/bar')).toBe(true);
+    expect(matchPath('/bar/[x]/[y]', '/foo/bar')).toBe(false);
+    expect(matchPath('/foo/[x]/[y]', '/foo/bar/baz/qux')).toBe(false);
+  });
+
+  test('handles paths with wildcards', () => {
+    expect(matchPath('/foo/[...x]', '/foo/bar/baz/qux')).toBe(true);
+    expect(matchPath('/foo/[...x]', '/foo/bar')).toBe(true);
+    expect(matchPath('/foo/[...x]', '/foo')).toBe(false);
+    expect(matchPath('/foo/[...x]', '/bar')).toBe(false);
+  });
+
+  test('handles paths with groups and wildcards', () => {
+    expect(matchPath('/foo/[x]/[...y]', '/foo/bar/baz/qux')).toBe(true);
+    expect(matchPath('/foo/[x]/[...y]', '/foo/bar')).toBe(false);
+    expect(matchPath('/foo/[x]/[...y]', '/foo')).toBe(false);
+    expect(matchPath('/foo/[x]/[...y]', '/bar')).toBe(false);
+  });
+});
diff --git a/tsconfig.e2e.json b/tsconfig.e2e.json
index 9096de6c..0bdf1a82 100644
--- a/tsconfig.e2e.json
+++ b/tsconfig.e2e.json
@@ -23,6 +23,12 @@
     },
     {
       "path": "./e2e/fixtures/ssr-context-provider/tsconfig.json"
+    },
+    {
+      "path": "./e2e/fixtures/render-type/tsconfig.json"
+    },
+    {
+      "path": "./e2e/fixtures/ssg-performance/tsconfig.json"
     }
   ]
 }