Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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