diff --git a/e2e/examples-smoke.spec.ts b/e2e/examples-smoke.spec.ts index 7beab4fb4..cc5756448 100644 --- a/e2e/examples-smoke.spec.ts +++ b/e2e/examples-smoke.spec.ts @@ -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)); @@ -29,21 +29,6 @@ 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)), @@ -51,93 +36,51 @@ const examples = [ ]; 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/); }); - } + }); } } diff --git a/examples/08_cookies/waku.config.ts b/examples/08_cookies/waku.config.ts index 7992bae25..d8719c8e4 100644 --- a/examples/08_cookies/waku.config.ts +++ b/examples/08_cookies/waku.config.ts @@ -11,6 +11,7 @@ export default { ), ] : []), + import('waku/middleware/headers'), import('waku/middleware/ssr'), import('waku/middleware/rsc'), ], diff --git a/examples/09_api/package.json b/examples/09_api/package.json new file mode 100644 index 000000000..4aa75138e --- /dev/null +++ b/examples/09_api/package.json @@ -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" + } +} diff --git a/examples/09_api/src/components/App.tsx b/examples/09_api/src/components/App.tsx new file mode 100644 index 000000000..20f8fb649 --- /dev/null +++ b/examples/09_api/src/components/App.tsx @@ -0,0 +1,14 @@ +import { Counter } from './Counter'; + +const App = ({ name }: { name: string }) => { + return ( +
+ Waku +

Hello {name}!!

+

This is a server component.

+ +
+ ); +}; + +export default App; diff --git a/examples/09_api/src/components/Counter.tsx b/examples/09_api/src/components/Counter.tsx new file mode 100644 index 000000000..8e48a5882 --- /dev/null +++ b/examples/09_api/src/components/Counter.tsx @@ -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>(); + useEffect(() => { + setPromise(fetch('/api/hello').then((res) => res.text())); + }, []); + return ( +
+

Count: {count}

+ +

This is a client component.

+ Loading...

}> + {promise && } +
+
+ ); +}; + +const Hello = ({ promise }: { promise: Promise }) => { + const message = `${use(promise)}`; + return ( +
+

Hello, {message}

+
+ ); +}; diff --git a/examples/09_api/src/entries.tsx b/examples/09_api/src/entries.tsx new file mode 100644 index 000000000..dc00ff134 --- /dev/null +++ b/examples/09_api/src/entries.tsx @@ -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: , + }; + }, + // getBuildConfig + async () => [{ pathname: '/', entries: [{ input: '' }] }], + // getSsrConfig + async (pathname) => { + switch (pathname) { + case '/': + return { + input: '', + body: , + }; + default: + return null; + } + }, +); diff --git a/examples/09_api/src/main.tsx b/examples/09_api/src/main.tsx new file mode 100644 index 000000000..fe026c6b2 --- /dev/null +++ b/examples/09_api/src/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot, hydrateRoot } from 'react-dom/client'; +import { Root, Slot } from 'waku/client'; + +const rootElement = ( + + + + + +); + +if (document.body.dataset.hydrate) { + hydrateRoot(document.body, rootElement); +} else { + createRoot(document.body).render(rootElement); +} diff --git a/examples/09_api/src/middleware/api.ts b/examples/09_api/src/middleware/api.ts new file mode 100644 index 000000000..50e9c4f5d --- /dev/null +++ b/examples/09_api/src/middleware/api.ts @@ -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; diff --git a/examples/09_api/tsconfig.json b/examples/09_api/tsconfig.json new file mode 100644 index 000000000..84d0d542f --- /dev/null +++ b/examples/09_api/tsconfig.json @@ -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" + } +} diff --git a/examples/09_api/waku.config.ts b/examples/09_api/waku.config.ts new file mode 100644 index 000000000..43cfa24c1 --- /dev/null +++ b/examples/09_api/waku.config.ts @@ -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'), + ], +}; diff --git a/package.json b/package.json index 4bac489a1..09fec220e 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a77a4ec72..02830d07d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -567,6 +567,31 @@ importers: specifier: 5.4.5 version: 5.4.5 + examples/09_api: + dependencies: + react: + specifier: 19.0.0-rc.0 + version: 19.0.0-rc.0 + react-dom: + specifier: 19.0.0-rc.0 + version: 19.0.0-rc.0(react@19.0.0-rc.0) + react-server-dom-webpack: + specifier: 19.0.0-rc.0 + version: 19.0.0-rc.0(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0)(webpack@5.91.0) + waku: + specifier: 0.21.0-alpha.2 + version: link:../../packages/waku + devDependencies: + '@types/react': + specifier: 18.3.3 + version: 18.3.3 + '@types/react-dom': + specifier: 18.3.0 + version: 18.3.0 + typescript: + specifier: 5.4.5 + version: 5.4.5 + examples/10_fs-router: dependencies: react: