From bab711ee5d2e347c09ca3a0993b940bee4191676 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 24 Jan 2024 16:39:06 -0500 Subject: [PATCH] feat: dev/preview platform emulation (#11730) * introduce devPlatform kit configuration * Revert "introduce devPlatform kit configuration" This reverts commit 4a847e484f548c5ec6784633e5b05339b4c48670. * add emulatPlatform method to adapters * use latest wrangler release * make wrangler a peer dependency of the cloudflare adapters * add emulate function to adapter API * regenerate types * fix * move cloudflare adapter changes into separate PR * lockfile * emulate platform during prerender * fix types * test * docs * regenerate types * reset pnpm-lock.yaml * prettier * goddammit * Create chatty-walls-warn.md --------- Co-authored-by: Dario Piotrowicz Co-authored-by: Rich Harris --- .changeset/chatty-walls-warn.md | 5 ++++ .../99-writing-adapters.md | 12 ++++++++- packages/kit/src/core/postbuild/prerender.js | 5 +++- packages/kit/src/exports/public.d.ts | 16 ++++++++++++ packages/kit/src/exports/vite/dev/index.js | 6 ++++- .../kit/src/exports/vite/preview/index.js | 6 ++++- packages/kit/src/runtime/server/respond.js | 12 ++++++--- packages/kit/src/types/internal.d.ts | 5 +++- packages/kit/src/utils/route_config.js | 1 + .../routes/adapter/dynamic/+page.server.js | 11 ++++++++ .../src/routes/adapter/dynamic/+page.svelte | 5 ++++ .../adapter/prerendered/+page.server.js | 11 ++++++++ .../routes/adapter/prerendered/+page.svelte | 5 ++++ .../kit/test/apps/basics/svelte.config.js | 15 +++++++++++ packages/kit/test/apps/basics/test/test.js | 26 +++++++++++++++++++ packages/kit/types/index.d.ts | 16 ++++++++++++ 16 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 .changeset/chatty-walls-warn.md create mode 100644 packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.svelte diff --git a/.changeset/chatty-walls-warn.md b/.changeset/chatty-walls-warn.md new file mode 100644 index 000000000000..bd2481409320 --- /dev/null +++ b/.changeset/chatty-walls-warn.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": minor +--- + +feat: dev/preview/prerender platform emulation diff --git a/documentation/docs/25-build-and-deploy/99-writing-adapters.md b/documentation/docs/25-build-and-deploy/99-writing-adapters.md index 183d4c727981..c4092af15fb2 100644 --- a/documentation/docs/25-build-and-deploy/99-writing-adapters.md +++ b/documentation/docs/25-build-and-deploy/99-writing-adapters.md @@ -4,7 +4,7 @@ title: Writing adapters If an adapter for your preferred environment doesn't yet exist, you can build your own. We recommend [looking at the source for an adapter](https://github.com/sveltejs/kit/tree/main/packages) to a platform similar to yours and copying it as a starting point. -Adapters packages must implement the following API, which creates an `Adapter`: +Adapter packages implement the following API, which creates an `Adapter`: ```js // @errors: 2322 @@ -21,6 +21,14 @@ export default function (options) { async adapt(builder) { // adapter implementation }, + async emulate() { + return { + async platform({ config, prerender }) { + // the returned object becomes `event.platform` during dev, build and + // preview. Its shape is that of `App.Platform` + } + } + }, supports: { read: ({ config, route }) => { // Return `true` if the route with the given `config` can use `read` @@ -34,6 +42,8 @@ export default function (options) { } ``` +Of these, `name` and `adapt` are required. `emulate` and `supports` are optional. + Within the `adapt` method, there are a number of things that an adapter should do: - Clear out the build directory diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index c29ff168f562..05c995ab9771 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -92,6 +92,8 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { /** @type {import('types').ValidatedKitConfig} */ const config = (await load_config()).kit; + const emulator = await config.adapter?.emulate?.(); + /** @type {import('types').Logger} */ const log = logger({ verbose }); @@ -211,7 +213,8 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { // stuff in `static` return readFileSync(join(config.files.assets, file)); - } + }, + emulator }); const encoded_id = response.headers.get('x-sveltekit-routeid'); diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 6b21eaae8e46..a9b808387123 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -45,6 +45,11 @@ export interface Adapter { */ read?: (details: { config: any; route: { id: string } }) => boolean; }; + /** + * Creates an `Emulator`, which allows the adapter to influence the environment + * during dev, build and prerendering + */ + emulate?(): MaybePromise; } export type LoadProperties | void> = input extends void @@ -260,6 +265,17 @@ export interface Cookies { ): string; } +/** + * A collection of functions that influence the environment during dev, build and prerendering + */ +export class Emulator { + /** + * A function that is called with the current route `config` and `prerender` option + * and returns an `App.Platform` object + */ + platform?(details: { config: any; prerender: PrerenderOption }): MaybePromise; +} + export interface KitConfig { /** * Your [adapter](https://kit.svelte.dev/docs/adapters) is run when executing `vite build`. It determines how the output is converted for different platforms. diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index cfe846dbe326..5981025da4ce 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -420,6 +420,9 @@ export async function dev(vite, vite_config, svelte_config) { const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, ''); + // TODO because of `RecursiveRequired`, TypeScript thinks this is guaranteed to exist, but it isn't + const emulator = await svelte_config.kit.adapter?.emulate?.(); + return () => { const serve_static_middleware = vite.middlewares.stack.find( (middleware) => @@ -529,7 +532,8 @@ export async function dev(vite, vite_config, svelte_config) { read: (file) => fs.readFileSync(path.join(svelte_config.kit.files.assets, file)), before_handle: (event, config, prerender) => { async_local_storage.enterWith({ event, config, prerender }); - } + }, + emulator }); if (rendered.status === 404) { diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index c34af3121e1d..3246fc6fe008 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -51,6 +51,9 @@ export async function preview(vite, vite_config, svelte_config) { read: (file) => createReadableStream(`${dir}/${file}`) }); + // TODO because of `RecursiveRequired`, TypeScript thinks this is guaranteed to exist, but it isn't + const emulator = await svelte_config.kit.adapter?.emulate?.(); + return () => { // Remove the base middleware. It screws with the URL. // It also only lets through requests beginning with the base path, so that requests beginning @@ -191,7 +194,8 @@ export async function preview(vite, vite_config, svelte_config) { if (remoteAddress) return remoteAddress; throw new Error('Could not determine clientAddress'); }, - read: (file) => fs.readFileSync(join(svelte_config.kit.files.assets, file)) + read: (file) => fs.readFileSync(join(svelte_config.kit.files.assets, file)), + emulator }) ); }); diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index bf59ba4cfd30..555248738c41 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -271,7 +271,7 @@ export async function respond(request, options, manifest, state) { } } - if (DEV && state.before_handle) { + if (state.before_handle || state.emulator?.platform) { let config = {}; /** @type {import('types').PrerenderOption} */ @@ -283,11 +283,17 @@ export async function respond(request, options, manifest, state) { prerender = node.prerender ?? prerender; } else if (route.page) { const nodes = await load_page_nodes(route.page, manifest); - config = get_page_config(nodes); + config = get_page_config(nodes) ?? config; prerender = get_option(nodes, 'prerender') ?? false; } - state.before_handle(event, config, prerender); + if (state.before_handle) { + state.before_handle(event, config, prerender); + } + + if (state.emulator?.platform) { + event.platform = await state.emulator.platform({ config, prerender }); + } } } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 22e11e906ea8..864de0be71a3 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -15,7 +15,8 @@ import { HandleClientError, Reroute, RequestEvent, - SSRManifest + SSRManifest, + Emulator } from '@sveltejs/kit'; import { HttpMethod, @@ -128,6 +129,7 @@ export class InternalServer extends Server { read: (file: string) => Buffer; /** A hook called before `handle` during dev, so that `AsyncLocalStorage` can be populated */ before_handle?: (event: RequestEvent, config: any, prerender: PrerenderOption) => void; + emulator?: Emulator; } ): Promise; } @@ -418,6 +420,7 @@ export interface SSRState { prerender_default?: PrerenderOption; read?: (file: string) => Buffer; before_handle?: (event: RequestEvent, config: any, prerender: PrerenderOption) => void; + emulator?: Emulator; } export type StrictBody = string | ArrayBufferView; diff --git a/packages/kit/src/utils/route_config.js b/packages/kit/src/utils/route_config.js index 30563018c1d5..27b67f71f765 100644 --- a/packages/kit/src/utils/route_config.js +++ b/packages/kit/src/utils/route_config.js @@ -16,5 +16,6 @@ export function get_page_config(nodes) { }; } + // TODO 3.0 always return `current`? then we can get rid of `?? {}` in other places return Object.keys(current).length ? current : undefined; } diff --git a/packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.server.js b/packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.server.js new file mode 100644 index 000000000000..96d1373d993d --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.server.js @@ -0,0 +1,11 @@ +export const prerender = false; + +export const config = { + message: 'hello from dynamic page' +}; + +export function load({ platform }) { + return { + platform + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.svelte b/packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.svelte new file mode 100644 index 000000000000..23ba131ac214 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/adapter/dynamic/+page.svelte @@ -0,0 +1,5 @@ + + +
{JSON.stringify(data.platform)}
diff --git a/packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.server.js b/packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.server.js new file mode 100644 index 000000000000..90cc61d03496 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.server.js @@ -0,0 +1,11 @@ +export const prerender = true; + +export const config = { + message: 'hello from prerendered page' +}; + +export function load({ platform }) { + return { + platform + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.svelte b/packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.svelte new file mode 100644 index 000000000000..23ba131ac214 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/adapter/prerendered/+page.svelte @@ -0,0 +1,5 @@ + + +
{JSON.stringify(data.platform)}
diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js index b74fb4612ff7..f7986e3e3ab9 100644 --- a/packages/kit/test/apps/basics/svelte.config.js +++ b/packages/kit/test/apps/basics/svelte.config.js @@ -1,6 +1,21 @@ /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { + adapter: { + name: 'test-adapter', + adapt() {}, + emulate() { + return { + platform({ config, prerender }) { + return { config, prerender }; + } + }; + }, + supports: { + read: () => true + } + }, + prerender: { entries: [ '*', diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index b408d1b56c3e..9c6d8a5c14f8 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -7,6 +7,32 @@ test.skip(() => process.env.KIT_E2E_BROWSER === 'webkit'); test.describe.configure({ mode: 'parallel' }); +test.describe('adapter', () => { + test('populates event.platform for dynamic SSR', async ({ page }) => { + await page.goto('/adapter/dynamic'); + const json = JSON.parse(await page.textContent('pre')); + + expect(json).toEqual({ + config: { + message: 'hello from dynamic page' + }, + prerender: false + }); + }); + + test('populates event.platform for prerendered page', async ({ page }) => { + await page.goto('/adapter/prerendered'); + const json = JSON.parse(await page.textContent('pre')); + + expect(json).toEqual({ + config: { + message: 'hello from prerendered page' + }, + prerender: true + }); + }); +}); + test.describe('Imports', () => { test('imports from node_modules', async ({ page, clicknav }) => { await page.goto('/imports'); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 231e2ea8ee23..5c43bbac143f 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -27,6 +27,11 @@ declare module '@sveltejs/kit' { */ read?: (details: { config: any; route: { id: string } }) => boolean; }; + /** + * Creates an `Emulator`, which allows the adapter to influence the environment + * during dev, build and prerendering + */ + emulate?(): MaybePromise; } export type LoadProperties | void> = input extends void @@ -242,6 +247,17 @@ declare module '@sveltejs/kit' { ): string; } + /** + * A collection of functions that influence the environment during dev, build and prerendering + */ + export class Emulator { + /** + * A function that is called with the current route `config` and `prerender` option + * and returns an `App.Platform` object + */ + platform?(details: { config: any; prerender: PrerenderOption }): MaybePromise; + } + export interface KitConfig { /** * Your [adapter](https://kit.svelte.dev/docs/adapters) is run when executing `vite build`. It determines how the output is converted for different platforms.