Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions packages/void/src/pages/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { withLoaderNotFoundBoundary } from './not-found-context.ts';
import { escapeScriptJson } from './script-json.ts';
import { serializePageData } from './serialize.ts';
import type { DevCssAsset, PageRenderMode } from './types.ts';
import { decodePagePath } from './url.ts';
import { ValidationError } from './validation-error.ts';

export { ValidationError };
Expand Down Expand Up @@ -299,8 +300,25 @@ function pageJsonResponse(c: CloudContext, pageObj: PageObject): Response {
});
}

function requestPathWithSearch(c: CloudContext): string {
return c.req.path + new URL(c.req.url).search;
/**
* The URL in the address bar, which a rewrite keeps while it serves another
* route. The path is decoded like c.req.path.
*/
function requestPathWithSearch(c: CloudContext, withoutActionKey = false): string {
// Build-time renders run without the routing middleware that adds originalUrl.
const original = c.originalUrl?.();
const requestSearch = new URL(c.req.url).search;
if (!original) {
return c.req.path + (withoutActionKey ? searchWithoutActionKey(requestSearch) : requestSearch);
}
// The action key is in the query this request carries. It is also in the
// original query only when the rewrite kept that query; a destination with
// its own query leaves the original's parameters as they were.
const search =
withoutActionKey && original.search === requestSearch
? searchWithoutActionKey(original.search)
: original.search;
return decodePagePath(original.pathname) + search;
}

function requestParams(c: CloudContext): Record<string, string> {
Expand Down Expand Up @@ -908,9 +926,7 @@ async function runPageMutation(
props,
params: requestParams(c),
shared,
url: hasActionKey(c.req.raw)
? c.req.path + searchWithoutActionKey(new URL(c.req.url).search)
: requestPathWithSearch(c),
url: requestPathWithSearch(c, hasActionKey(c.req.raw)),
errors,
...(head && { head }),
};
Expand Down
2 changes: 1 addition & 1 deletion packages/void/test/unit/pages-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,7 @@ describe('createVoidRouter', () => {
const response = await app.fetch(new Request(new URL(url, window.location.origin), init));
expect(response.status).toBe(200);
expect(decodePageData(await response.clone().json())).toMatchObject({
url: '/books?category=internal',
url: '/books?category=public',
});
return response;
}),
Expand Down
42 changes: 42 additions & 0 deletions packages/void/test/unit/routing-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,48 @@ describe('typed Pages and SSR execution', () => {
expect((decodePageData(await response.json()) as { url: string }).url).toBe('/named?page=2');
});
});
it.each([
['middleware', 'GET', '/app/account?tab=1', '/maintenance', '/app/account?tab=1'],
['rule', 'GET', '/app/account?tab=1', '/maintenance', '/app/account?tab=1'],
['middleware', 'GET', '/app/caf%C3%A9', '/maintenance', '/app/café'],
['middleware', 'GET', '/app/account?tab=1', '/maintenance?mode=read', '/app/account?tab=1'],
['middleware', 'POST', '/app/account?save&tab=1', '/maintenance', '/app/account?tab=1'],
['rule', 'POST', '/app/account?save&tab=1', '/maintenance', '/app/account?tab=1'],
// The destination's query selects the action; the original query has no key.
['middleware', 'POST', '/app/account?tab=1', '/maintenance?save&tab=1', '/app/account?tab=1'],
['rule', 'POST', '/app/account?tab=1', '/maintenance?save&tab=1', '/app/account?tab=1'],
])(
'reports the requested URL after a %s rewrite of %s %s to %s',
async (kind, method, path, destination, url) => {
const app = new Hono();
const runtime = createRoutingRuntime(app, { nodeTarget: true });
app.use(runtime.contextMiddleware);
app.use(createNamedActionMarker(matchedRoutes));
if (kind === 'middleware') {
app.use('/app/*', (c) => (c as RoutingContext).rewrite(destination));
} else {
app.use(
runtime.routingMiddleware([{ source: '/app/*', destination, status: 200, force: true }]),
);
}
app.get('/maintenance', createPageGetHandler(pageOptions));
app.post(
'/maintenance',
createPageMutationHandler({
...pageOptions,
load: async () => ({ actions: { save: () => undefined } }),
namedActions: true,
}),
);
const response = await app.fetch(
new Request('https://app.test' + path, {
method,
headers: { 'x-voidpages': 'true', 'X-VoidPages-Version': PAGES_PROTOCOL_VERSION },
}),
);
expect((decodePageData(await response.json()) as { url: string }).url).toBe(url);
},
);
it('executes the real page loader and protocol', async () => {
const app = new Hono();
app.get(
Expand Down
Loading