diff --git a/docs/redirects.mdx b/docs/redirects.mdx
index 89302acc8..b239a12ba 100644
--- a/docs/redirects.mdx
+++ b/docs/redirects.mdx
@@ -52,9 +52,9 @@ import type { Config } from 'waku/config';
export default {
middleware: () => [
+ import('waku/middleware/context'),
import('./src/middleware/redirects.js'),
import('waku/middleware/dev-server'),
- import('waku/middleware/headers'),
import('waku/middleware/rsc'),
import('waku/middleware/ssr'),
],
diff --git a/e2e/fixtures/broken-links/waku.config.ts b/e2e/fixtures/broken-links/waku.config.ts
index 6babe53f4..d05df8033 100644
--- a/e2e/fixtures/broken-links/waku.config.ts
+++ b/e2e/fixtures/broken-links/waku.config.ts
@@ -1,9 +1,9 @@
/** @type {import('waku/config').Config} */
export default {
middleware: () => [
+ import('waku/middleware/context'),
import('./src/redirects.js'),
import('waku/middleware/dev-server'),
- import('waku/middleware/headers'),
import('waku/middleware/rsc'),
import('waku/middleware/ssr'),
],
diff --git a/examples/12_nossr/waku.config.ts b/examples/12_nossr/waku.config.ts
index 8b8ba4250..770a600a2 100644
--- a/examples/12_nossr/waku.config.ts
+++ b/examples/12_nossr/waku.config.ts
@@ -1,6 +1,7 @@
/** @type {import('waku/config').Config} */
export default {
middleware: () => [
+ import('waku/middleware/context'),
import('waku/middleware/dev-server'),
import('waku/middleware/rsc'),
import('waku/middleware/fallback'),
diff --git a/examples/31_minimal/src/entries.tsx b/examples/31_minimal/src/entries.tsx
index 894d8e6d6..3c3b34835 100644
--- a/examples/31_minimal/src/entries.tsx
+++ b/examples/31_minimal/src/entries.tsx
@@ -1,27 +1,18 @@
-import { defineEntries } from 'waku/server';
-import { Slot } from 'waku/client';
+import { new_defineEntries } from 'waku/minimal/server';
+import { Slot } from 'waku/minimal/client';
import App from './components/App';
-export default defineEntries(
- // renderEntries
- async (rscPath) => {
- return {
- App: ,
- };
- },
- // getBuildConfig
- async () => [{ pathname: '/', entries: [{ rscPath: '' }] }],
- // getSsrConfig
- async (pathname) => {
- switch (pathname) {
- case '/':
- return {
- rscPath: '',
- html: ,
- };
- default:
- return null;
+export default new_defineEntries({
+ unstable_handleRequest: async (input, { renderRsc, renderHtml }) => {
+ if (input.type === 'component') {
+ return renderRsc({ App: });
+ }
+ if (input.type === 'custom' && input.pathname === '/') {
+ return renderHtml({ App: }, , '');
}
},
-);
+ unstable_getBuildConfig: async () => [
+ { pathname: '/', entries: [{ rscPath: '' }] },
+ ],
+});
diff --git a/examples/31_minimal/waku.config.ts b/examples/31_minimal/waku.config.ts
new file mode 100644
index 000000000..d2d8ee9d7
--- /dev/null
+++ b/examples/31_minimal/waku.config.ts
@@ -0,0 +1,12 @@
+// This is a temporary file while experimenting new_defineEntries
+
+import { defineConfig } from 'waku/config';
+
+export default defineConfig({
+ middleware: () => [
+ import('waku/middleware/context'),
+ import('waku/middleware/dev-server'),
+ import('waku/middleware/handler'),
+ import('waku/middleware/fallback'),
+ ],
+});
diff --git a/examples/34_functions/src/als.ts b/examples/34_functions/src/als.ts
new file mode 100644
index 000000000..92f2240f8
--- /dev/null
+++ b/examples/34_functions/src/als.ts
@@ -0,0 +1,16 @@
+import { AsyncLocalStorage } from 'node:async_hooks';
+
+type Rerender = (rscPath: string) => void;
+
+const store = new AsyncLocalStorage();
+
+export const runWithRerender = (rerender: Rerender, fn: () => T): T => {
+ return store.run(rerender, fn);
+};
+
+export const rerender = (rscPath: string) => {
+ const fn = store.getStore();
+ if (fn) {
+ fn(rscPath);
+ }
+};
diff --git a/examples/34_functions/src/components2/funcs.ts b/examples/34_functions/src/components2/funcs.ts
index c8172f348..a9e6141ae 100644
--- a/examples/34_functions/src/components2/funcs.ts
+++ b/examples/34_functions/src/components2/funcs.ts
@@ -1,13 +1,9 @@
'use server';
-import {
- rerender,
- unstable_getCustomContext as getCustomContext,
-} from 'waku/server';
+import { rerender } from '../als';
export const greet = async (name: string) => {
await Promise.resolve();
- console.log('Custom Context:', getCustomContext()); // ---> {}
return `Hello ${name} from server!`;
};
diff --git a/examples/34_functions/src/components2/funcs2.ts b/examples/34_functions/src/components2/funcs2.ts
index 1514c5c66..e9056791d 100644
--- a/examples/34_functions/src/components2/funcs2.ts
+++ b/examples/34_functions/src/components2/funcs2.ts
@@ -1,6 +1,6 @@
'use server';
-import { unstable_getCustomContext as getCustomContext } from 'waku/server';
+import { getContext } from 'waku/middleware/context';
export const greet = async (name: string) => {
await Promise.resolve();
@@ -9,6 +9,6 @@ export const greet = async (name: string) => {
export const hello = async (name: string) => {
await Promise.resolve();
- console.log('Custom Context:', getCustomContext()); // ---> {}
+ console.log('Context:', getContext());
console.log('Hello', name, '!');
};
diff --git a/examples/34_functions/src/entries.tsx b/examples/34_functions/src/entries.tsx
index 852bca1ca..b037ebb03 100644
--- a/examples/34_functions/src/entries.tsx
+++ b/examples/34_functions/src/entries.tsx
@@ -1,27 +1,29 @@
-import { defineEntries } from 'waku/server';
-import { Slot } from 'waku/client';
+import { new_defineEntries } from 'waku/minimal/server';
+import { Slot } from 'waku/minimal/client';
import App from './components2/App';
+import { runWithRerender } from './als';
-export default defineEntries(
- // renderEntries
- async (rscPath) => {
- return {
- App: ,
- };
- },
- // getBuildConfig
- async () => [{ pathname: '/', entries: [{ rscPath: '' }] }],
- // getSsrConfig
- async (pathname) => {
- switch (pathname) {
- case '/':
- return {
- rscPath: '',
- html: ,
- };
- default:
- return null;
+export default new_defineEntries({
+ unstable_handleRequest: async (input, { renderRsc, renderHtml }) => {
+ if (input.type === 'component') {
+ return renderRsc({ App: });
+ }
+ if (input.type === 'function') {
+ const elements: Record = {};
+ const rerender = (rscPath: string) => {
+ elements.App = ;
+ };
+ const value = await runWithRerender(rerender, () =>
+ input.fn(...input.args),
+ );
+ return renderRsc({ ...elements, _value: value });
+ }
+ if (input.type === 'custom' && input.pathname === '/') {
+ return renderHtml({ App: }, , '');
}
},
-);
+ unstable_getBuildConfig: async () => [
+ { pathname: '/', entries: [{ rscPath: '' }] },
+ ],
+});
diff --git a/examples/34_functions/tsconfig.json b/examples/34_functions/tsconfig.json
index 84d0d542f..33d25f480 100644
--- a/examples/34_functions/tsconfig.json
+++ b/examples/34_functions/tsconfig.json
@@ -9,7 +9,7 @@
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
- "types": ["react/experimental"],
+ "types": ["node", "react/experimental"],
"jsx": "react-jsx"
}
}
diff --git a/examples/34_functions/waku.config.ts b/examples/34_functions/waku.config.ts
new file mode 100644
index 000000000..d2d8ee9d7
--- /dev/null
+++ b/examples/34_functions/waku.config.ts
@@ -0,0 +1,12 @@
+// This is a temporary file while experimenting new_defineEntries
+
+import { defineConfig } from 'waku/config';
+
+export default defineConfig({
+ middleware: () => [
+ import('waku/middleware/context'),
+ import('waku/middleware/dev-server'),
+ import('waku/middleware/handler'),
+ import('waku/middleware/fallback'),
+ ],
+});
diff --git a/examples/35_nesting/src/components/Counter.tsx b/examples/35_nesting/src/components/Counter.tsx
index b1397860c..6de58400a 100644
--- a/examples/35_nesting/src/components/Counter.tsx
+++ b/examples/35_nesting/src/components/Counter.tsx
@@ -1,7 +1,7 @@
'use client';
import { useState, useTransition } from 'react';
-import { Slot, useRefetch } from 'waku/client';
+import { Slot, useRefetch } from 'waku/minimal/client';
export const Counter = ({ enableInnerApp }: { enableInnerApp?: boolean }) => {
const [count, setCount] = useState(0);
diff --git a/examples/38_cookies/waku.config.ts b/examples/38_cookies/waku.config.ts
index e8db632d1..395ecb424 100644
--- a/examples/38_cookies/waku.config.ts
+++ b/examples/38_cookies/waku.config.ts
@@ -1,9 +1,9 @@
/** @type {import('waku/config').Config} */
export default {
middleware: () => [
+ import('waku/middleware/context'),
import('./src/middleware/cookie.js'),
import('waku/middleware/dev-server'),
- import('waku/middleware/headers'),
import('waku/middleware/rsc'),
import('waku/middleware/ssr'),
],
diff --git a/examples/39_api/waku.config.ts b/examples/39_api/waku.config.ts
index fd6aa031f..ef7abba9f 100644
--- a/examples/39_api/waku.config.ts
+++ b/examples/39_api/waku.config.ts
@@ -1,9 +1,9 @@
/** @type {import('waku/config').Config} */
export default {
middleware: () => [
+ import('waku/middleware/context'),
import('./src/middleware/api.js'),
import('waku/middleware/dev-server'),
- import('waku/middleware/headers'),
import('waku/middleware/rsc'),
import('waku/middleware/ssr'),
],
diff --git a/packages/waku/package.json b/packages/waku/package.json
index 93046aa93..22866ef6a 100644
--- a/packages/waku/package.json
+++ b/packages/waku/package.json
@@ -42,6 +42,14 @@
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
},
+ "./minimal/server": {
+ "types": "./dist/minimal/server.d.ts",
+ "default": "./dist/minimal/server.js"
+ },
+ "./minimal/client": {
+ "types": "./dist/minimal/client.d.ts",
+ "default": "./dist/minimal/client.js"
+ },
"./server": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
diff --git a/packages/waku/src/client.ts b/packages/waku/src/client.ts
index 49c7f0c37..ba760b34a 100644
--- a/packages/waku/src/client.ts
+++ b/packages/waku/src/client.ts
@@ -1,321 +1,14 @@
-///
-'use client';
-
import {
- Component,
- createContext,
- createElement,
- memo,
- use,
- useCallback,
- useEffect,
- useState,
-} from 'react';
-import type { ReactNode } from 'react';
-import RSDWClient from 'react-server-dom-webpack/client';
-
-import { encodeRscPath, encodeFuncId } from './lib/renderers/utils.js';
-
-const { createFromFetch, encodeReply } = RSDWClient;
-
-declare global {
- interface ImportMeta {
- readonly env: Record;
- }
-}
-
-const BASE_PATH = `${import.meta.env?.WAKU_CONFIG_BASE_PATH}${
- import.meta.env?.WAKU_CONFIG_RSC_BASE
-}/`;
-
-const checkStatus = async (
- responsePromise: Promise,
-): Promise => {
- const response = await responsePromise;
- if (!response.ok) {
- const err = new Error(response.statusText);
- (err as any).statusCode = response.status;
- throw err;
- }
- return response;
-};
-
-type Elements = Promise> & {
- prev?: Record | undefined;
-};
-
-const getCached = (c: () => T, m: WeakMap
]*>/.test(data)) {
+ return;
+ }
+ headSent = true;
+ data = modifyHeadAndBody(data);
+ }
+ controller.enqueue(encoder.encode(data));
+ data = '';
+ },
+ flush(controller) {
+ if (!headSent) {
+ headSent = true;
+ data = modifyHeadAndBody(data);
+ controller.enqueue(encoder.encode(data));
+ data = '';
+ }
+ },
+ });
+};
+
+// HACK for now, do we want to use HTML parser?
+const rectifyHtml = () => {
+ const pending: Uint8Array[] = [];
+ const decoder = new TextDecoder();
+ let timer: ReturnType | undefined;
+ return new TransformStream({
+ transform(chunk, controller) {
+ if (!(chunk instanceof Uint8Array)) {
+ throw new Error('Unknown chunk type');
+ }
+ pending.push(chunk);
+ if (/<\/\w+>$/.test(decoder.decode(chunk))) {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ controller.enqueue(concatUint8Arrays(pending.splice(0)));
+ });
+ }
+ },
+ flush(controller) {
+ clearTimeout(timer);
+ if (pending.length) {
+ controller.enqueue(concatUint8Arrays(pending.splice(0)));
+ }
+ },
+ });
+};
+
+export function renderHtml(
+ config: PureConfig,
+ ctx: Pick,
+ htmlHead: string,
+ elements: Elements,
+ html: ReactNode,
+ rscPath: string,
+): ReadableStream {
+ const modules = ctx.unstable_modules;
+ if (!modules) {
+ throw new Error('handler middleware required (missing modules)');
+ }
+ const {
+ default: { renderToReadableStream },
+ } = modules.rdServer as { default: typeof RDServerType };
+ const {
+ default: { createFromReadableStream },
+ } = modules.rsdwClient as { default: typeof RSDWClientType };
+ const { ServerRootInternal: ServerRoot } =
+ modules.wakuMinimalClient as typeof WakuMinimalClientType;
+
+ const stream = renderRsc(config, ctx, elements);
+ const htmlStream = renderRscElement(config, ctx, html);
+ const isDev = !!ctx.unstable_devServer;
+ const rootDir = ctx.unstable_devServer?.rootDir || '';
+ const moduleMap = new Proxy(
+ {} as Record>,
+ {
+ get(_target, filePath: string) {
+ return new Proxy(
+ {},
+ {
+ get(_target, name: string) {
+ if (isDev) {
+ // TODO too long, we need to refactor this logic
+ let file = filePath
+ .slice(config.basePath.length)
+ .split('?')[0]!;
+ const isFsPath = file.startsWith('@fs/');
+ file = isFsPath ? file.slice('@fs'.length) : file;
+ const fileWithAbsolutePath = isFsPath
+ ? file
+ : encodeFilePathToAbsolute(joinPath(rootDir, file));
+ const wakuDist = joinPath(
+ fileURLToFilePath(import.meta.url),
+ '../../..',
+ );
+ if (fileWithAbsolutePath.startsWith(wakuDist)) {
+ const id =
+ 'waku' +
+ fileWithAbsolutePath
+ .slice(wakuDist.length)
+ .replace(/\.\w+$/, '');
+ (globalThis as any).__WAKU_CLIENT_CHUNK_LOAD__(id);
+ return { id, chunks: [id], name };
+ }
+ const id = filePathToFileURL(file);
+ (globalThis as any).__WAKU_CLIENT_CHUNK_LOAD__(id);
+ return { id, chunks: [id], name };
+ }
+ // !isDev
+ const id = filePath.slice(config.basePath.length);
+ (globalThis as any).__WAKU_CLIENT_CHUNK_LOAD__(id);
+ return { id, chunks: [id], name };
+ },
+ },
+ );
+ },
+ },
+ );
+ const [stream1, stream2] = stream.tee();
+ const elementsPromise: Promise = createFromReadableStream(stream1, {
+ ssrManifest: { moduleMap, moduleLoading: null },
+ });
+ const htmlNode: Promise = createFromReadableStream(htmlStream, {
+ ssrManifest: { moduleMap, moduleLoading: null },
+ });
+ const readable = streamFromPromise(
+ renderToReadableStream(
+ createElement(
+ ServerRoot as FunctionComponent<
+ Omit, 'children'>
+ >,
+ { elements: elementsPromise },
+ htmlNode as any,
+ ),
+ {
+ onError(err: unknown) {
+ console.error(err);
+ },
+ },
+ ),
+ )
+ .pipeThrough(rectifyHtml())
+ .pipeThrough(
+ injectHtmlHead(
+ config.basePath + config.rscBase + '/' + encodeRscPath(rscPath),
+ htmlHead,
+ isDev ? `${config.basePath}${config.srcDir}/${SRC_MAIN}` : '',
+ ),
+ )
+ .pipeThrough(injectRSCPayload(stream2));
+ return readable;
+}
diff --git a/packages/waku/src/lib/renderers/rsc-renderer.ts b/packages/waku/src/lib/renderers/rsc-renderer.ts
index e9aac4e97..45ca31495 100644
--- a/packages/waku/src/lib/renderers/rsc-renderer.ts
+++ b/packages/waku/src/lib/renderers/rsc-renderer.ts
@@ -3,12 +3,11 @@ import type { default as RSDWServerType } from 'react-server-dom-webpack/server.
import { unstable_getPlatformObject } from '../../server.js';
import type {
- EntriesDev,
- EntriesPrd,
setAllEnvInternal as setAllEnvInternalType,
runWithRenderStoreInternal as runWithRenderStoreInternalType,
} from '../../server.js';
-import type { ResolvedConfig } from '../config.js';
+import type { EntriesDev, EntriesPrd } from '../../minimal/server.js';
+import type { PureConfig } from '../config.js';
import { filePathToFileURL } from '../utils/path.js';
import { streamToArrayBuffer } from '../utils/stream.js';
import { decodeFuncId } from '../renderers/utils.js';
@@ -25,12 +24,12 @@ const resolveClientEntryForPrd = (id: string, config: { basePath: string }) => {
export type RenderRscArgs = {
env: Record;
- config: Omit;
+ config: PureConfig;
rscPath: string;
context: Record | undefined;
// TODO we hope to get only decoded one
decodedBody?: unknown;
- body?: ReadableStream | undefined;
+ body?: ReadableStream | null;
contentType?: string | undefined;
moduleIdCallback?: ((id: string) => void) | undefined;
onError?: (err: unknown) => void;
@@ -231,7 +230,7 @@ export async function renderRsc(
type GetBuildConfigArgs = {
env: Record;
- config: Omit;
+ config: PureConfig;
};
type GetBuildConfigOpts = { entries: EntriesPrd };
@@ -300,7 +299,7 @@ export async function getBuildConfig(
export type GetSsrConfigArgs = {
env: Record;
- config: Omit;
+ config: PureConfig;
pathname: string;
searchParams: URLSearchParams;
};
diff --git a/packages/waku/src/lib/renderers/rsc.ts b/packages/waku/src/lib/renderers/rsc.ts
new file mode 100644
index 000000000..c54b61a3c
--- /dev/null
+++ b/packages/waku/src/lib/renderers/rsc.ts
@@ -0,0 +1,152 @@
+import type { ReactNode } from 'react';
+import type { default as RSDWServerType } from 'react-server-dom-webpack/server.edge';
+
+import type { PureConfig } from '../config.js';
+// TODO move types somewhere
+import type { HandlerContext } from '../middleware/types.js';
+import { filePathToFileURL } from '../utils/path.js';
+import { streamToArrayBuffer } from '../utils/stream.js';
+import { bufferToString, parseFormData } from '../utils/buffer.js';
+
+const resolveClientEntryForPrd = (id: string, config: { basePath: string }) => {
+ return config.basePath + id + '.js';
+};
+
+export function renderRsc(
+ config: PureConfig,
+ ctx: Pick,
+ elements: Record,
+ moduleIdCallback?: (id: string) => void,
+): ReadableStream {
+ const modules = ctx.unstable_modules;
+ if (!modules) {
+ throw new Error('handler middleware required (missing modules)');
+ }
+ const {
+ default: {
+ renderToReadableStream,
+ // decodeReply,
+ },
+ } = modules.rsdwServer as { default: typeof RSDWServerType };
+ const resolveClientEntry = ctx.unstable_devServer
+ ? ctx.unstable_devServer.resolveClientEntry
+ : resolveClientEntryForPrd;
+ const clientBundlerConfig = new Proxy(
+ {},
+ {
+ get(_target, encodedId: string) {
+ const [file, name] = encodedId.split('#') as [string, string];
+ const id = resolveClientEntry(file, config);
+ moduleIdCallback?.(id);
+ return { id, chunks: [id], name, async: true };
+ },
+ },
+ );
+ return renderToReadableStream(elements, clientBundlerConfig);
+}
+
+export function renderRscElement(
+ config: PureConfig,
+ ctx: Pick,
+ element: ReactNode,
+): ReadableStream {
+ const modules = ctx.unstable_modules;
+ if (!modules) {
+ throw new Error('handler middleware required (missing modules)');
+ }
+ const {
+ default: {
+ renderToReadableStream,
+ // decodeReply,
+ },
+ } = modules.rsdwServer as { default: typeof RSDWServerType };
+ const resolveClientEntry = ctx.unstable_devServer
+ ? ctx.unstable_devServer.resolveClientEntry
+ : resolveClientEntryForPrd;
+ const clientBundlerConfig = new Proxy(
+ {},
+ {
+ get(_target, encodedId: string) {
+ const [file, name] = encodedId.split('#') as [string, string];
+ const id = resolveClientEntry(file, config);
+ return { id, chunks: [id], name, async: true };
+ },
+ },
+ );
+ return renderToReadableStream(element, clientBundlerConfig);
+}
+
+export async function collectClientModules(
+ config: PureConfig,
+ rsdwServer: { default: typeof RSDWServerType },
+ elements: Record,
+): Promise {
+ const {
+ default: { renderToReadableStream },
+ } = rsdwServer;
+ const idSet = new Set();
+ const clientBundlerConfig = new Proxy(
+ {},
+ {
+ get(_target, encodedId: string) {
+ const [file, name] = encodedId.split('#') as [string, string];
+ const id = resolveClientEntryForPrd(file, config);
+ idSet.add(id);
+ return { id, chunks: [id], name, async: true };
+ },
+ },
+ );
+ const readable = renderToReadableStream(elements, clientBundlerConfig);
+ await new Promise((resolve, reject) => {
+ const writable = new WritableStream({
+ close() {
+ resolve();
+ },
+ abort(reason) {
+ reject(reason);
+ },
+ });
+ readable.pipeTo(writable).catch(reject);
+ });
+ return Array.from(idSet);
+}
+
+export async function decodeBody(
+ ctx: Pick,
+): Promise {
+ const isDev = !!ctx.unstable_devServer;
+ const modules = ctx.unstable_modules;
+ if (!modules) {
+ throw new Error('handler middleware required (missing modules)');
+ }
+ const {
+ default: { decodeReply },
+ } = modules.rsdwServer as { default: typeof RSDWServerType };
+ const serverBundlerConfig = new Proxy(
+ {},
+ {
+ get(_target, encodedId: string) {
+ const [fileId, name] = encodedId.split('#') as [string, string];
+ const id = isDev ? filePathToFileURL(fileId) : fileId + '.js';
+ return { id, chunks: [id], name, async: true };
+ },
+ },
+ );
+ let decodedBody: unknown = ctx.req.url.searchParams;
+ if (ctx.req.body) {
+ const bodyBuf = await streamToArrayBuffer(ctx.req.body);
+ const contentType = ctx.req.headers['content-type'];
+ if (
+ typeof contentType === 'string' &&
+ contentType.startsWith('multipart/form-data')
+ ) {
+ // XXX This doesn't support streaming unlike busboy
+ const formData = await parseFormData(bodyBuf, contentType);
+ decodedBody = await decodeReply(formData, serverBundlerConfig);
+ } else if (bodyBuf.byteLength > 0) {
+ const bodyStr = bufferToString(bodyBuf);
+ decodedBody = await decodeReply(bodyStr, serverBundlerConfig);
+ }
+ }
+ return decodedBody;
+}
diff --git a/packages/waku/src/lib/utils/stream.ts b/packages/waku/src/lib/utils/stream.ts
index 5dfa4f152..1e32da6cc 100644
--- a/packages/waku/src/lib/utils/stream.ts
+++ b/packages/waku/src/lib/utils/stream.ts
@@ -66,3 +66,23 @@ export const stringToStream = (str: string): ReadableStream => {
},
});
};
+
+export const streamFromPromise = (promise: Promise) =>
+ new ReadableStream({
+ async start(controller) {
+ try {
+ const stream = await promise;
+ const reader = stream.getReader();
+ let result: ReadableStreamReadResult;
+ do {
+ result = await reader.read();
+ if (result.value) {
+ controller.enqueue(result.value);
+ }
+ } while (!result.done);
+ controller.close();
+ } catch (err) {
+ controller.error(err);
+ }
+ },
+ });
diff --git a/packages/waku/src/middleware/context.ts b/packages/waku/src/middleware/context.ts
new file mode 100644
index 000000000..19fbf3008
--- /dev/null
+++ b/packages/waku/src/middleware/context.ts
@@ -0,0 +1,2 @@
+export { context as default } from '../lib/middleware/context.js';
+export { getContext } from '../lib/middleware/context.js';
diff --git a/packages/waku/src/middleware/handler.ts b/packages/waku/src/middleware/handler.ts
new file mode 100644
index 000000000..d95239b5a
--- /dev/null
+++ b/packages/waku/src/middleware/handler.ts
@@ -0,0 +1 @@
+export { handler as default } from '../lib/middleware/handler.js';
diff --git a/packages/waku/src/middleware/headers.ts b/packages/waku/src/middleware/headers.ts
deleted file mode 100644
index 6aeae397a..000000000
--- a/packages/waku/src/middleware/headers.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { headers as default } from '../lib/middleware/headers.js';
diff --git a/packages/waku/src/minimal/client.ts b/packages/waku/src/minimal/client.ts
new file mode 100644
index 000000000..da3915a13
--- /dev/null
+++ b/packages/waku/src/minimal/client.ts
@@ -0,0 +1,335 @@
+///
+'use client';
+
+import {
+ Component,
+ createContext,
+ createElement,
+ memo,
+ use,
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+import type { ReactNode } from 'react';
+import RSDWClient from 'react-server-dom-webpack/client';
+
+import { encodeRscPath, encodeFuncId } from '../lib/renderers/utils.js';
+
+const { createFromFetch, encodeReply } = RSDWClient;
+
+declare global {
+ interface ImportMeta {
+ readonly env: Record;
+ }
+}
+
+const BASE_PATH = `${import.meta.env?.WAKU_CONFIG_BASE_PATH}${
+ import.meta.env?.WAKU_CONFIG_RSC_BASE
+}/`;
+
+const checkStatus = async (
+ responsePromise: Promise,
+): Promise => {
+ const response = await responsePromise;
+ if (!response.ok) {
+ const err = new Error(response.statusText);
+ (err as any).statusCode = response.status;
+ throw err;
+ }
+ return response;
+};
+
+type Elements = Promise> & {
+ prev?: Record | undefined;
+};
+
+const getCached = (c: () => T, m: WeakMap';
+
+const injectHtmlHead = (
+ urlForFakeFetch: string,
+ htmlHead: string,
+ mainJsPath: string, // for DEV only, pass `''` for PRD
+) => {
+ const modifyHeadAndBody = (data: string) => {
+ const closingHeadIndex = data.indexOf(CLOSING_HEAD);
+ let [head, body] =
+ closingHeadIndex === -1
+ ? ['
' + CLOSING_HEAD, data]
+ : [
+ data.slice(0, closingHeadIndex + CLOSING_HEAD.length),
+ data.slice(closingHeadIndex + CLOSING_HEAD.length),
+ ];
+ head =
+ head.slice(0, -CLOSING_HEAD.length) +
+ DEFAULT_HTML_HEAD +
+ htmlHead +
+ CLOSING_HEAD;
+ const matchPrefetched = head.match(
+ // HACK This is very brittle
+ /(.*` +
+ CLOSING_HEAD;
+ }
+ if (mainJsPath) {
+ const closingBodyIndex = body.indexOf(CLOSING_BODY);
+ const [firstPart, secondPart] =
+ closingBodyIndex === -1
+ ? [body, '']
+ : [body.slice(0, closingBodyIndex), body.slice(closingBodyIndex)];
+ body =
+ firstPart +
+ `` +
+ secondPart;
+ }
+ return head + body;
+ };
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+ let headSent = false;
+ let data = '';
+ return new TransformStream({
+ transform(chunk, controller) {
+ if (!(chunk instanceof Uint8Array)) {
+ throw new Error('Unknown chunk type');
+ }
+ data += decoder.decode(chunk);
+ if (!headSent) {
+ if (!/