Skip to content

Commit

Permalink
feat: dev/preview platform emulation (#11730)
Browse files Browse the repository at this point in the history
* introduce devPlatform kit configuration

* Revert "introduce devPlatform kit configuration"

This reverts commit 4a847e4.

* 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 <[email protected]>
Co-authored-by: Rich Harris <[email protected]>
  • Loading branch information
3 people authored Jan 24, 2024
1 parent 6161351 commit bab711e
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-walls-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sveltejs/kit": minor
---

feat: dev/preview/prerender platform emulation
12 changes: 11 additions & 1 deletion documentation/docs/25-build-and-deploy/99-writing-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/core/postbuild/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -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');
Expand Down
16 changes: 16 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Emulator>;
}

export type LoadProperties<input extends Record<string, any> | void> = input extends void
Expand Down Expand Up @@ -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<App.Platform>;
}

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.
Expand Down
6 changes: 5 additions & 1 deletion packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion packages/kit/src/exports/vite/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
})
);
});
Expand Down
12 changes: 9 additions & 3 deletions packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} */
Expand All @@ -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 });
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
HandleClientError,
Reroute,
RequestEvent,
SSRManifest
SSRManifest,
Emulator
} from '@sveltejs/kit';
import {
HttpMethod,
Expand Down Expand Up @@ -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<Response>;
}
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/utils/route_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const prerender = false;

export const config = {
message: 'hello from dynamic page'
};

export function load({ platform }) {
return {
platform
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
export let data;
</script>

<pre>{JSON.stringify(data.platform)}</pre>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const prerender = true;

export const config = {
message: 'hello from prerendered page'
};

export function load({ platform }) {
return {
platform
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
export let data;
</script>

<pre>{JSON.stringify(data.platform)}</pre>
15 changes: 15 additions & 0 deletions packages/kit/test/apps/basics/svelte.config.js
Original file line number Diff line number Diff line change
@@ -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: [
'*',
Expand Down
26 changes: 26 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
16 changes: 16 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Emulator>;
}

export type LoadProperties<input extends Record<string, any> | void> = input extends void
Expand Down Expand Up @@ -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<App.Platform>;
}

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.
Expand Down

0 comments on commit bab711e

Please sign in to comment.