From 435440233f042ec00654bc1b0b17a9696e789441 Mon Sep 17 00:00:00 2001 From: Julian Sigmund <58262143+pixelmund@users.noreply.github.com> Date: Tue, 15 Feb 2022 17:12:26 +0100 Subject: [PATCH] [feat] transformPage (#3914) * 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 --- .changeset/forty-cycles-punch.md | 5 ++++ documentation/docs/04-hooks.md | 7 +++-- packages/kit/src/runtime/server/index.js | 27 ++++++++++++++----- packages/kit/src/runtime/server/page/index.js | 8 +++--- .../kit/src/runtime/server/page/render.js | 14 +++++----- .../kit/src/runtime/server/page/respond.js | 12 ++++----- .../runtime/server/page/respond_with_error.js | 14 +++++++--- packages/kit/test/apps/basics/src/app.html | 1 + packages/kit/test/apps/basics/src/hooks.js | 7 ++++- .../src/routes/transform-page/index.svelte | 0 packages/kit/test/apps/basics/test/test.js | 5 ++++ .../kit/test/prerendering/basics/CHANGELOG.md | 3 +-- packages/kit/types/hooks.d.ts | 9 ++++--- packages/kit/types/index.d.ts | 9 ++++++- 14 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 .changeset/forty-cycles-punch.md create mode 100644 packages/kit/test/apps/basics/src/routes/transform-page/index.svelte diff --git a/.changeset/forty-cycles-punch.md b/.changeset/forty-cycles-punch.md new file mode 100644 index 000000000000..909c480ac8c1 --- /dev/null +++ b/.changeset/forty-cycles-punch.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Add `transformPage` option to `resolve` diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md index af30e83ac071..9739730d2125 100644 --- a/documentation/docs/04-hooks.md +++ b/documentation/docs/04-hooks.md @@ -28,6 +28,7 @@ export interface RequestEvent { export interface ResolveOpts { ssr?: boolean; + transformPage?: ({ html }: { html: string }) => string; } export interface Handle { @@ -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; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 853e86cce680..177a3e347fc5 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -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); @@ -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({ @@ -110,7 +122,10 @@ export async function respond(request, options, state = {}) { stuff: {}, status: 200, branch: [], - ssr: false + resolve_opts: { + ...resolve_opts, + ssr: false + } }); } @@ -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) { @@ -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 }); } @@ -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); diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 50cb22d4e955..3e1915672105 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -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} */ -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}`, { @@ -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) { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 361510bafeb7..38fdecd11ba5 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -25,7 +25,7 @@ const updated = { * error?: Error; * url: URL; * params: Record; - * ssr: boolean; + * resolve_opts: import('types/hooks').RequiredResolveOptions; * stuff: Record; * }} opts */ @@ -39,7 +39,7 @@ export async function render_response({ error, url, params, - ssr, + resolve_opts, stuff }) { if (state.prerender) { @@ -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)); @@ -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: [ @@ -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', diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index bb3adefeac0b..81819cd2de8f 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -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; - * ssr: boolean; * }} opts * @returns {Promise} */ export async function respond(opts) { - const { event, options, state, $session, route, ssr } = opts; + const { event, options, state, $session, route, resolve_opts } = opts; /** @type {Array} */ let nodes; - if (!ssr) { + if (!resolve_opts.ssr) { return await render_response({ ...opts, branch: [], @@ -58,7 +58,7 @@ export async function respond(opts) { $session, status: 500, error, - ssr + resolve_opts }); } @@ -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]; @@ -194,7 +194,7 @@ export async function respond(opts) { $session, status, error, - ssr + resolve_opts }), set_cookie_headers ); diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index f77f369a793e..a93f6b4196d7 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -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 @@ -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); diff --git a/packages/kit/test/apps/basics/src/app.html b/packages/kit/test/apps/basics/src/app.html index b1dea1509bbe..2a867511e6e2 100644 --- a/packages/kit/test/apps/basics/src/app.html +++ b/packages/kit/test/apps/basics/src/app.html @@ -4,6 +4,7 @@ + %svelte.head% diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js index dfb9959d0e66..948a9038034f 100644 --- a/packages/kit/test/apps/basics/src/hooks.js +++ b/packages/kit/test/apps/basics/src/hooks.js @@ -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; diff --git a/packages/kit/test/apps/basics/src/routes/transform-page/index.svelte b/packages/kit/test/apps/basics/src/routes/transform-page/index.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 0b6ce8465d9c..69c98bd0f2b7 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -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'); diff --git a/packages/kit/test/prerendering/basics/CHANGELOG.md b/packages/kit/test/prerendering/basics/CHANGELOG.md index 27868dd6aa08..8c50a6dcfff8 100644 --- a/packages/kit/test/prerendering/basics/CHANGELOG.md +++ b/packages/kit/test/prerendering/basics/CHANGELOG.md @@ -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)) diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index fdcf697bdfa2..4f52773d724b 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -14,14 +14,17 @@ export interface GetSession { (event: RequestEvent): MaybePromise; } -export interface ResolveOpts { - ssr?: boolean; +export interface RequiredResolveOptions { + ssr: boolean; + transformPage: ({ html }: { html: string }) => string; } +export type ResolveOptions = Partial; + export interface Handle { (input: { event: RequestEvent; - resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise; }): MaybePromise; } diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b4f3d0578467..8c6121ab7b63 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -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';