diff --git a/e2e/fixtures/monorepo/package.json b/e2e/fixtures/monorepo/package.json
new file mode 100644
index 000000000..1950a019e
--- /dev/null
+++ b/e2e/fixtures/monorepo/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "monorepo",
+ "private": true,
+ "workspaces": [
+ "packages/*"
+ ]
+}
diff --git a/e2e/fixtures/monorepo/packages/waku-project/package.json b/e2e/fixtures/monorepo/packages/waku-project/package.json
new file mode 100644
index 000000000..644c9aa5b
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "waku-project",
+ "version": "0.0.0",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "waku dev",
+ "build": "waku build",
+ "start": "waku start"
+ },
+ "dependencies": {
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "react-server-dom-webpack": "19.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "19.0.1",
+ "@types/react-dom": "19.0.2",
+ "autoprefixer": "10.4.20",
+ "tailwindcss": "3.4.16",
+ "typescript": "5.7.2"
+ }
+}
diff --git a/e2e/fixtures/monorepo/packages/waku-project/postcss.config.js b/e2e/fixtures/monorepo/packages/waku-project/postcss.config.js
new file mode 100644
index 000000000..709af5d83
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/postcss.config.js
@@ -0,0 +1,7 @@
+/** @type {import('postcss-load-config').Config} */
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/public/images/favicon.png b/e2e/fixtures/monorepo/packages/waku-project/public/images/favicon.png
new file mode 100644
index 000000000..cd90d7908
Binary files /dev/null and b/e2e/fixtures/monorepo/packages/waku-project/public/images/favicon.png differ
diff --git a/e2e/fixtures/monorepo/packages/waku-project/src/components/counter.tsx b/e2e/fixtures/monorepo/packages/waku-project/src/components/counter.tsx
new file mode 100644
index 000000000..adb6b94e9
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/src/components/counter.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { useState } from 'react'; // eslint-disable-line import/no-unresolved
+
+export const Counter = () => {
+ const [count, setCount] = useState(0);
+
+ const handleIncrement = () => setCount((c) => c + 1);
+
+ return (
+
+ Count: {count}
+
+
+ );
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/src/components/footer.tsx b/e2e/fixtures/monorepo/packages/waku-project/src/components/footer.tsx
new file mode 100644
index 000000000..8cfd9c897
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/src/components/footer.tsx
@@ -0,0 +1,18 @@
+export const Footer = () => {
+ return (
+
+ );
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/src/components/header.tsx b/e2e/fixtures/monorepo/packages/waku-project/src/components/header.tsx
new file mode 100644
index 000000000..1b03ba54d
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/src/components/header.tsx
@@ -0,0 +1,11 @@
+import { Link } from 'waku';
+
+export const Header = () => {
+ return (
+
+ );
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/src/pages/_layout.tsx b/e2e/fixtures/monorepo/packages/waku-project/src/pages/_layout.tsx
new file mode 100644
index 000000000..6d227c9f6
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/src/pages/_layout.tsx
@@ -0,0 +1,39 @@
+import '../styles.css';
+
+import type { ReactNode } from 'react';
+
+import { Header } from '../components/header';
+import { Footer } from '../components/footer';
+
+type RootLayoutProps = { children: ReactNode };
+
+export default async function RootLayout({ children }: RootLayoutProps) {
+ const data = await getData();
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
+
+const getData = async () => {
+ const data = {
+ description: 'An internet website!',
+ icon: '/images/favicon.png',
+ };
+
+ return data;
+};
+
+export const getConfig = async () => {
+ return {
+ render: 'static',
+ } as const;
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/src/pages/about.tsx b/e2e/fixtures/monorepo/packages/waku-project/src/pages/about.tsx
new file mode 100644
index 000000000..15d4c90e1
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/src/pages/about.tsx
@@ -0,0 +1,32 @@
+import { Link } from 'waku';
+
+export default async function AboutPage() {
+ const data = await getData();
+
+ return (
+
+
{data.title}
+
{data.headline}
+
{data.body}
+
+ Return home
+
+
+ );
+}
+
+const getData = async () => {
+ const data = {
+ title: 'About',
+ headline: 'About Waku',
+ body: 'The minimal React framework',
+ };
+
+ return data;
+};
+
+export const getConfig = async () => {
+ return {
+ render: 'static',
+ } as const;
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/src/pages/index.tsx b/e2e/fixtures/monorepo/packages/waku-project/src/pages/index.tsx
new file mode 100644
index 000000000..6a825dd7d
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/src/pages/index.tsx
@@ -0,0 +1,37 @@
+import { Link } from 'waku';
+
+import { Counter } from '../components/counter';
+
+export default async function HomePage() {
+ const data = await getData();
+
+ return (
+
+
{data.title}
+
+ {data.headline}
+
+
{data.body}
+
+
+ About page
+
+
+ );
+}
+
+const getData = async () => {
+ const data = {
+ title: 'Waku',
+ headline: 'Waku',
+ body: 'Hello world!',
+ };
+
+ return data;
+};
+
+export const getConfig = async () => {
+ return {
+ render: 'static',
+ } as const;
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/src/styles.css b/e2e/fixtures/monorepo/packages/waku-project/src/styles.css
new file mode 100644
index 000000000..4cb5445ab
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/src/styles.css
@@ -0,0 +1,4 @@
+@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,700;1,400;1,700&display=swap');
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/e2e/fixtures/monorepo/packages/waku-project/tailwind.config.js b/e2e/fixtures/monorepo/packages/waku-project/tailwind.config.js
new file mode 100644
index 000000000..df5c92956
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/tailwind.config.js
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}'],
+};
diff --git a/e2e/fixtures/monorepo/packages/waku-project/tsconfig.json b/e2e/fixtures/monorepo/packages/waku-project/tsconfig.json
new file mode 100644
index 000000000..0d0f8993d
--- /dev/null
+++ b/e2e/fixtures/monorepo/packages/waku-project/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "strict": true,
+ "target": "esnext",
+ "downlevelIteration": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "skipLibCheck": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "types": ["react/experimental"],
+ "jsx": "react-jsx",
+ "rootDir": "./src",
+ "outDir": "./dist"
+ }
+}
diff --git a/e2e/monorepo.spec.ts b/e2e/monorepo.spec.ts
new file mode 100644
index 000000000..dbeec7620
--- /dev/null
+++ b/e2e/monorepo.spec.ts
@@ -0,0 +1,29 @@
+import { expect } from '@playwright/test';
+
+import { test, prepareStandaloneSetup } from './utils.js';
+
+const startApp = prepareStandaloneSetup('monorepo');
+
+for (const mode of ['DEV', 'PRD'] as const) {
+ for (const packageManager of ['npm', 'pnpm', 'yarn'] as const) {
+ test.describe(`${packageManager} monorepo: ${mode}`, () => {
+ let port: number;
+ let stopApp: () => Promise;
+ test.beforeAll(async () => {
+ ({ port, stopApp } = await startApp(
+ mode,
+ packageManager,
+ 'packages/waku-project',
+ ));
+ });
+ test.afterAll(async () => {
+ await stopApp();
+ });
+
+ test('renders the home page', async ({ page }) => {
+ await page.goto(`http://localhost:${port}`);
+ await expect(page.getByTestId('header')).toHaveText('Waku');
+ });
+ });
+ }
+}
diff --git a/e2e/utils.ts b/e2e/utils.ts
index 0b4df5fb8..f369eb0bf 100644
--- a/e2e/utils.ts
+++ b/e2e/utils.ts
@@ -138,6 +138,12 @@ export const prepareNormalSetup = (fixtureName: string) => {
return startApp;
};
+const PACKAGE_INSTALL = {
+ npm: (path: string) => `npm add ${path}`,
+ pnpm: (path: string) => `pnpm add ${path}`,
+ yarn: (path: string) => `yarn add ${path}`,
+} as const;
+
export const prepareStandaloneSetup = (fixtureName: string) => {
const wakuDir = fileURLToPath(new URL('../packages/waku', import.meta.url));
const { version } = createRequire(import.meta.url)(
@@ -151,7 +157,11 @@ export const prepareStandaloneSetup = (fixtureName: string) => {
const tmpDir = process.env.TEMP_DIR || tmpdir();
let standaloneDir: string | undefined;
let built = false;
- const startApp = async (mode: 'DEV' | 'PRD' | 'STATIC') => {
+ const startApp = async (
+ mode: 'DEV' | 'PRD' | 'STATIC',
+ packageManager: 'npm' | 'pnpm' | 'yarn' = 'npm',
+ packageDir = '',
+ ) => {
if (!standaloneDir) {
standaloneDir = mkdtempSync(join(tmpDir, fixtureName));
cpSync(fixtureDir, standaloneDir, {
@@ -164,16 +174,22 @@ export const prepareStandaloneSetup = (fixtureName: string) => {
cwd: wakuDir,
stdio: 'inherit',
});
+ const wakuPackageTgz = join(standaloneDir, `waku-${version}.tgz`);
+ const installScript = PACKAGE_INSTALL[packageManager](wakuPackageTgz);
+ execSync(installScript, { cwd: standaloneDir, stdio: 'inherit' });
execSync(
`npm install --force ${join(standaloneDir, `waku-${version}.tgz`)}`,
{ cwd: standaloneDir, stdio: 'inherit' },
);
}
if (mode !== 'DEV' && !built) {
- rmSync(`${standaloneDir}/dist`, { recursive: true, force: true });
+ rmSync(`${join(standaloneDir, packageDir, 'dist')}`, {
+ recursive: true,
+ force: true,
+ });
execSync(
`node ${join(standaloneDir, './node_modules/waku/dist/cli.js')} build`,
- { cwd: standaloneDir },
+ { cwd: join(standaloneDir, packageDir) },
);
built = true;
}
@@ -190,7 +206,7 @@ export const prepareStandaloneSetup = (fixtureName: string) => {
cmd = `node ${join(standaloneDir, './node_modules/serve/build/main.js')} dist/public -p ${port}`;
break;
}
- const cp = exec(cmd, { cwd: standaloneDir });
+ const cp = exec(cmd, { cwd: join(standaloneDir, packageDir) });
debugChildProcess(cp, fileURLToPath(import.meta.url), [
/ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time/,
]);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8699f989b..21408f93e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -175,6 +175,8 @@ importers:
specifier: ^5.7.2
version: 5.7.2
+ e2e/fixtures/monorepo: {}
+
e2e/fixtures/partial-build:
dependencies:
react:
diff --git a/tsconfig.e2e.json b/tsconfig.e2e.json
index 9ec7e2831..1845263d0 100644
--- a/tsconfig.e2e.json
+++ b/tsconfig.e2e.json
@@ -51,6 +51,9 @@
{
"path": "./e2e/fixtures/hot-reload/tsconfig.json"
},
+ {
+ "path": "./e2e/fixtures/monorepo/packages/waku-project/tsconfig.json"
+ },
{
"path": "./e2e/fixtures/create-pages/tsconfig.json"
}