Skip to content

Commit

Permalink
Merge branch 'main' into canary-ci-2-fix-2
Browse files Browse the repository at this point in the history
  • Loading branch information
dai-shi committed Dec 14, 2024
2 parents 6a56265 + 2172fc7 commit c12e62f
Show file tree
Hide file tree
Showing 14 changed files with 196 additions and 42 deletions.
2 changes: 1 addition & 1 deletion e2e/fixtures/use-router/src/TestRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function TestRouter() {
const router = useRouter_UNSTABLE();
const params = new URLSearchParams(router.query);
const queryCount = parseInt(params.get('count') || '0');
const hashCount = parseInt(router.hash?.substr(1) || '0');
const hashCount = parseInt(router.hash?.slice(1) || '0');
return (
<>
<p data-testid="path">Path: {router.path}</p>
Expand Down
12 changes: 12 additions & 0 deletions e2e/fixtures/use-router/src/components/my-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';

import { useRouter_UNSTABLE } from 'waku';

export const MyButton = () => {
const router = useRouter_UNSTABLE();
return (
<button onClick={() => router.push(`/static`)}>
Static router.push button
</button>
);
};
3 changes: 3 additions & 0 deletions e2e/fixtures/use-router/src/pages/dynamic.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Link } from 'waku';
import TestRouter from '../TestRouter.js';

import { MyButton } from '../components/my-button.js';

const Page = () => (
<>
<h1>Dynamic</h1>
<p>
<Link to="/static">Go to static</Link>
<MyButton />
</p>
<TestRouter />
</>
Expand Down
8 changes: 8 additions & 0 deletions e2e/use-router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ test.describe('useRouter', async () => {
await expect(page.getByTestId('path')).toHaveText('Path: /dynamic');
await terminate(pid!);
});
test('router.push changes the page', async ({ page }) => {
const [port, pid] = await start();
await page.goto(`http://localhost:${port}/dynamic`);
await page.click('text=Static router.push button');
await expect(page.getByRole('heading', { name: 'Static' })).toBeVisible();
await expect(page.getByTestId('path')).toHaveText('Path: /static');
await terminate(pid!);
});
});

