From a4d5eca394b97cfee60d8470da5344474594b459 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 5 Jul 2023 15:44:41 +0200 Subject: [PATCH 01/22] feat: add dynamic image optimization part of #241 closes #9787 This adds image optimization through a new $app/images import. It's deliberately low level: The only export is getImage which you pass an image src and it returns an object containing src and srcset (possibly more?) values which you spread on an img tag. In order to use this you need to define a path to a loader in kit.config.images. The loader takes the original img src and a width and returns a URL pointing to the optimized image. You can also modify the number of sizes and trusted domains. --- packages/adapter-vercel/image-loader.d.ts | 4 ++ packages/adapter-vercel/image-loader.js | 49 +++++++++++++++++++++++ packages/adapter-vercel/index.d.ts | 11 ++++- packages/adapter-vercel/index.js | 14 +++++++ packages/adapter-vercel/package.json | 4 ++ packages/kit/scripts/generate-dts.js | 1 + packages/kit/src/core/config/options.js | 20 +++++++++ packages/kit/src/exports/public.d.ts | 25 ++++++++++++ packages/kit/src/exports/vite/index.js | 13 ++++++ packages/kit/src/runtime/app/images.js | 47 ++++++++++++++++++++++ packages/kit/src/types/ambient.d.ts | 7 ++++ 11 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 packages/adapter-vercel/image-loader.d.ts create mode 100644 packages/adapter-vercel/image-loader.js create mode 100644 packages/kit/src/runtime/app/images.js diff --git a/packages/adapter-vercel/image-loader.d.ts b/packages/adapter-vercel/image-loader.d.ts new file mode 100644 index 000000000000..e07362d1a057 --- /dev/null +++ b/packages/adapter-vercel/image-loader.d.ts @@ -0,0 +1,4 @@ +/** + * https://vercel.com/docs/concepts/image-optimization + */ +export default function loader(src: string, width: number, options?: { quality?: number }): string; diff --git a/packages/adapter-vercel/image-loader.js b/packages/adapter-vercel/image-loader.js new file mode 100644 index 000000000000..44b055a64818 --- /dev/null +++ b/packages/adapter-vercel/image-loader.js @@ -0,0 +1,49 @@ +// https://vercel.com/docs/concepts/image-optimization + +/** + * @param {string} src + * @param {number} width + * @param {{ quality?: number }} [options] + */ +export default function loader(src, width, options) { + const url = new URL(src, 'http://n'); // If the base is a relative URL, we need to add a dummy host to the URL + if (url.pathname === '/_vercel/image') { + set_param(url, 'w', width); + set_param(url, 'q', options?.quality ?? 75, false); + } else { + url.pathname = `/_vercel/image`; + set_param(url, 'url', src); + set_param(url, 'w', width); + set_param(url, 'q', options?.quality ?? 75); + } + return src === url.href ? url.href : relative_url(url); +} + +/** + * @param {URL} url + */ +function relative_url(url) { + const { pathname, search } = url; + return `${pathname}${search}`; +} +/** + * @param {URL} url + * @param {string} param + * @param {any} value + * @param {boolean} [override] + */ +function set_param(url, param, value, override = true) { + if (value === undefined) { + return; + } + + if (value === null) { + if (override || url.searchParams.has(param)) { + url.searchParams.delete(param); + } + } else { + if (override || !url.searchParams.has(param)) { + url.searchParams.set(param, value); + } + } +} diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts index 634f90ed8db9..a376c390fc9b 100644 --- a/packages/adapter-vercel/index.d.ts +++ b/packages/adapter-vercel/index.d.ts @@ -1,7 +1,16 @@ import { Adapter } from '@sveltejs/kit'; import './ambient.js'; -export default function plugin(config?: Config): Adapter; +export default function plugin( + config?: Config & { + /** + * Enable or disable Vercel's image optimization. This is enabled by default if you have + * defined the Vercel loader in your `svelte.config.js` file, else disabled by default. + * https://vercel.com/docs/concepts/image-optimization + */ + images?: boolean; + } +): Adapter; export interface ServerlessConfig { /** diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index a46a953dd32f..144c5ce37f82 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -407,8 +407,22 @@ function static_vercel_config(builder) { overrides[page.file] = { path: overrides_path }; } + /** @type {Record | undefined} */ + let images = undefined; + const img_config = builder.config.kit.images; + if (config.images || img_config.loader === '@sveltejs/adapter-vercel/image-loader') { + images = { + sizes: img_config.sizes, + domains: img_config.domains, + // TODO should we expose the following and some other optional options through the adapter? + formats: ['image/avif', 'image/webp'], + minimumCacheTTL: 300 + }; + } + return { version: 3, + images, routes: [ ...prerendered_redirects, { diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json index 382c60de4206..f413d738846e 100644 --- a/packages/adapter-vercel/package.json +++ b/packages/adapter-vercel/package.json @@ -15,6 +15,10 @@ "types": "./index.d.ts", "import": "./index.js" }, + "./image-loader": { + "types": "./image-loader.d.ts", + "import": "./image-loader.js" + }, "./package.json": "./package.json" }, "types": "index.d.ts", diff --git a/packages/kit/scripts/generate-dts.js b/packages/kit/scripts/generate-dts.js index 8eeffb5b2f82..cc6b2f5c4c01 100644 --- a/packages/kit/scripts/generate-dts.js +++ b/packages/kit/scripts/generate-dts.js @@ -10,6 +10,7 @@ createBundle({ '@sveltejs/kit/vite': 'src/exports/vite/index.js', '$app/environment': 'src/runtime/app/environment.js', '$app/forms': 'src/runtime/app/forms.js', + '$app/images': 'src/runtime/app/images.js', '$app/navigation': 'src/runtime/app/navigation.js', '$app/paths': 'src/runtime/app/paths.js', '$app/stores': 'src/runtime/app/stores.js' diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 7d7351023e61..912dc62b8486 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -138,6 +138,12 @@ const options = object( errorTemplate: string(join('src', 'error.html')) }), + images: object({ + domains: string_array([]), + loader: string(null), + sizes: number_array([640, 828, 1200, 1920, 3840]) + }), + inlineStyleThreshold: number(0), moduleExtensions: string_array(['.js', '.ts']), @@ -354,6 +360,20 @@ function string(fallback, allow_empty = true) { }); } +/** + * @param {number[] | undefined} [fallback] + * @returns {Validator} + */ +function number_array(fallback) { + return validate(fallback, (input, keypath) => { + if (!Array.isArray(input) || input.some((value) => typeof value !== 'number')) { + throw new Error(`${keypath} must be an array of numbers, if specified`); + } + + return input; + }); +} + /** * @param {string[] | undefined} [fallback] * @returns {Validator} diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 9e9fe3e8926f..64a83192cda9 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -430,6 +430,31 @@ export interface KitConfig { */ errorTemplate?: string; }; + /** + * Image optimization configuration + */ + images?: { + /** + * Path to a a file that contains a loader that will be used to generate the an image URL out of the given source and width. + * It optionally also takes third parameter for options. + * + * ```js + * export default function loader(src, width, opts) { + * return `https://example.com/${src}?w=${width}&q=${opts.quality || 75}`; + * } + * ``` + */ + loader?: string; + /** + * Which srcset sizes to generate + * @default [640, 828, 1200, 1920, 3840] + */ + sizes?: number[]; + /** + * Which external domains to trust when optimizing images + */ + domains?: string[]; + }; /** * Inline CSS inside a `