Skip to content

Commit

Permalink
Vite plugin customization (#16)
Browse files Browse the repository at this point in the history
* Add vite plugin to customize constants

* custom promise field

* Update readme and fix vite plugin

* bump version
  • Loading branch information
paoloricciuti authored Feb 7, 2023
1 parent d8553cd commit eac1a84
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 30 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,32 @@ then on the client side you can access the data via the store provided by `svelt
</main>
```

## Configuration

`sveltekit-defer` makes use of apis that require to choose a name for them (e.g we need to create a couple of endpoints, an event name, a field to store the deferred promises etc etc). We tryed to chose unique enaugh names so that they should never collide with your applications but you know what they say and the internet is a vast enaugh place to encounter the weirdest circumstances. To avoid this `sveltekit-defer` provide a custom vite plugin to override those names.

```ts
import { sveltekit } from '@sveltejs/kit/vite';
import { sveltekit_defer } from 'sveltekit-defer/vite';
import type { UserConfig } from 'vite';

const config: UserConfig = {
plugins: [
sveltekit_defer({
cookie_name: 'your_cookie_name',
stream_event: 'your_stream_event',
stream_pathname: '/your_pathname', //this should start with a / but don't worry, if you don't we take care of it
promise_field: 'your_promise_field'
}),
sveltekit()
]
};

export default config;
```

Make sure to put this plugin before the `sveltekit` one.

## How it works?

Here's how it works:
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sveltekit-defer",
"version": "0.0.3",
"version": "0.0.4",
"repository": "git+https://github.com/paoloricciuti/sveltekit-defer.git",
"author": "Paolo Ricciuti",
"license": "MIT",
Expand Down Expand Up @@ -36,11 +36,13 @@
},
"peerDependencies": {
"@sveltejs/kit": "^1.0.0",
"svelte": "^3.54.0",
"devalue": "^4.2.3"
"devalue": "^4.2.3",
"magic-string": "^0.27.0",
"svelte": "^3.54.0"
},
"type": "module",
"dependencies": {
"devalue": "^4.2.3"
"devalue": "^4.2.3",
"magic-string": "^0.27.0"
}
}
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 48 additions & 3 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
export const COOKIE_NAME = 'sveltekit-defer-user';
export const STREAM_PATHNAME = '/__sveltekit-defer-events';
export const STREAM_EVENT = 'sveltekit-defer-resolved';
/// @sveltekit-defer-constants
export const env = {
cookie_name: 'sveltekit-defer-user',
stream_pathname: '/__sveltekit-defer-events',
stream_event: 'sveltekit-defer-resolved',
promise_field: '__sveltekit-defer-promises'
} as const;

export type Env = typeof env;

export type SveltekitDeferOptions = {
-readonly [K in keyof Env]?: LiteralToPrimitive<Env[K]>;
};

export type PromisesField = Env['promise_field'];

