Skip to content

Commit

Permalink
[feat] transformPage (#3914)
Browse files Browse the repository at this point in the history
* Start implementing transformPage

* format

* better format

* changeset + test

* Weird formatting fix

* Try again

* tweak changeset

* tweak implementation - transformPage shouldnt go on options

* add docs

* handle SPA fallback case

* lint

* simplify test

* remove transformPage

Co-authored-by: Rich Harris <hello@rich-harris.dev>
  • Loading branch information
pixelmund and Rich-Harris committed Feb 15, 2022
1 parent 5badfe9 commit 4354402
Show file tree
Hide file tree
Showing 14 changed files with 87 additions and 34 deletions.
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';

0 comments on commit 4354402

Please sign in to comment.