Skip to content

[feat] transformPage #3914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 15, 2022
Merged
5 changes: 5 additions & 0 deletions .changeset/forty-cycles-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Add `transformPage` option to `resolve`
7 changes: 5 additions & 2 deletions documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface RequestEvent {

export interface ResolveOpts {
ssr?: boolean;
transformPage?: ({ html }: { html: string }) => string;
}

export interface Handle {
Expand Down Expand Up @@ -58,13 +59,15 @@ You can add call multiple `handle` functions with [the `sequence` helper functio

`resolve` also supports a second, optional parameter that gives you more control over how the response will be rendered. That parameter is an object that can have the following fields:

- `ssr` (boolean, default `true`) — specifies whether the page will be loaded and rendered on the server.
- `ssr: boolean` (default `true`) — if `false`, renders an empty 'shell' page instead of server-side rendering
- `transformPage(opts: { html: string }): string` — applies custom transforms to HTML

```js
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/admin')
ssr: !event.url.pathname.startsWith('/admin'),
transformPage: ({ html }) => html.replace('old', 'new')
});

return response;
Expand Down
27 changes: 21 additions & 6 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { normalize_path } from '../../utils/url.js';

const DATA_SUFFIX = '/__data.json';

/** @param {{ html: string }} opts */
const default_transform = ({ html }) => html;

/** @type {import('types/internal').Respond} */
export async function respond(request, options, state = {}) {
const url = new URL(request.url);
Expand Down Expand Up @@ -91,13 +94,22 @@ export async function respond(request, options, state = {}) {
rawBody: body_getter
});

let ssr = true;
/** @type {import('types/hooks').RequiredResolveOptions} */
let resolve_opts = {
ssr: true,
transformPage: default_transform
};

try {
const response = await options.hooks.handle({
event,
resolve: async (event, opts) => {
if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr);
if (opts) {
resolve_opts = {
ssr: opts.ssr !== false,
transformPage: opts.transformPage || default_transform
};
}

if (state.prerender && state.prerender.fallback) {
return await render_response({
Expand All @@ -110,7 +122,10 @@ export async function respond(request, options, state = {}) {
stuff: {},
status: 200,
branch: [],
ssr: false
resolve_opts: {
...resolve_opts,
ssr: false
}
});
}

Expand Down Expand Up @@ -169,7 +184,7 @@ export async function respond(request, options, state = {}) {
response =
route.type === 'endpoint'
? await render_endpoint(event, await route.load())
: await render_page(event, route, options, state, ssr);
: await render_page(event, route, options, state, resolve_opts);
}

if (response) {
Expand Down Expand Up @@ -221,7 +236,7 @@ export async function respond(request, options, state = {}) {
$session,
status: 404,
error: new Error(`Not found: ${event.url.pathname}`),
ssr
resolve_opts
});
}

Expand Down Expand Up @@ -257,7 +272,7 @@ export async function respond(request, options, state = {}) {
$session,
status: 500,
error,
ssr
resolve_opts
});
} catch (/** @type {unknown} */ e) {
const error = coalesce_to_error(e);
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { respond } from './respond.js';
* @param {import('types/internal').SSRPage} route
* @param {import('types/internal').SSROptions} options
* @param {import('types/internal').SSRState} state
* @param {boolean} ssr
* @param {import('types/hooks').RequiredResolveOptions} resolve_opts
* @returns {Promise<Response | undefined>}
*/
export async function render_page(event, route, options, state, ssr) {
export async function render_page(event, route, options, state, resolve_opts) {
if (state.initiator === route) {
// infinite request cycle detected
return new Response(`Not found: ${event.url.pathname}`, {
Expand All @@ -35,9 +35,9 @@ export async function render_page(event, route, options, state, ssr) {
options,
state,
$session,
resolve_opts,
route,
params: event.params, // TODO this is redundant
ssr
params: event.params // TODO this is redundant
});

if (response) {
Expand Down
14 changes: 8 additions & 6 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const updated = {
* error?: Error;
* url: URL;
* params: Record<string, string>;
* ssr: boolean;
* resolve_opts: import('types/hooks').RequiredResolveOptions;
* stuff: Record<string, any>;
* }} opts
*/
Expand All @@ -39,7 +39,7 @@ export async function render_response({
error,
url,
params,
ssr,
resolve_opts,
stuff
}) {
if (state.prerender) {
Expand Down Expand Up @@ -71,7 +71,7 @@ export async function render_response({
error.stack = options.get_stack(error);
}

if (ssr) {
if (resolve_opts.ssr) {
branch.forEach(({ node, props, loaded, fetched, uses_credentials }) => {
if (node.css) node.css.forEach((url) => stylesheets.add(url));
if (node.js) node.js.forEach((url) => modulepreloads.add(url));
Expand Down Expand Up @@ -167,9 +167,9 @@ export async function render_response({
throw new Error(`Failed to serialize session data: ${error.message}`);
})},
route: ${!!page_config.router},
spa: ${!ssr},
spa: ${!resolve_opts.ssr},
trailing_slash: ${s(options.trailing_slash)},
hydrate: ${ssr && page_config.hydrate ? `{
hydrate: ${resolve_opts.ssr && page_config.hydrate ? `{
status: ${status},
error: ${serialize_error(error)},
nodes: [
Expand Down Expand Up @@ -295,7 +295,9 @@ export async function render_response({
const assets =
options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');

const html = options.template({ head, body, assets, nonce: /** @type {string} */ (csp.nonce) });
const html = resolve_opts.transformPage({
html: options.template({ head, body, assets, nonce: /** @type {string} */ (csp.nonce) })
});

const headers = new Headers({
'content-type': 'text/html',
Expand Down
12 changes: 6 additions & 6 deletions packages/kit/src/runtime/server/page/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ import { coalesce_to_error } from '../../../utils/error.js';
* options: SSROptions;
* state: SSRState;
* $session: any;
* resolve_opts: import('types/hooks').RequiredResolveOptions;
* route: import('types/internal').SSRPage;
* params: Record<string, string>;
* ssr: boolean;
* }} opts
* @returns {Promise<Response | undefined>}
*/
export async function respond(opts) {
const { event, options, state, $session, route, ssr } = opts;
const { event, options, state, $session, route, resolve_opts } = opts;

/** @type {Array<SSRNode | undefined>} */
let nodes;

if (!ssr) {
if (!resolve_opts.ssr) {
return await render_response({
...opts,
branch: [],
Expand Down Expand Up @@ -58,7 +58,7 @@ export async function respond(opts) {
$session,
status: 500,
error,
ssr
resolve_opts
});
}

Expand Down Expand Up @@ -89,7 +89,7 @@ export async function respond(opts) {

let stuff = {};

ssr: if (ssr) {
ssr: if (resolve_opts.ssr) {
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];

Expand Down Expand Up @@ -194,7 +194,7 @@ export async function respond(opts) {
$session,
status,
error,
ssr
resolve_opts
}),
set_cookie_headers
);
Expand Down
14 changes: 11 additions & 3 deletions packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ import { coalesce_to_error } from '../../../utils/error.js';
* $session: any;
* status: number;
* error: Error;
* ssr: boolean;
* resolve_opts: import('types/hooks').RequiredResolveOptions;
* }} opts
*/
export async function respond_with_error({ event, options, state, $session, status, error, ssr }) {
export async function respond_with_error({
event,
options,
state,
$session,
status,
error,
resolve_opts
}) {
try {
const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
const default_error = await options.manifest._.nodes[1](); // 1 is always the root error
Expand Down Expand Up @@ -75,7 +83,7 @@ export async function respond_with_error({ event, options, state, $session, stat
branch: [layout_loaded, error_loaded],
url: event.url,
params,
ssr
resolve_opts
});
} catch (err) {
const error = coalesce_to_error(err);
Expand Down
1 change: 1 addition & 0 deletions packages/kit/test/apps/basics/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="%svelte.assets%/favicon.png" />
<meta name="transform-page" content="__REPLACEME__" />
%svelte.head%
</head>
<body>
Expand Down
7 changes: 6 additions & 1 deletion packages/kit/test/apps/basics/src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ export const handle = sequence(
throw new Error('Error in handle');
}

const response = await resolve(event, { ssr: !event.url.pathname.startsWith('/no-ssr') });
const response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/no-ssr'),
transformPage: event.url.pathname.startsWith('/transform-page')
? ({ html }) => html.replace('__REPLACEME__', 'Worked!')
: undefined
});
response.headers.append('set-cookie', 'name=SvelteKit; path=/; HttpOnly');

return response;
Expand Down
Empty file.
5 changes: 5 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,11 @@ test.describe.parallel('Page options', () => {
}
});

test('transformPage can change the html output', async ({ page }) => {
await page.goto('/transform-page');
expect(await page.getAttribute('meta[name="transform-page"]', 'content')).toBe('Worked!');
});

test('does not SSR page with ssr=false', async ({ page, javaScriptEnabled }) => {
await page.goto('/no-ssr');

Expand Down
3 changes: 1 addition & 2 deletions packages/kit/test/prerendering/basics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# prerendering-test-basics

## 0.0.2-next.0
### Patch Changes


### Patch Changes

- Use shadow endpoint without defining a `get` endpoint ([#3816](https://github.com/sveltejs/kit/pull/3816))
9 changes: 6 additions & 3 deletions packages/kit/types/hooks.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ export interface GetSession {
(event: RequestEvent): MaybePromise<App.Session>;
}

export interface ResolveOpts {
ssr?: boolean;
export interface RequiredResolveOptions {
ssr: boolean;
transformPage: ({ html }: { html: string }) => string;
}

export type ResolveOptions = Partial<RequiredResolveOptions>;

export interface Handle {
(input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise<Response>;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}): MaybePromise<Response>;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ export {
} from './config';
export { EndpointOutput, RequestHandler } from './endpoint';
export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page';
export { ExternalFetch, GetSession, Handle, HandleError, RequestEvent, ResolveOpts } from './hooks';
export {
ExternalFetch,
GetSession,
Handle,
HandleError,
RequestEvent,
ResolveOptions
} from './hooks';