/*
MIT License
Copyright (c) Sindre Sorhus <[email protected]> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
type LiteralToPrimitive<T> = T extends number
? number
: T extends bigint
? bigint
: T extends string
? string
: T extends boolean
? boolean
: T extends symbol
? symbol
: T extends null
? null
: T extends undefined
? undefined
: T extends any[]
? LiteralToPrimitive<T[number]>[]
: T extends object
? {
[K in keyof T]: LiteralToPrimitive<T[K]>;
}
: never;
16 changes: 8 additions & 8 deletions src/lib/defer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ServerLoadEvent } from '@sveltejs/kit';
import { COOKIE_NAME, STREAM_PATHNAME } from './constants';
import { env, type PromisesField } from './constants';
import * as devalue from 'devalue';

function pushEvent(event: ServerLoadEvent, data: any) {
event.fetch(`${STREAM_PATHNAME}?load=${event.url}`, {
event.fetch(`${env.stream_pathname}?load=${event.url}`, {
method: 'POST',
body: JSON.stringify(data)
});
Expand All @@ -16,7 +16,7 @@ type GetPromises<T> = {
type Transform<T> = {
[Key in keyof T]: T[Key];
} & {
promises: GetPromises<T>[];
[K in PromisesField]: GetPromises<T>[];
};

function get_promise_or_throw(promise: Promise<any>) {
Expand All @@ -37,8 +37,8 @@ export function defer<T extends (...args: any[]) => any>(
func: T
): (event: ServerLoadEvent) => Promise<Transform<Awaited<ReturnType<T>>>> {
return async (event: ServerLoadEvent) => {
if (!event.cookies.get(COOKIE_NAME)) {
event.cookies.set(COOKIE_NAME, crypto.randomUUID(), {
if (!event.cookies.get(env.cookie_name)) {
event.cookies.set(env.cookie_name, crypto.randomUUID(), {
path: '/',
httpOnly: true,
secure: true
Expand All @@ -53,10 +53,10 @@ export function defer<T extends (...args: any[]) => any>(
const actualValue = await get_promise_or_throw(returnVal[key]);
returnVal[key] = actualValue;
} catch (e) {
if (!returnVal.promises) {
returnVal.promises = [];
if (!returnVal[env.promise_field]) {
returnVal[env.promise_field] = [];
}
returnVal.promises.push(key);
returnVal[env.promise_field].push(key);
returnVal[key]
.then((res: any) => {
pushEvent(event, { value: stringify(res), key, kind: 'resolve' });
Expand Down
8 changes: 4 additions & 4 deletions src/lib/handle.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Handle } from '@sveltejs/kit';
import { COOKIE_NAME, STREAM_EVENT, STREAM_PATHNAME } from './constants';
import { env } from './constants';

const controllers = new Map<string, ReadableStreamController<any>>();
const queued = new Map<string, ((controller: ReadableStreamController<any>) => void)[]>();

async function enqueue(request: Request, key: string) {
const controller = controllers.get(key);
const data = await request.clone().text();
const to_write = `event: ${STREAM_EVENT}\ndata: ${data}\n\n`;
const to_write = `event: ${env.stream_event}\ndata: ${data}\n\n`;
const flush = (controller: ReadableStreamController<any>) => {
const encoder = new TextEncoder();
controller.enqueue(encoder.encode(to_write));
Expand All @@ -31,8 +31,8 @@ async function enqueue(request: Request, key: string) {

export const defer_handle: Handle = async ({ event, resolve }) => {
const { searchParams, pathname } = new URL(event.request.url);
if (pathname === STREAM_PATHNAME) {
const defer_user = event.cookies.get(COOKIE_NAME);
if (pathname === env.stream_pathname) {
const defer_user = event.cookies.get(env.cookie_name);
const load = searchParams.get('load') ?? '';
const key = [defer_user, load].toString();
if (!defer_user) {
Expand Down
18 changes: 9 additions & 9 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import { derived, get } from 'svelte/store';
import { STREAM_EVENT, STREAM_PATHNAME } from './constants';
import { env, type PromisesField } from './constants';
import * as devalue from 'devalue';

function parse(value: any) {
Expand All @@ -14,23 +14,23 @@ function parse(value: any) {
return ret;
}

type TransformBack<T extends { promises: string[] }> = Omit<
type TransformBack<T extends { [K in PromisesField]: string[] }> = Omit<
{
[Key in keyof T]: [Key] extends T['promises'] ? Promise<T[Key]> : T[Key];
[Key in keyof T]: [Key] extends T[PromisesField] ? Promise<T[Key]> : T[Key];
},
'promises'
PromisesField
>;

export function get_data<T extends { promises: string[] }>() {
export function get_data<T extends { [K in PromisesField]: string[] }>() {
const resolvers: Map<string, { resolve: (arg: any) => void; reject: (arg: any) => void }> =
new Map();
let eventSource: EventSource;
onMount(() => {
const $page = get(page);
eventSource = new EventSource(`${STREAM_PATHNAME}?load=${$page.url.href}`, {
eventSource = new EventSource(`${env.stream_pathname}?load=${$page.url.href}`, {
withCredentials: true
});
eventSource?.addEventListener(STREAM_EVENT, (evt) => {
eventSource?.addEventListener(env.stream_event, (evt) => {
const resolved = JSON.parse(evt.data);
const resolver = resolvers.get(resolved.key);
if (resolved.kind === 'resolve') {
Expand All @@ -45,7 +45,7 @@ export function get_data<T extends { promises: string[] }>() {
});
const retval = derived<typeof page, TransformBack<T>>(page, ($page) => {
const data = $page.data;
const { promises = [] } = data;
const { [env.promise_field]: promises = [] } = data;
resolvers.clear();
Object.keys(data).forEach((_key) => {
const key = _key as keyof typeof data;
Expand All @@ -55,7 +55,7 @@ export function get_data<T extends { promises: string[] }>() {
});
}
});
delete data.promises;
delete data[env.promise_field];
return data as TransformBack<T>;
});

Expand Down
41 changes: 41 additions & 0 deletions src/lib/vite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { SveltekitDeferOptions } from '$lib/constants';
import type { Plugin } from 'vite';
import MagicString from 'magic-string';

export const env_parser: {
[K in keyof SveltekitDeferOptions]?: (
to_parse: SveltekitDeferOptions[K]
) => SveltekitDeferOptions[K];
} = {
stream_pathname(to_parse) {
if (!to_parse || to_parse.startsWith('/')) return to_parse;
return `/${to_parse}`;
}
};

export function sveltekit_defer(options: SveltekitDeferOptions): Plugin {
return {
name: 'vite-plugin-sveltekit-defer',
transform(code, id) {
if (id.endsWith('node_modules/sveltekit-defer/constants.js')) {
if (code.startsWith('/// @sveltekit-defer-constants')) {
const magicCode = new MagicString(code);
Object.keys(options).forEach((_key) => {
const key = _key as keyof typeof options;
const to_replace = env_parser[key]?.(options[key]) ?? options[key];
if (to_replace) {
magicCode.replace(
new RegExp(`${key}:\\s\\'.*\\',?\\r?\\n`),
`${key}: '${to_replace}',\r\n`
);
}
});
return {
code: magicCode.toString(),
map: magicCode.generateMap()
};
}
}
}
};
}

0 comments on commit eac1a84

Please sign in to comment.