test.describe('retrieves query variables', () => {
Expand Down
14 changes: 12 additions & 2 deletions examples/36_form/src/entries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ export default defineEntries({
);
return renderRsc({ ...elements, _value: value });
}
if (input.type === 'custom' && input.pathname === '/') {
return renderHtml({ App: <App name="Waku" /> }, <Slot id="App" />, '');
if (
(input.type === 'action' || input.type === 'custom') &&
input.pathname === '/'
) {
const actionResult =
input.type === 'action' ? await input.fn() : undefined;
return renderHtml(
{ App: <App name="Waku" /> },
<Slot id="App" />,
'',
actionResult,
);
}
},
getBuildConfig: async () => [{ pathSpec: [], entries: [{ rscPath: '' }] }],
Expand Down
6 changes: 3 additions & 3 deletions packages/waku/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@
"vitest": "^2.1.8"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-server-dom-webpack": "^19.0.0"
"react": "~19.0.0",
"react-dom": "~19.0.0",
"react-server-dom-webpack": "~19.0.0"
}
}
15 changes: 14 additions & 1 deletion packages/waku/src/lib/middleware/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { PureConfig } from '../config.js';
import { setAllEnvInternal } from '../../server.js';
import type { HandleRequest } from '../types.js';
import type { Middleware, HandlerContext } from './types.js';
import { renderRsc, decodeBody } from '../renderers/rsc.js';
import { renderRsc, decodeBody, decodePostAction } from '../renderers/rsc.js';
import { renderHtml } from '../renderers/html.js';
import { decodeRscPath, decodeFuncId } from '../renderers/utils.js';
import { filePathToFileURL, getPathMapping } from '../utils/path.js';
Expand Down Expand Up @@ -47,6 +47,17 @@ const getInput = async (
}
return { type: 'component', rscPath, rscParams: decodedBody, req: ctx.req };
}
if (ctx.req.method === 'POST') {
const postAction = await decodePostAction(ctx);
if (postAction) {
return {
type: 'action',
fn: postAction,
pathname: '/' + ctx.req.url.pathname.slice(config.basePath.length),
req: ctx.req,
};
}
}
return {
type: 'custom',
pathname: '/' + ctx.req.url.pathname.slice(config.basePath.length),
Expand Down Expand Up @@ -118,6 +129,7 @@ export const handler: Middleware = (options) => {
elements: Record<string, ReactNode>,
html: ReactNode,
rscPath: string,
actionResult?: unknown,
) => {
const readable = renderHtml(
config,
Expand All @@ -126,6 +138,7 @@ export const handler: Middleware = (options) => {
elements,
html,
rscPath,
actionResult,
);
const headers = { 'content-type': 'text/html; charset=utf-8' };
return {
Expand Down
33 changes: 20 additions & 13 deletions packages/waku/src/lib/renderers/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SRC_MAIN } from '../constants.js';
import { concatUint8Arrays, streamFromPromise } from '../utils/stream.js';
import { filePathToFileURL } from '../utils/path.js';
import { encodeRscPath } from './utils.js';
import { renderRsc, renderRscElement } from './rsc.js';
import { renderRsc, renderRscElement, getExtractFormState } from './rsc.js';
// TODO move types somewhere
import type { HandlerContext } from '../middleware/types.js';

Expand Down Expand Up @@ -165,6 +165,7 @@ export function renderHtml(
elements: Elements,
html: ReactNode,
rscPath: string,
actionResult?: unknown,
): ReadableStream {
const modules = ctx.unstable_modules;
if (!modules) {
Expand Down Expand Up @@ -220,19 +221,25 @@ export function renderHtml(
serverConsumerManifest: { moduleMap, moduleLoading: null },
});
const readable = streamFromPromise(
renderToReadableStream(
createElement(
ServerRoot as FunctionComponent<
Omit<ComponentProps<typeof ServerRoot>, 'children'>
>,
{ elements: elementsPromise },
htmlNode as any,
),
{
onError(err: unknown) {
console.error(err);
(actionResult === undefined
? Promise.resolve(null)
: getExtractFormState(ctx)(actionResult)
).then((formState) =>
renderToReadableStream(
createElement(
ServerRoot as FunctionComponent<
Omit<ComponentProps<typeof ServerRoot>, 'children'>
>,
{ elements: elementsPromise },
htmlNode as any,
),
{
formState,
onError(err: unknown) {
console.error(err);
},
},
},
),
),
)
.pipeThrough(rectifyHtml())
Expand Down
76 changes: 68 additions & 8 deletions packages/waku/src/lib/renderers/rsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ export function renderRsc(
throw new Error('handler middleware required (missing modules)');
}
const {
default: {
renderToReadableStream,
// decodeReply,
},
default: { renderToReadableStream },
} = modules.rsdwServer as { default: typeof RSDWServerType };
const resolveClientEntry = ctx.unstable_devServer
? ctx.unstable_devServer.resolveClientEntry
Expand Down Expand Up @@ -55,10 +52,7 @@ export function renderRscElement(
throw new Error('handler middleware required (missing modules)');
}
const {
default: {
renderToReadableStream,
// decodeReply,
},
default: { renderToReadableStream },
} = modules.rsdwServer as { default: typeof RSDWServerType };
const resolveClientEntry = ctx.unstable_devServer
? ctx.unstable_devServer.resolveClientEntry
Expand Down Expand Up @@ -150,3 +144,69 @@ export async function decodeBody(
}
return decodedBody;
}

const EXTRACT_FORM_STATE_SYMBOL = Symbol('EXTRACT_FORM_STATE');
type ExtractFormState = (
actionResult: unknown,
) => ReturnType<(typeof RSDWServerType)['decodeFormState']>;

const setExtractFormState = (
ctx: object,
extractFormState: ExtractFormState,
) => {
(
ctx as unknown as Record<typeof EXTRACT_FORM_STATE_SYMBOL, ExtractFormState>
)[EXTRACT_FORM_STATE_SYMBOL] = extractFormState;
};

export const getExtractFormState = (ctx: object): ExtractFormState => {
const extractFormState = (
ctx as unknown as Record<
typeof EXTRACT_FORM_STATE_SYMBOL,
ExtractFormState | undefined
>
)[EXTRACT_FORM_STATE_SYMBOL];
if (!extractFormState) {
throw new Error('extractFormState not set');
}
return extractFormState;
};

export async function decodePostAction(
ctx: Pick<HandlerContext, 'unstable_modules' | 'unstable_devServer' | 'req'>,
): Promise<(() => Promise<unknown>) | null> {
const isDev = !!ctx.unstable_devServer;
const modules = ctx.unstable_modules;
if (!modules) {
throw new Error('handler middleware required (missing modules)');
}
const {
default: { decodeAction, decodeFormState },
} = modules.rsdwServer as { default: typeof RSDWServerType };
if (ctx.req.body) {
const contentType = ctx.req.headers['content-type'];
if (
typeof contentType === 'string' &&
contentType.startsWith('multipart/form-data')
) {
const bodyBuf = await streamToArrayBuffer(ctx.req.body);
// XXX This doesn't support streaming unlike busboy
const formData = await parseFormData(bodyBuf, contentType);
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 };
},
},
);
setExtractFormState(ctx, (actionResult) =>
decodeFormState(actionResult, formData, serverBundlerConfig),
);
return decodeAction(formData, serverBundlerConfig);
}
}
return null;
}
8 changes: 8 additions & 0 deletions packages/waku/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { PathSpec } from '../lib/utils/path.js';

type Elements = Record<string, ReactNode>;

// This API is still unstable
export type HandleRequest = (
input: (
| { type: 'component'; rscPath: string; rscParams: unknown }
Expand All @@ -13,6 +14,11 @@ export type HandleRequest = (
fn: (...args: unknown[]) => Promise<unknown>;
args: unknown[];
}
| {
type: 'action';
fn: () => Promise<unknown>;
pathname: string;
}
| { type: 'custom'; pathname: string }
) & {
req: HandlerReq;
Expand All @@ -23,13 +29,15 @@ export type HandleRequest = (
elements: Elements,
html: ReactNode,
rscPath: string,
actionResult?: unknown,
) => {
body: ReadableStream;
headers: Record<'content-type', string>;
};
},
) => Promise<ReadableStream | HandlerRes | null | undefined>;

// This API is still unstable
export type BuildConfig = {
pathSpec: PathSpec;
isStatic?: boolean | undefined;
Expand Down
15 changes: 6 additions & 9 deletions packages/waku/src/router/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,17 +354,14 @@ const InnerRouter = ({
(route, options) => {
const { skipRefetch } = options || {};
startTransition(() => {
if (!staticPathSet.has(route.path) && !skipRefetch) {
const skip = Array.from(cachedIdSet);
const rscPath = encodeRoutePath(route.path);
const rscParams = createRscParams(route.query, skip);
refetch(rscPath, rscParams);
}
setRoute(route);
});
if (staticPathSet.has(route.path)) {
return;
}
if (!skipRefetch) {
const skip = Array.from(cachedIdSet);
const rscPath = encodeRoutePath(route.path);
const rscParams = createRscParams(route.query, skip);
refetch(rscPath, rscParams);
}
},
[refetch, cachedIdSet, staticPathSet],
);
Expand Down
6 changes: 4 additions & 2 deletions packages/waku/src/router/define-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ globalThis.__WAKU_ROUTER_PREFETCH__ = (path) => {
rendered = true;
return renderRsc({ ...(await elementsPromise), _value: value });
}
if (input.type === 'custom') {
if (input.type === 'action' || input.type === 'custom') {
let pathname = input.pathname;
const query = input.req.url.searchParams.toString();
const pathStatus = await existsPath(pathname);
Expand All @@ -320,7 +320,9 @@ globalThis.__WAKU_ROUTER_PREFETCH__ = (path) => {
const html = createElement(ServerRouter, {
route: { path: pathname, query, hash: '' },
});
return renderHtml(entries, html, rscPath);
const actionResult =
input.type === 'action' ? await input.fn() : undefined;
return renderHtml(entries, html, rscPath, actionResult);
}
},
getBuildConfig,
Expand Down
Loading

0 comments on commit c12e62f

Please sign in to comment.