Skip to content

Commit

Permalink
add api example (#743)
Browse files Browse the repository at this point in the history
While we wait for #329 (which takes time to design and implement, and we
believe most cases are covered by server actions), we can use the
low-level middleware api.
  • Loading branch information
dai-shi authored Jun 9, 2024
1 parent 8906b73 commit 81a7bc3
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 96 deletions.
135 changes: 39 additions & 96 deletions e2e/examples-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
import waitPort from 'wait-port';
import { readdir, rm } from 'node:fs/promises';
import { basename } from 'node:path';
import { debugChildProcess, getFreePort, terminate, test } from './utils.js';
import { getFreePort, terminate, test } from './utils.js';
import { error, info } from '@actions/core';

const examplesDir = fileURLToPath(new URL('../examples', import.meta.url));
Expand All @@ -29,115 +29,58 @@ const commands = [
},
];

const specialExamples = [
{
name: '08_cookies',
commands: [
{
command: 'node dev.js',
},
{
build: 'waku build',
command: 'node start.js',
},
],
},
].slice(Infinity); // FIXME: remove, as no longer needed

const examples = [
...(await readdir(examplesDir)).map((example) =>
fileURLToPath(new URL(`../examples/${example}`, import.meta.url)),
),
];

for (const cwd of examples) {
const specialExample = specialExamples.find(({ name }) => cwd.includes(name));
if (specialExample) {
for (const { build, command } of specialExample.commands) {
test.describe(`smoke test on ${basename(cwd)}: ${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(build, {
cwd,
env: process.env,
});
}
port = await getFreePort();
cp = exec(`${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('check title', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
// title maybe doesn't ready yet
await page.waitForLoadState('load');
await expect.poll(() => page.title()).toMatch(/^Waku/);
for (const { build, command } of commands) {
test.describe(`smoke test on ${basename(cwd)}: ${command}`, () => {
let cp: ChildProcess;
let port: number;
test.beforeAll('remove cache', async () => {
await rm(`${cwd}/dist`, {
recursive: true,
force: true,
});
});
}
} else {
for (const { build, command } of commands) {
test.describe(`smoke test on ${basename(cwd)}: ${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 });
test.beforeAll(async () => {
if (build) {
execSync(`node ${waku} ${build}`, { cwd });
}
port = await getFreePort();
cp = exec(`node ${waku} ${command} --port ${port}`, { cwd });
cp.stdout?.on('data', (data) => {
info(`${port} stdout: ${data}`);
console.log(`${port} stdout: `, `${data}`);
});
cp.stderr?.on('data', (data) => {
if (
command === 'dev' &&
/WebSocket server error: Port is already in use/.test(`${data}`)
) {
// ignore this error
return;
}
port = await getFreePort();
cp = exec(`node ${waku} ${command} --port ${port}`, { cwd });
cp.stdout?.on('data', (data) => {
info(`${port} stdout: ${data}`);
console.log(`${port} stdout: `, `${data}`);
});
cp.stderr?.on('data', (data) => {
if (
command === 'dev' &&
/WebSocket server error: Port is already in use/.test(`${data}`)
) {
// ignore this error
return;
}
error(`${port} stderr: ${data}`);
console.error(`${port} stderr: `, `${data}`);
});
await waitPort({ port });
error(`${port} stderr: ${data}`);
console.error(`${port} stderr: `, `${data}`);
});
await waitPort({ port });
});

test.afterAll(async () => {
await terminate(cp.pid!);
});
test.afterAll(async () => {
await terminate(cp.pid!);
});

test('check title', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
// title maybe doesn't ready yet
await page.waitForLoadState('load');
await expect.poll(() => page.title()).toMatch(/^Waku/);
});
test('check title', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
// title maybe doesn't ready yet
await page.waitForLoadState('load');
await expect.poll(() => page.title()).toMatch(/^Waku/);
});
}
});
}
}
1 change: 1 addition & 0 deletions examples/08_cookies/waku.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
),
]
: []),
import('waku/middleware/headers'),
import('waku/middleware/ssr'),
import('waku/middleware/rsc'),
],
Expand Down
22 changes: 22 additions & 0 deletions examples/09_api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "waku-example",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start"
},
"dependencies": {
"react": "19.0.0-rc.0",
"react-dom": "19.0.0-rc.0",
"react-server-dom-webpack": "19.0.0-rc.0",
"waku": "0.21.0-alpha.2"
},
"devDependencies": {
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"typescript": "5.4.5"
}
}
14 changes: 14 additions & 0 deletions examples/09_api/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Counter } from './Counter';

const App = ({ name }: { name: string }) => {
return (
<div style={{ border: '3px red dashed', margin: '1em', padding: '1em' }}>
<title>Waku</title>
<h1>Hello {name}!!</h1>
<h3>This is a server component.</h3>
<Counter />
</div>
);
};

export default App;
30 changes: 30 additions & 0 deletions examples/09_api/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import { Suspense, use, useState, useEffect } from 'react';

export const Counter = () => {
const [count, setCount] = useState(0);
const [promise, setPromise] = useState<Promise<unknown>>();
useEffect(() => {
setPromise(fetch('/api/hello').then((res) => res.text()));
}, []);
return (
<div style={{ border: '3px blue dashed', margin: '1em', padding: '1em' }}>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<h3>This is a client component.</h3>
<Suspense fallback={<p>Loading...</p>}>
{promise && <Hello promise={promise} />}
</Suspense>
</div>
);
};

const Hello = ({ promise }: { promise: Promise<unknown> }) => {
const message = `${use(promise)}`;
return (
<div>
<p>Hello, {message}</p>
</div>
);
};
27 changes: 27 additions & 0 deletions examples/09_api/src/entries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineEntries } from 'waku/server';
import { Slot } from 'waku/client';

import App from './components/App';

export default defineEntries(
// renderEntries
async (input) => {
return {
App: <App name={input || 'Waku'} />,
};
},
// getBuildConfig
async () => [{ pathname: '/', entries: [{ input: '' }] }],
// getSsrConfig
async (pathname) => {
switch (pathname) {
case '/':
return {
input: '',
body: <Slot id="App" />,
};
default:
return null;
}
},
);
17 changes: 17 additions & 0 deletions examples/09_api/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { StrictMode } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import { Root, Slot } from 'waku/client';

const rootElement = (
<StrictMode>
<Root>
<Slot id="App" />
</Root>
</StrictMode>
);

if (document.body.dataset.hydrate) {
hydrateRoot(document.body, rootElement);
} else {
createRoot(document.body).render(rootElement);
}
24 changes: 24 additions & 0 deletions examples/09_api/src/middleware/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Middleware } from 'waku/config';

const stringToStream = (str: string): ReadableStream => {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(str));
controller.close();
},
});
};

const apiMiddleware: Middleware = () => {
return async (ctx, next) => {
const path = ctx.req.url.pathname;
if (path === '/api/hello') {
ctx.res.body = stringToStream('world');
return;
}
await next();
};
};

export default apiMiddleware;
15 changes: 15 additions & 0 deletions examples/09_api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"target": "esnext",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"types": ["react/experimental"],
"jsx": "react-jsx"
}
}
18 changes: 18 additions & 0 deletions examples/09_api/waku.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const DO_NOT_BUNDLE = '';

/** @type {import('waku/config').Config} */
export default {
middleware: (cmd: 'dev' | 'start') => [
import('./src/middleware/api.js'),
...(cmd === 'dev'
? [
import(
/* @vite-ignore */ DO_NOT_BUNDLE + 'waku/middleware/dev-server'
),
]
: []),
import('waku/middleware/headers'),
import('waku/middleware/ssr'),
import('waku/middleware/rsc'),
],
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"examples:dev:05_nossr": "NAME=05_nossr pnpm run examples:dev",
"examples:dev:07_router": "NAME=07_router pnpm run examples:dev",
"examples:dev:08_cookies": "NAME=08_cookies pnpm run examples:dev",
"examples:dev:09_api": "NAME=09_api pnpm run examples:dev",
"examples:dev:10_fs-router": "NAME=10_fs-router pnpm run examples:dev",
"examples:dev:11_form": "NAME=11_form pnpm run examples:dev",
"examples:dev:12_css": "NAME=12_css pnpm run examples:dev",
Expand All @@ -37,6 +38,7 @@
"examples:prd:05_nossr": "NAME=05_nossr pnpm run examples:prd",
"examples:prd:07_router": "NAME=07_router pnpm run examples:prd",
"examples:prd:08_cookies": "NAME=08_cookies pnpm run examples:prd",
"examples:prd:09_api": "NAME=09_api pnpm run examples:prd",
"examples:prd:10_fs-router": "NAME=10_fs-router pnpm run examples:prd",
"examples:prd:11_form": "NAME=11_form pnpm run examples:prd",
"examples:prd:12_css": "NAME=12_css pnpm run examples:prd",
Expand Down
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 81a7bc3

Please sign in to comment.