diff --git a/e2e/fixtures/rsc-css-modules/README.md b/e2e/fixtures/rsc-css-modules/README.md
new file mode 100644
index 000000000..088740a69
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/README.md
@@ -0,0 +1,3 @@
+# RSC CSS Modules
+
+Only RSC features, no SSR. Using css modules to style the application, both on server and client components.
diff --git a/e2e/fixtures/rsc-css-modules/declaration.d.ts b/e2e/fixtures/rsc-css-modules/declaration.d.ts
new file mode 100644
index 000000000..60260a3ad
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/declaration.d.ts
@@ -0,0 +1 @@
+declare module '*.module.css';
diff --git a/e2e/fixtures/rsc-css-modules/package.json b/e2e/fixtures/rsc-css-modules/package.json
new file mode 100644
index 000000000..4466cd536
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "rsc-css-modules",
+ "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/rsc-css-modules/src/components/App.tsx b/e2e/fixtures/rsc-css-modules/src/components/App.tsx
new file mode 100644
index 000000000..74d730263
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/src/components/App.tsx
@@ -0,0 +1,16 @@
+import styles from './app.module.css';
+import { ClientCounter } from './ClientCounter.js';
+
+const App = ({ name }: { name: string }) => {
+ return (
+
+
Waku example
+
+ {name}
+
+
+
+ );
+};
+
+export default App;
diff --git a/e2e/fixtures/rsc-css-modules/src/components/ClientCounter.tsx b/e2e/fixtures/rsc-css-modules/src/components/ClientCounter.tsx
new file mode 100644
index 000000000..731cc2f6b
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/src/components/ClientCounter.tsx
@@ -0,0 +1,20 @@
+'use client';
+import { useState } from 'react';
+import styles from './clientCounter.module.css';
+
+export const ClientCounter = () => {
+ const [count, setCount] = useState(0);
+
+ return (
+
+
{count}
+
+
+ );
+};
diff --git a/e2e/fixtures/rsc-css-modules/src/components/app.module.css b/e2e/fixtures/rsc-css-modules/src/components/app.module.css
new file mode 100644
index 000000000..68d6e759c
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/src/components/app.module.css
@@ -0,0 +1,9 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.text {
+ font-size: 18px;
+ color: blue;
+}
diff --git a/e2e/fixtures/rsc-css-modules/src/components/clientCounter.module.css b/e2e/fixtures/rsc-css-modules/src/components/clientCounter.module.css
new file mode 100644
index 000000000..af2190d17
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/src/components/clientCounter.module.css
@@ -0,0 +1,9 @@
+.counterWrapper {
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+}
+
+.counterButton {
+ padding: 8px;
+}
diff --git a/e2e/fixtures/rsc-css-modules/src/entries.tsx b/e2e/fixtures/rsc-css-modules/src/entries.tsx
new file mode 100644
index 000000000..00cd7ab33
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/src/entries.tsx
@@ -0,0 +1,19 @@
+import { lazy } from 'react';
+import { defineEntries } from 'waku/server';
+
+const App = lazy(() => import('./components/App.js'));
+
+export default defineEntries(
+ // renderEntries
+ async (input) => {
+ return {
+ App: ,
+ };
+ },
+ // getBuildConfig
+ async () => [{ pathname: '/', entries: [{ input: '' }] }],
+ // getSsrConfig
+ () => {
+ throw new Error('SSR should not be used in this test.');
+ },
+);
diff --git a/e2e/fixtures/rsc-css-modules/src/main.tsx b/e2e/fixtures/rsc-css-modules/src/main.tsx
new file mode 100644
index 000000000..057e44a90
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/src/main.tsx
@@ -0,0 +1,13 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { Root, Slot } from 'waku/client';
+
+const rootElement = (
+
+
+
+
+
+);
+
+createRoot(document.body).render(rootElement);
diff --git a/e2e/fixtures/rsc-css-modules/tsconfig.json b/e2e/fixtures/rsc-css-modules/tsconfig.json
new file mode 100644
index 000000000..2f2c65b2d
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/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", "declaration.d.ts"]
+}
diff --git a/e2e/fixtures/rsc-css-modules/waku.config.ts b/e2e/fixtures/rsc-css-modules/waku.config.ts
new file mode 100644
index 000000000..419997de7
--- /dev/null
+++ b/e2e/fixtures/rsc-css-modules/waku.config.ts
@@ -0,0 +1,16 @@
+const DO_NOT_BUNDLE = '';
+
+/** @type {import('waku/config').Config} */
+export default {
+ middleware: (cmd: 'dev' | 'start') => [
+ ...(cmd === 'dev'
+ ? [
+ import(
+ /* @vite-ignore */ DO_NOT_BUNDLE + 'waku/middleware/dev-server'
+ ),
+ ]
+ : []),
+ import('waku/middleware/rsc'),
+ import('waku/middleware/fallback'),
+ ],
+};
diff --git a/e2e/rsc-css-modules.spec.ts b/e2e/rsc-css-modules.spec.ts
new file mode 100644
index 000000000..721259593
--- /dev/null
+++ b/e2e/rsc-css-modules.spec.ts
@@ -0,0 +1,77 @@
+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 commands = [
+ {
+ command: 'dev',
+ },
+ {
+ build: 'build',
+ command: 'start',
+ },
+];
+
+const cwd = fileURLToPath(
+ new URL('./fixtures/rsc-css-modules', import.meta.url),
+);
+
+for (const { build, command } of commands) {
+ test.describe(`rsc-css-modules: ${command}`, () => {
+ let cp: ChildProcess;
+ let port: number;
+ test.beforeAll('remove cache', async () => {
+ await rm(`${cwd}/dist`, {
+ recursive: true,
+ force: true,
+ });
+ });
+
+ test.beforeAll(async () => {
+ if (build) {
+ execSync(`node ${waku} ${build}`, { cwd });
+ }
+ port = await getFreePort();
+ cp = exec(`node ${waku} ${command} --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.afterAll(async () => {
+ await terminate(cp.pid!);
+ });
+
+ test('css-modules classes', async ({ page }) => {
+ await page.goto(`http://localhost:${port}/`);
+
+ const wrapperClass = await page
+ .getByTestId('app-wrapper')
+ .getAttribute('class');
+ expect(wrapperClass).toContain('wrapper');
+
+ const appNameClass = await page
+ .getByTestId('app-name')
+ .getAttribute('class');
+ expect(appNameClass).toContain('text');
+
+ const clientcounterClass = await page
+ .getByTestId('client-counter')
+ .getAttribute('class');
+ expect(clientcounterClass).toContain('counterWrapper');
+
+ const incrementClass = await page
+ .getByTestId('increment')
+ .getAttribute('class');
+ expect(incrementClass).toContain('counterButton');
+ });
+ });
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 063b273f9..823374f12 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -125,6 +125,31 @@ importers:
specifier: 5.4.4
version: 5.4.4
+ e2e/fixtures/rsc-css-modules:
+ 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/rsc-router:
dependencies:
react:
diff --git a/tsconfig.e2e.json b/tsconfig.e2e.json
index 0bdf1a825..9a4b6f30c 100644
--- a/tsconfig.e2e.json
+++ b/tsconfig.e2e.json
@@ -9,6 +9,9 @@
{
"path": "./e2e/fixtures/rsc-basic/tsconfig.json"
},
+ {
+ "path": "./e2e/fixtures/rsc-css-modules/tsconfig.json"
+ },
{
"path": "./e2e/fixtures/ssr-basic/tsconfig.json"
},