From 2010480783e03be6c8c3409a4aa1325c65210851 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 25 Sep 2023 09:08:23 -0700 Subject: [PATCH 001/108] feat: static image preprocessor --- packages/image/README.md | 86 ++++++++ packages/image/package.json | 67 +++++++ packages/image/src/preprocessor.js | 189 ++++++++++++++++++ packages/image/src/vite-plugin.js | 64 ++++++ packages/image/test/Input.svelte | 45 +++++ packages/image/test/Output.svelte | 85 ++++++++ packages/image/test/index.spec.js | 21 ++ packages/image/tsconfig.json | 19 ++ packages/image/types/vite.d.ts | 3 + pnpm-lock.yaml | 86 +++++--- sites/kit.svelte.dev/package.json | 1 + sites/kit.svelte.dev/src/lib/Image.svelte | 18 -- .../src/routes/home/Hero.svelte | 5 +- .../src/routes/home/Showcase.svelte | 4 +- sites/kit.svelte.dev/vite.config.js | 22 +- 15 files changed, 647 insertions(+), 68 deletions(-) create mode 100644 packages/image/README.md create mode 100644 packages/image/package.json create mode 100644 packages/image/src/preprocessor.js create mode 100644 packages/image/src/vite-plugin.js create mode 100644 packages/image/test/Input.svelte create mode 100644 packages/image/test/Output.svelte create mode 100644 packages/image/test/index.spec.js create mode 100644 packages/image/tsconfig.json create mode 100644 packages/image/types/vite.d.ts delete mode 100644 sites/kit.svelte.dev/src/lib/Image.svelte diff --git a/packages/image/README.md b/packages/image/README.md new file mode 100644 index 000000000000..ee94ed758bd4 --- /dev/null +++ b/packages/image/README.md @@ -0,0 +1,86 @@ +# `@sveltejs/image` + +**WARNING**: This package is experimental. It uses pre-1.0 versioning and may introduce breaking changes with every minor version release. + +This package aims to bring a plug and play image component to SvelteKit that is opinionated enough so you don't have to worry about the details, yet flexible enough for more advanced use cases or tweaks. It serves a smaller file format like `avif` or `webp`. + +## Setup + +Install: + +```bash +npm install --save @sveltejs/image +``` + +Adjust `vite.config.js`: + +```diff ++import { images } from '@sveltejs/image/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ ++ images(), + sveltekit() + ] +}); +``` + +## Usage + +Static build time optimization uses `vite-imagetools`, which comes as an optional peer dependency, so you first need to install it: + +```bash +npm install --save-dev vite-imagetools +``` + +Use in your `.svelte` components by referencing a relative path beginning with `./` or `$` (for Vite aliases): + +```svelte +An alt text +``` + +This optimizes the image at build time using `vite-imagetools`. `width` and `height` are optional as they can be inferred from the source image. + +You can also manually import an image and then pass it to a transformed `img` tag. + +```svelte + + + +An alt text +``` + +This is useful when you have a collection of static images and would like to dynamically choose one. A collection of images can be imported with [Vite's `import.meta.glob`](https://vitejs.dev/guide/features.html#glob-import). + +> If you have existing image imports like `import SomeImage from './some/image.jpg';` they will be treated differently now. If you want to get back the previous behavior of this import returning a URL string, add `?url` to the end of the import. + +Note that the generated code is a `picture` tag wrapping one `source` tag per image type. + +If you have an image tag you do not want to be transformed you can use the comment ``. + +### Static vs dynamic image references + +This package only handles images that are located in your project and can be referred to with a static string. It generates images at build time, so building may take longer the more images you transform. + +Alternatively, using an image CDN provides more flexibility with regards to sizes and you can pass image sources not known at build time, but it comes with potentially a bit of setup overhead (configuring the image CDN) and possibly usage cost. CDNs reduce latency by distributing copies of static assets globally. Building HTML to target CDNs may result in slightly smaller HTML because they can serve the appropriate file format for an `img` tag based on the `User-Agent` header whereas build-time optimizations must produce `picture` tags. Finally some CDNs may generate images lazily, which could have a negative performance impact for sites with low traffic and frequently changing images. + +You can mix and match both solutions in one project, but can only use this library for static image references. + +## Best practices + +- Always provide a good `alt` text +- Your original images should have a good quality/resolution. Images will only be sized down +- Choose one image per page which is the most important/largest one and give it `priority` so it loads faster. This gives you better web vitals scores (largest contentful paint in particular) +- Give the image a container or a styling so that it is constrained and does not jump around. `width` and `height` help the browser reserving space while the image is still loading + +## Roadmap + +This is an experimental MVP for getting initial feedback on the implementation/usability of an image component usable with SvelteKit (can also be used with Vite only). Once the API is stable, we may enable in SvelteKit or the templates by default. + +## Acknowledgements + +We'd like to thank the authors of the Next/Nuxt/Astro/`unpic` image components and `svelte-preprocess-import-assets` for inspiring this work. We'd also like to thank the authors of `vite-imagetools` which is used in `@sveltejs/image`. diff --git a/packages/image/package.json b/packages/image/package.json new file mode 100644 index 000000000000..0d0132da3f53 --- /dev/null +++ b/packages/image/package.json @@ -0,0 +1,67 @@ +{ + "name": "@sveltejs/image", + "version": "0.1.0", + "description": "Image optimization for your Svelte apps", + "repository": { + "type": "git", + "url": "https://github.com/sveltejs/kit", + "directory": "packages/image" + }, + "license": "MIT", + "homepage": "https://kit.svelte.dev", + "type": "module", + "scripts": { + "lint": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore", + "check": "tsc", + "format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore", + "test": "vitest" + }, + "files": [ + "src", + "types" + ], + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./types/index.d.ts", + "import": "./src/index.js" + }, + "./vite": { + "import": "./src/vite-plugin.js" + } + }, + "types": "types/index.d.ts", + "typesVersions": { + "*": { + "index": [ + "types/index.d.ts" + ], + "vite": [ + "types/vite.d.ts" + ] + } + }, + "dependencies": { + "esm-env": "^1.0.0", + "magic-string": "^0.30.0", + "svelte-parse-markup": "^0.1.1" + }, + "devDependencies": { + "@types/estree": "^1.0.2", + "@types/node": "^16.18.6", + "svelte": "^4.0.5", + "typescript": "^4.9.4", + "vite": "^4.4.2", + "vite-imagetools": "^5.0.8", + "vitest": "^0.34.0" + }, + "peerDependencies": { + "svelte": "^4.0.0", + "vite-imagetools": "^5.0.8" + }, + "peerDependenciesMeta": { + "vite-imagetools": { + "optional": true + } + } +} diff --git a/packages/image/src/preprocessor.js b/packages/image/src/preprocessor.js new file mode 100644 index 000000000000..d91e998edd87 --- /dev/null +++ b/packages/image/src/preprocessor.js @@ -0,0 +1,189 @@ +import MagicString from 'magic-string'; +import { parse } from 'svelte-parse-markup'; +import { walk } from 'svelte/compiler'; + +const IGNORE_FLAG = 'svelte-image-disable'; +const FORCE_FLAG = 'svelte-image-enable'; +const ASSET_PREFIX = '___ASSET___'; + +// TODO: expose this in vite-imagetools rather than duplicating it +const OPTIMIZABLE = /^[^?]+\.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif)(\?.*)?$/; + +/** + * @returns {import('svelte/types/compiler/preprocess').PreprocessorGroup} + */ +export function image() { + return { + markup({ content, filename }) { + const s = new MagicString(content); + const ast = parse(content, { filename }); + + // Import path to import name + // e.g. ./foo.png => ___ASSET___0 + /** @type {Map} */ + const imports = new Map(); + + /** + * @param {import('svelte/types/compiler/interfaces').TemplateNode} node + * @param {{ type: string, start: number, end: number, raw: string }} attribute_value + */ + function update_element(node, attribute_value) { + if (attribute_value.type === 'MustacheTag') { + const src_var_name = content + .substring(attribute_value.start + 1, attribute_value.end - 1) + .trim(); + s.update(node.start, node.end, dynamic_img_to_picture(content, node, src_var_name)); + return; + } + + const url = attribute_value.raw.trim(); + + // if it's not a relative reference or Vite alias then skip it + // TODO: read vite aliases here rather than assuming $ + if (!url.startsWith('./') && !url.startsWith('$')) return; + + let import_name = ''; + + if (imports.has(url)) { + import_name = /** @type {string} */ (imports.get(url)); + } else { + import_name = ASSET_PREFIX + imports.size; + imports.set(url, import_name); + } + + if (OPTIMIZABLE.test(url)) { + s.update(node.start, node.end, img_to_picture(content, node, import_name)); + } else { + // e.g. => + s.update(attribute_value.start, attribute_value.end, `{${import_name}}`); + } + } + + let ignore_next_element = false; + let force_next_element = false; + + // @ts-ignore + walk(ast.html, { + /** + * @param {import('svelte/types/compiler/interfaces').TemplateNode} node + */ + enter(node) { + if (node.type === 'Comment') { + if (node.data.trim() === IGNORE_FLAG) { + ignore_next_element = true; + } else if (node.data.trim() === FORCE_FLAG) { + force_next_element = true; + } + } else if (node.type === 'Element') { + if (ignore_next_element) { + ignore_next_element = false; + return; + } + + // Compare node tag match + if (node.name === 'img') { + /** + * @param {string} attr + */ + function get_attr_value(attr) { + const attribute = node.attributes.find( + /** @param {any} v */ (v) => v.type === 'Attribute' && v.name === attr + ); + if (!attribute) return; + + // Ensure value only consists of one element, and is of type "Text". + // Which should only match instances of static `foo="bar"` attributes. + if ( + !force_next_element && + (attribute.value.length !== 1 || attribute.value[0].type !== 'Text') + ) { + return; + } + + return attribute.value[0]; + } + + const src = get_attr_value('src'); + if (!src) return; + update_element(node, src); + } + } + } + }); + + // add imports + if (imports.size) { + let import_text = ''; + for (const [path, import_name] of imports.entries()) { + import_text += `import ${import_name} from "${path}";`; + } + if (ast.instance) { + // @ts-ignore + s.appendLeft(ast.instance.content.start, import_text); + } else { + s.append(``); + } + } + + return { + code: s.toString(), + map: s.generateMap() + }; + } + }; +} + +/** + * @param {string} content + * @param {Array} attributes + * @param {string} src_var_name + */ +function attributes_to_markdown(content, attributes, src_var_name) { + const attribute_strings = attributes.map((attribute) => { + if (attribute.name === 'src') { + return `src={${src_var_name}.img.src}`; + } + return content.substring(attribute.start, attribute.end); + }); + + let has_width = false; + let has_height = false; + for (const attribute of attributes) { + if (attribute.name === 'width') has_width = true; + if (attribute.name === 'height') has_height = true; + } + if (!has_width && !has_height) { + attribute_strings.push(`width={${src_var_name}.img.w}`); + attribute_strings.push(`height={${src_var_name}.img.h}`); + } + + return attribute_strings.join(' '); +} + +/** + * @param {string} content + * @param {import('svelte/types/compiler/interfaces').TemplateNode} node + * @param {string} import_name + */ +function img_to_picture(content, node, import_name) { + return ` + {#each Object.entries(${import_name}.sources) as [format, images]} + \`\${i.src} \${i.w}w\`).join(', ')} type={'image/' + format} /> + {/each} + +`; +} + +/** + * For images like `` + * @param {string} content + * @param {import('svelte/types/compiler/interfaces').TemplateNode} node + * @param {string} src_var_name + */ +function dynamic_img_to_picture(content, node, src_var_name) { + return `{#if typeof ${src_var_name} === 'string'} + +{:else} + ${img_to_picture(content, node, src_var_name)} +{/if}`; +} diff --git a/packages/image/src/vite-plugin.js b/packages/image/src/vite-plugin.js new file mode 100644 index 000000000000..4a1500648459 --- /dev/null +++ b/packages/image/src/vite-plugin.js @@ -0,0 +1,64 @@ +import path from 'node:path'; +import { image } from './preprocessor.js'; + +/** + * @returns {Promise} + */ +export async function images() { + const imagetools_plugin = await imagetools(); + if (!imagetools_plugin) { + console.error( + '@sveltejs/image: vite-imagetools is not installed. Skipping build-time optimizations' + ); + } + return imagetools_plugin ? [image_plugin(), imagetools_plugin] : []; +} + +/** + * Creates the Svelte image plugin which provides the preprocessor. + * @returns {import('vite').Plugin} + */ +function image_plugin() { + const preprocessor = image(); + + return { + name: 'vite-plugin-svelte-image', + api: { + sveltePreprocess: preprocessor + } + }; +} + +// TODO: move this into vite-imagetools +/** @type {Record} */ +const fallback = { + '.heic': 'jpg', + '.heif': 'jpg', + '.avif': 'png', + '.jpeg': 'jpg', + '.jpg': 'jpg', + '.png': 'png', + '.tiff': 'jpg', + '.webp': 'png', + '.gif': 'gif' +}; + +async function imagetools() { + /** @type {typeof import('vite-imagetools').imagetools} */ + let imagetools; + try { + ({ imagetools } = await import('vite-imagetools')); + } catch (err) { + return; + } + + // TODO: make formats configurable + // TODO: generate img rather than picture if only a single format is provided + // TODO: support configurable widths + return imagetools({ + defaultDirectives: (url) => { + const ext = path.extname(url.pathname); + return new URLSearchParams(`format=avif;webp;${fallback[ext]}&as=picture`); + } + }); +} diff --git a/packages/image/test/Input.svelte b/packages/image/test/Input.svelte new file mode 100644 index 000000000000..798c88cad089 --- /dev/null +++ b/packages/image/test/Input.svelte @@ -0,0 +1,45 @@ + + +{foo} + +test + +test + +test + +test + +test + +test + +test + +{#each images as image} + + test +{/each} + + +test + +test + + + + + + + +test diff --git a/packages/image/test/Output.svelte b/packages/image/test/Output.svelte new file mode 100644 index 000000000000..05f99a903d68 --- /dev/null +++ b/packages/image/test/Output.svelte @@ -0,0 +1,85 @@ + + +{foo} + + + {#each Object.entries(___ASSET___0.sources) as [format, images]} + `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {/each} + test + + + + {#each Object.entries(___ASSET___0.sources) as [format, images]} + `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {/each} + test + + + + {#each Object.entries(___ASSET___1.sources) as [format, images]} + `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {/each} + test + + + + {#each Object.entries(___ASSET___0.sources) as [format, images]} + `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {/each} + test + + +test + + + {#each Object.entries(___ASSET___3.sources) as [format, images]} + `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {/each} + test + + +test + +{#each images as image} + + {#if typeof image === 'string'} + test +{:else} + + {#each Object.entries(image.sources) as [format, images]} + `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {/each} + test + +{/if} +{/each} + + +test + +test + + + + + + + +test diff --git a/packages/image/test/index.spec.js b/packages/image/test/index.spec.js new file mode 100644 index 000000000000..ab16f38a6059 --- /dev/null +++ b/packages/image/test/index.spec.js @@ -0,0 +1,21 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { preprocess } from 'svelte/compiler'; +import { expect, it } from 'vitest'; +import { image } from '../src/preprocessor.js'; + +const resolve = /** @param {string} file */ (file) => path.resolve(__dirname, file); + +it('Image preprocess snapshot test', async () => { + const filename = 'Input.svelte'; + const processed = await preprocess( + await fs.readFile(resolve(filename), { encoding: 'utf-8' }), + [image()], + { filename } + ); + + // Make imports readable + const outputCode = processed.code.replace(/import/g, '\n\timport'); + + expect(outputCode).toMatchFileSnapshot('./Output.svelte'); +}); diff --git a/packages/image/tsconfig.json b/packages/image/tsconfig.json new file mode 100644 index 000000000000..3e4314946f0f --- /dev/null +++ b/packages/image/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "paths": { + "types": ["./types/index"], + "types/*": ["./types/*"] + }, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src/**/*", "types/**/*", "test/**/*"] +} diff --git a/packages/image/types/vite.d.ts b/packages/image/types/vite.d.ts new file mode 100644 index 000000000000..b9db01fbacf8 --- /dev/null +++ b/packages/image/types/vite.d.ts @@ -0,0 +1,3 @@ +import { Plugin } from 'vite'; + +export function images(): Promise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea815958a4db..c45863153d1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -362,6 +362,40 @@ importers: specifier: workspace:* version: link:../../../adapter-auto + packages/image: + dependencies: + esm-env: + specifier: ^1.0.0 + version: 1.0.0 + magic-string: + specifier: ^0.30.0 + version: 0.30.3 + svelte-parse-markup: + specifier: ^0.1.1 + version: 0.1.2(svelte@4.2.0) + devDependencies: + '@types/estree': + specifier: ^1.0.2 + version: 1.0.2 + '@types/node': + specifier: ^16.18.6 + version: 16.18.6 + svelte: + specifier: ^4.0.5 + version: 4.2.0 + typescript: + specifier: ^4.9.4 + version: 4.9.4 + vite: + specifier: ^4.4.2 + version: 4.4.9(@types/node@16.18.6)(lightningcss@1.21.8) + vite-imagetools: + specifier: ^5.0.8 + version: 5.0.8(rollup@3.7.0) + vitest: + specifier: ^0.34.0 + version: 0.34.5(lightningcss@1.21.8)(playwright@1.30.0) + packages/kit: dependencies: '@sveltejs/vite-plugin-svelte': @@ -1013,6 +1047,9 @@ importers: '@sveltejs/amp': specifier: workspace:^ version: link:../../packages/amp + '@sveltejs/image': + specifier: workspace:^ + version: link:../../packages/image '@sveltejs/kit': specifier: workspace:^ version: link:../../packages/kit @@ -1974,7 +2011,7 @@ packages: rollup: optional: true dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 3.7.0 @@ -1988,7 +2025,7 @@ packages: rollup: optional: true dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 3.7.0 @@ -2057,7 +2094,7 @@ packages: debug: 4.3.4 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.2 + magic-string: 0.30.3 svelte: 4.1.2 svelte-hmr: 0.15.3(svelte@4.1.2) vite: 4.4.9(@types/node@16.18.6)(lightningcss@1.21.8) @@ -2111,8 +2148,8 @@ packages: '@types/geojson': 7946.0.10 dev: true - /@types/estree@1.0.1: - resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + /@types/estree@1.0.2: + resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} /@types/geojson@7946.0.10: resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==} @@ -2883,7 +2920,7 @@ packages: resolution: {integrity: sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 acorn: 8.10.0 estree-walker: 3.0.3 periscopic: 3.1.0 @@ -2892,11 +2929,10 @@ packages: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 acorn: 8.10.0 estree-walker: 3.0.3 periscopic: 3.1.0 - dev: true /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -3563,7 +3599,7 @@ packages: /estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -4160,18 +4196,17 @@ packages: /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 /is-reference@3.0.1: resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 /is-reference@3.0.2: resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} dependencies: - '@types/estree': 1.0.1 - dev: true + '@types/estree': 1.0.2 /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} @@ -4959,7 +4994,7 @@ packages: /periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.2 estree-walker: 3.0.3 is-reference: 3.0.1 @@ -5751,8 +5786,8 @@ packages: picocolors: 1.0.0 sade: 1.8.1 svelte: 4.1.2 - svelte-preprocess: 5.0.4(postcss@8.4.27)(svelte@4.1.2)(typescript@5.2.2) - typescript: 5.2.2 + svelte-preprocess: 5.0.4(postcss@8.4.27)(svelte@4.1.2)(typescript@5.0.4) + typescript: 5.0.4 transitivePeerDependencies: - '@babel/core' - coffeescript @@ -5800,6 +5835,14 @@ packages: svelte: 4.2.0 dev: true + /svelte-parse-markup@0.1.2(svelte@4.2.0): + resolution: {integrity: sha512-DycY7DJr7VqofiJ63ut1/NEG92HrWWL56VWITn/cJCu+LlZhMoBkBXT4opUitPEEwbq1nMQbv4vTKUfbOqIW1g==} + peerDependencies: + svelte: ^3.0.0 || ^4.0.0 + dependencies: + svelte: 4.2.0 + dev: false + /svelte-preprocess@5.0.4(postcss@8.4.27)(svelte@4.1.2)(typescript@4.9.4): resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} engines: {node: '>= 14.10.0'} @@ -5848,7 +5891,7 @@ packages: typescript: 4.9.4 dev: true - /svelte-preprocess@5.0.4(postcss@8.4.27)(svelte@4.1.2)(typescript@5.2.2): + /svelte-preprocess@5.0.4(postcss@8.4.27)(svelte@4.1.2)(typescript@5.0.4): resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==} engines: {node: '>= 14.10.0'} requiresBuild: true @@ -5893,7 +5936,7 @@ packages: sorcery: 0.11.0 strip-indent: 3.0.0 svelte: 4.1.2 - typescript: 5.2.2 + typescript: 5.0.4 dev: true /svelte2tsx@0.6.19(svelte@4.1.2)(typescript@4.9.4): @@ -5943,7 +5986,6 @@ packages: locate-character: 3.0.0 magic-string: 0.30.3 periscopic: 3.1.0 - dev: true /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} @@ -6230,12 +6272,6 @@ packages: engines: {node: '>=12.20'} hasBin: true - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - /ufo@1.3.0: resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==} dev: true diff --git a/sites/kit.svelte.dev/package.json b/sites/kit.svelte.dev/package.json index 6f8719637be4..0c40b087d1c6 100644 --- a/sites/kit.svelte.dev/package.json +++ b/sites/kit.svelte.dev/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@sveltejs/adapter-vercel": "workspace:^", "@sveltejs/amp": "workspace:^", + "@sveltejs/image": "workspace:^", "@sveltejs/kit": "workspace:^", "@sveltejs/site-kit": "6.0.0-next.50", "@types/d3-geo": "^3.0.4", diff --git a/sites/kit.svelte.dev/src/lib/Image.svelte b/sites/kit.svelte.dev/src/lib/Image.svelte deleted file mode 100644 index ab3830d54fb2..000000000000 --- a/sites/kit.svelte.dev/src/lib/Image.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - -{#if typeof src === 'string'} - -{:else} - - {#each Object.entries(src.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> - {/each} - - -{/if} diff --git a/sites/kit.svelte.dev/src/routes/home/Hero.svelte b/sites/kit.svelte.dev/src/routes/home/Hero.svelte index 212f655223d2..0cc612f18a83 100644 --- a/sites/kit.svelte.dev/src/routes/home/Hero.svelte +++ b/sites/kit.svelte.dev/src/routes/home/Hero.svelte @@ -1,8 +1,6 @@
@@ -17,7 +15,7 @@
- SvelteKit illustration + SvelteKit illustration
@@ -98,6 +96,7 @@ https://github.com/sveltejs/svelte/issues/2870#issuecomment-1161082065 */ .hero-image :global(img) { width: var(--size); + height: auto; aspect-ratio: 4 / 3; object-fit: cover; } diff --git a/sites/kit.svelte.dev/src/routes/home/Showcase.svelte b/sites/kit.svelte.dev/src/routes/home/Showcase.svelte index f13b2cdd9f7e..ccc8d8dbb0e3 100644 --- a/sites/kit.svelte.dev/src/routes/home/Showcase.svelte +++ b/sites/kit.svelte.dev/src/routes/home/Showcase.svelte @@ -1,5 +1,4 @@ - - ``` - */ -declare module "*.gif" { - const value: string; - export = value; -} - -declare module "*.jpg" { - const value: string; - export = value; -} - -declare module "*.jpeg" { - const value: string; - export = value; -} - -declare module "*.png" { - const value: string; - export = value; -} - -declare module "*.svg" { - const value: string; - export = value; -} - -declare module "*.webp" { - const value: string; - export = value; -} -//#endregion diff --git a/sites/kit.svelte.dev/src/routes/home/Showcase.svelte b/sites/kit.svelte.dev/src/routes/home/Showcase.svelte index 58966c393e9f..c95e4c11cd02 100644 --- a/sites/kit.svelte.dev/src/routes/home/Showcase.svelte +++ b/sites/kit.svelte.dev/src/routes/home/Showcase.svelte @@ -1,13 +1,13 @@ An alt text ``` +You can also use [Vite's `import.meta.glob`](https://vitejs.dev/guide/features.html#glob-import). Note that you will have to specify `svelte-static-img` via a [custom query](https://vitejs.dev/guide/features.html#custom-queries): + +```js +const pictures = import.meta.glob( + '/path/to/assets/*.{heic,heif,avif,jpg,jpeg,png,tiff,webp,gif,svg}', + { + query: { + 'svelte-static-img': true + } + } +); +``` + ### Intrinsic Dimensions `width` and `height` are optional as they can be inferred from the source image and will be automatically added when the `` tag is preprocessed. These attributes are added because the browser can reserve the correct amount of space when it knows image dimensions which prevents layout shift. If you'd like to use a different `width` and `height` you can style the image with CSS. Because the preprocessor adds a `width` and `height` for you, if you'd like one of the dimensions to be automatically calculated then you will need to specify that: From fc9c343a9a41b45a1f9abe481dedc01726f20d0b Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:17:03 -0700 Subject: [PATCH 010/108] upgrade vite-imagetools and get image width --- packages/static-img/package.json | 3 +-- packages/static-img/src/index.js | 8 +++----- packages/static-img/src/preprocessor.js | 9 +++++++-- packages/static-img/types/index.d.ts | 5 +++-- pnpm-lock.yaml | 14 +++++++------- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/static-img/package.json b/packages/static-img/package.json index 009f9a1b6473..2a2435affd7b 100644 --- a/packages/static-img/package.json +++ b/packages/static-img/package.json @@ -28,7 +28,7 @@ "dependencies": { "magic-string": "^0.30.0", "svelte-parse-markup": "^0.1.1", - "vite-imagetools": "^5.0.8" + "vite-imagetools": "^5.1.2" }, "devDependencies": { "@types/estree": "^1.0.2", @@ -36,7 +36,6 @@ "svelte": "^4.0.5", "typescript": "^4.9.4", "vite": "^4.4.2", - "vite-imagetools": "^5.0.8", "vitest": "^0.34.0" } } diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index 8ed0b27890bf..6e2bfede291c 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -58,7 +58,7 @@ async function imagetools(plugin_opts) { /** @type {import('../types').PluginOptions} */ const imagetools_opts = { - defaultDirectives: (url) => { + defaultDirectives: async (url, metadata) => { if (url.searchParams.has('static-img')) { /** @type {Record} */ const result = { @@ -68,9 +68,7 @@ async function imagetools(plugin_opts) { const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840]; const allSizes = [16, 32, 48, 64, 96, 128, 256, 384].concat(deviceSizes); const sizes = url.searchParams.get('sizes') ?? undefined; - // TODO: we can't get the width right now because it's not determined until the import is loaded - // we will need to eagerly load the import URL - const width = '100%'; + const width = url.searchParams.get('width') || (await metadata()).width; getWidths(deviceSizes, allSizes, width, sizes); result.w = ''; } @@ -94,7 +92,7 @@ async function imagetools(plugin_opts) { * https://github.com/vercel/next.js/blob/3f25a2e747fc27da6c2166e45d54fc95e96d7895/packages/next/src/shared/lib/get-img-props.ts#L132 * @param {number[]} deviceSizes * @param {number[]} allSizes - * @param {number | string} width + * @param {number | string | undefined} width * @param {string | undefined} sizes * @returns {{ widths: number[]; kind: 'w' | 'x' }} */ diff --git a/packages/static-img/src/preprocessor.js b/packages/static-img/src/preprocessor.js index 17e9ca4b0a7c..b92806a0f2de 100644 --- a/packages/static-img/src/preprocessor.js +++ b/packages/static-img/src/preprocessor.js @@ -44,10 +44,15 @@ export function image() { /** @type {string | undefined} */ const sizes = get_attr_value(node, 'sizes'); + const width = get_attr_value(node, 'width'); + url += (url.includes('?') ? '&' : '?'); if (sizes) { - url += (url.includes('?') ? '&' : '?') + 'sizes=' + encodeURIComponent(sizes); + url += 'sizes=' + encodeURIComponent(sizes) + '&'; } - url += (url.includes('?') ? '&' : '?') + 'static-img'; + if (width) { + url += 'width=' + encodeURIComponent(width) + '&'; + } + url += 'static-img'; let import_name = ''; if (imports.has(url)) { diff --git a/packages/static-img/types/index.d.ts b/packages/static-img/types/index.d.ts index 8226ad9745b4..438be3c80d94 100644 --- a/packages/static-img/types/index.d.ts +++ b/packages/static-img/types/index.d.ts @@ -1,4 +1,5 @@ -import { Plugin } from 'vite'; +import type { Plugin } from 'vite'; +import type { DefaultDirectives } from 'vite-imagetools'; interface PluginOptions { /** @@ -6,7 +7,7 @@ interface PluginOptions { * You can also provide a function, in which case the function gets passed the asset ID and should return an object of directives. * This can be used to define all sorts of shorthands or presets. */ - defaultDirectives?: URLSearchParams | ((url: URL) => URLSearchParams); + defaultDirectives?: DefaultDirectives; } export function staticImages(opts?: PluginOptions): Promise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51d82d6d8182..58769932ee26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -986,8 +986,8 @@ importers: specifier: ^0.1.1 version: 0.1.2(svelte@4.2.0) vite-imagetools: - specifier: ^5.0.8 - version: 5.0.8(rollup@3.7.0) + specifier: ^5.1.2 + version: 5.1.2(rollup@3.7.0) devDependencies: '@types/estree': specifier: ^1.0.2 @@ -4015,8 +4015,8 @@ packages: engines: {node: '>= 4'} dev: true - /imagetools-core@4.0.5: - resolution: {integrity: sha512-sNRVfUwkUcsVWNn5inTHDXWzpPRWPWbSgGkuQmlsFCWXAR2+K5R5vG5tC3Qs4LeJaMugKB8hGVm6rvZjFHQrUw==} + /imagetools-core@4.1.0: + resolution: {integrity: sha512-GDMgj3zhQ3g6Ga3yWBSmdJC7mM8UePgHiZsVQZ8sUWgAk2UzRyGlFV5qdv0topv63vE86KaK6MmAbjVUVibOzA==} engines: {node: '>=12.0.0'} dependencies: sharp: 0.32.5 @@ -6342,12 +6342,12 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /vite-imagetools@5.0.8(rollup@3.7.0): - resolution: {integrity: sha512-oFNfc58iLz1lHFsIKQy+wp0RNcZjiaDeHYTexYowpf4RYx9tZ97eWEcw8lQ1jDT8AnOso6XZi5iGjLNAeTR9Tw==} + /vite-imagetools@5.1.2(rollup@3.7.0): + resolution: {integrity: sha512-r8WVrrmYSmctm1IEMJuYSo3jGH93W6mf0NK3+85rmG+BJBO05E/YsbWw/lhE1nPBHgYb8liPC2ID3jSzGiPzJw==} engines: {node: '>=12.0.0'} dependencies: '@rollup/pluginutils': 5.0.4(rollup@3.7.0) - imagetools-core: 4.0.5 + imagetools-core: 4.1.0 transitivePeerDependencies: - rollup dev: false From 8e6e9029f20e1ed8b3012a66c9b00406611eb779 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:19:44 -0700 Subject: [PATCH 011/108] remove accidental import --- packages/static-img/src/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index 6e2bfede291c..0723af80c2a5 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -1,6 +1,5 @@ import path from 'node:path'; import { image } from './preprocessor.js'; -import { get } from 'node:http'; /** * @param {import('../types').PluginOptions | undefined} opts From d33d221828b29b562f3085130bba5bc4d87ffaf7 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:28:43 -0700 Subject: [PATCH 012/108] fix missing alt tags --- packages/static-img/src/preprocessor.js | 10 ++++--- packages/static-img/test/Output.svelte | 35 +++++++++++++------------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/static-img/src/preprocessor.js b/packages/static-img/src/preprocessor.js index b92806a0f2de..e50c3f507876 100644 --- a/packages/static-img/src/preprocessor.js +++ b/packages/static-img/src/preprocessor.js @@ -45,7 +45,7 @@ export function image() { /** @type {string | undefined} */ const sizes = get_attr_value(node, 'sizes'); const width = get_attr_value(node, 'width'); - url += (url.includes('?') ? '&' : '?'); + url += url.includes('?') ? '&' : '?'; if (sizes) { url += 'sizes=' + encodeURIComponent(sizes) + '&'; } @@ -180,9 +180,11 @@ function img_to_picture(content, node, import_name) { /** @type {Array} attributes */ const attributes = node.attributes; const index = attributes.findIndex((attribute) => attribute.name === 'sizes'); - const sizes_string = - index >= 0 ? ' ' + content.substring(attributes[index].start, attributes[index].end) : ''; - attributes.splice(index, 1); + let sizes_string = ''; + if (index >= 0) { + sizes_string = content.substring(attributes[index].start, attributes[index].end); + attributes.splice(index, 1); + } return ` {#each Object.entries(${import_name}.sources) as [format, images]} diff --git a/packages/static-img/test/Output.svelte b/packages/static-img/test/Output.svelte index 981c7393565e..2e5f1f6a2137 100644 --- a/packages/static-img/test/Output.svelte +++ b/packages/static-img/test/Output.svelte @@ -1,9 +1,10 @@ An alt text ``` -You can also use [Vite's `import.meta.glob`](https://vitejs.dev/guide/features.html#glob-import). Note that you will have to specify `svelte-static-img` via a [custom query](https://vitejs.dev/guide/features.html#custom-queries): +You can also use [Vite's `import.meta.glob`](https://vitejs.dev/guide/features.html#glob-import). Note that you will have to specify `static-img` via a [custom query](https://vitejs.dev/guide/features.html#custom-queries): ```js const pictures = import.meta.glob( '/path/to/assets/*.{heic,heif,avif,jpg,jpeg,png,tiff,webp,gif,svg}', { query: { - 'svelte-static-img': true + 'static-img': true } } ); @@ -131,7 +131,11 @@ In this example, we don't have you to have to manually create three versions of /> ``` -If `sizes` is specified as a `string` (i.e. not a text expression like `sizes={['(min-width: 60rem) 80vw', '(min-width: 40rem) 90vw', '100vw'].join(', ')}`) then we will automatically generate different width images. If `sizes` is not provided, `1x` and `2x` images will be generated. If sizes is provided, then a large srcset is generated. If some of the `sizes` have been specified as a percentage of the viewport width using the `vw` unit then the `srcset` will filter out any values which are too small to ever be requested by the browser. +If `sizes` is specified directly as a string on the `img` tag then the plugin will automatically generate different width images: +- If `sizes` is not provided, then a HiDPI/Retina image and a standard resolution image will be generated. The image you provide should be 2x the resolution you wish to display so that the browser can display that image on devices with a high [device pixel ratio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio). +- If sizes is provided, then a large srcset is generated. If some of the `sizes` have been specified as a percentage of the viewport width using the `vw` unit then the `srcset` will filter out any values which are too small to ever be requested by the browser. + +> Text expressions like `sizes={computedSizes}` or `sizes={['(min-width: 60rem) 80vw', '(min-width: 40rem) 90vw', '100vw'].join(', ')}` will not be evaluated for the purposes of automatic image generation and will be skipped. ### Per-image transforms diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index a8c05078048b..b80771aa728f 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -64,16 +64,10 @@ async function imagetools(plugin_opts) { as: 'picture' }; - const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840]; - const allSizes = [16, 32, 48, 64, 96, 128, 256, 384].concat(deviceSizes); const sizes = url.searchParams.get('sizes') ?? undefined; - const width = url.searchParams.get('width') || (await metadata()).width; - const calculated = getWidths(deviceSizes, allSizes, width, sizes); - // TODO: how should 2x work? we don't want to double the image... - // do we take the one on disk and half it? but that could be frustrating if it's not what you want... - if (calculated.kind === 'w') { - directives.w = calculated.widths.map((w) => w).join(','); - } + const widthParam = url.searchParams.get('width'); + const width = widthParam === null ? (await metadata()).width : parseInt(widthParam); + directives.w = getWidths(width, sizes).join(','); const ext = path.extname(url.pathname); directives.format = `avif;webp;${fallback[ext] ?? 'png'}`; @@ -91,49 +85,50 @@ async function imagetools(plugin_opts) { } /** - * Taken under MIT license (Copyright (c) 2023 Vercel, Inc.) from + * Derived from * https://github.com/vercel/next.js/blob/3f25a2e747fc27da6c2166e45d54fc95e96d7895/packages/next/src/shared/lib/get-img-props.ts#L132 - * @param {number[]} deviceSizes - * @param {number[]} allSizes - * @param {number | string | undefined} width + * under the MIT license. Copyright (c) Vercel, Inc. + * @param {number | undefined} width * @param {string | undefined} sizes - * @returns {{ widths: number[]; kind: 'w' | 'x' }} + * @param {number[]} [deviceSizes] + * @param {number[]} [imageSizes] + * @returns { number[] } */ -function getWidths(deviceSizes, allSizes, width, sizes) { +function getWidths(width, sizes, deviceSizes, imageSizes) { + const chosen_device_sizes = deviceSizes || [640, 750, 828, 1080, 1200, 1920, 2048, 3840]; + const all_sizes = (imageSizes || [16, 32, 48, 64, 96, 128, 256, 384]).concat(chosen_device_sizes); + if (sizes) { // Find all the "vw" percent sizes used in the sizes prop - const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g; - const percentSizes = []; - for (let match; (match = viewportWidthRe.exec(sizes)); match) { - percentSizes.push(parseInt(match[2])); + const viewport_width_re = /(^|\s)(1?\d?\d)vw/g; + const percent_sizes = []; + for (let match; (match = viewport_width_re.exec(sizes)); match) { + percent_sizes.push(parseInt(match[2])); } - if (percentSizes.length) { - const smallestRatio = Math.min(...percentSizes) * 0.01; - return { - widths: allSizes.filter((s) => s >= deviceSizes[0] * smallestRatio), - kind: 'w' - }; + if (percent_sizes.length) { + const smallest_ratio = Math.min(...percent_sizes) * 0.01; + return all_sizes.filter((s) => s >= chosen_device_sizes[0] * smallest_ratio); } - return { widths: allSizes, kind: 'w' }; + return all_sizes; } if (typeof width !== 'number') { - return { widths: deviceSizes, kind: 'w' }; + return chosen_device_sizes; } - const widths = [ - ...new Set( - // > This means that most OLED screens that say they are 3x resolution, - // > are actually 3x in the green color, but only 1.5x in the red and - // > blue colors. Showing a 3x resolution image in the app vs a 2x - // > resolution image will be visually the same, though the 3x image - // > takes significantly more data. Even true 3x resolution screens are - // > wasteful as the human eye cannot see that level of detail without - // > something like a magnifying glass. - // https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html - [width, width * 2 /*, width * 3*/].map( - (w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1] - ) - ) - ]; - return { widths, kind: 'x' }; + // Don't need more than 2x resolution. + // Most OLED screens that say they are 3x resolution, + // are actually 3x in the green color, but only 1.5x in the red and + // blue colors. Showing a 3x resolution image in the app vs a 2x + // resolution image will be visually the same, though the 3x image + // takes significantly more data. Even true 3x resolution screens are + // wasteful as the human eye cannot see that level of detail without + // something like a magnifying glass. + // https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html + + // We diverge from the Next.js logic here + // You can't really scale up an image, so you can't 2x the width + // Instead the user should provide the high-res image and we'll downscale + // Also, Vercel builds specific image sizes and picks the closest from those, + // but we can just build the ones we want exactly. + return [width, Math.round(width / 2)]; } From 627b3316b75c1726f49eee5761ea25f99a64e195 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 12 Oct 2023 20:19:11 -0700 Subject: [PATCH 016/108] fix join character --- packages/static-img/src/index.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index b80771aa728f..21ed16b20e7e 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -67,10 +67,11 @@ async function imagetools(plugin_opts) { const sizes = url.searchParams.get('sizes') ?? undefined; const widthParam = url.searchParams.get('width'); const width = widthParam === null ? (await metadata()).width : parseInt(widthParam); - directives.w = getWidths(width, sizes).join(','); + directives.w = getWidths(width, sizes).join(';'); const ext = path.extname(url.pathname); directives.format = `avif;webp;${fallback[ext] ?? 'png'}`; + return new URLSearchParams(directives); } return url.searchParams; @@ -125,9 +126,9 @@ function getWidths(width, sizes, deviceSizes, imageSizes) { // something like a magnifying glass. // https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html - // We diverge from the Next.js logic here - // You can't really scale up an image, so you can't 2x the width - // Instead the user should provide the high-res image and we'll downscale + // We diverge from the Next.js logic here. + // You can't really scale up an image, so you can't 2x the width. + // Instead the user should provide the high-res image and we'll downscale. // Also, Vercel builds specific image sizes and picks the closest from those, // but we can just build the ones we want exactly. return [width, Math.round(width / 2)]; From f79812d879dc9f70742b394d35419c85221bb0eb Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 12 Oct 2023 20:39:34 -0700 Subject: [PATCH 017/108] partial revert of earlier commit --- packages/static-img/src/index.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index 21ed16b20e7e..0fc2d6a98c6e 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -67,7 +67,7 @@ async function imagetools(plugin_opts) { const sizes = url.searchParams.get('sizes') ?? undefined; const widthParam = url.searchParams.get('width'); const width = widthParam === null ? (await metadata()).width : parseInt(widthParam); - directives.w = getWidths(width, sizes).join(';'); + directives.w = getWidths(width, sizes).widths.join(';'); const ext = path.extname(url.pathname); directives.format = `avif;webp;${fallback[ext] ?? 'png'}`; @@ -89,11 +89,11 @@ async function imagetools(plugin_opts) { * Derived from * https://github.com/vercel/next.js/blob/3f25a2e747fc27da6c2166e45d54fc95e96d7895/packages/next/src/shared/lib/get-img-props.ts#L132 * under the MIT license. Copyright (c) Vercel, Inc. - * @param {number | undefined} width + * @param {number | string | undefined} width * @param {string | undefined} sizes * @param {number[]} [deviceSizes] * @param {number[]} [imageSizes] - * @returns { number[] } + * @returns {{ widths: number[]; kind: 'w' | 'x' }} */ function getWidths(width, sizes, deviceSizes, imageSizes) { const chosen_device_sizes = deviceSizes || [640, 750, 828, 1080, 1200, 1920, 2048, 3840]; @@ -108,12 +108,15 @@ function getWidths(width, sizes, deviceSizes, imageSizes) { } if (percent_sizes.length) { const smallest_ratio = Math.min(...percent_sizes) * 0.01; - return all_sizes.filter((s) => s >= chosen_device_sizes[0] * smallest_ratio); + return { + widths: all_sizes.filter((s) => s >= chosen_device_sizes[0] * smallest_ratio), + kind: 'w' + }; } - return all_sizes; + return { widths: all_sizes, kind: 'w' }; } if (typeof width !== 'number') { - return chosen_device_sizes; + return { widths: chosen_device_sizes, kind: 'w' }; } // Don't need more than 2x resolution. @@ -126,10 +129,10 @@ function getWidths(width, sizes, deviceSizes, imageSizes) { // something like a magnifying glass. // https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html - // We diverge from the Next.js logic here. - // You can't really scale up an image, so you can't 2x the width. - // Instead the user should provide the high-res image and we'll downscale. + // We diverge from the Next.js logic here + // You can't really scale up an image, so you can't 2x the width + // Instead the user should provide the high-res image and we'll downscale // Also, Vercel builds specific image sizes and picks the closest from those, // but we can just build the ones we want exactly. - return [width, Math.round(width / 2)]; + return { widths: [width, Math.round(width / 2)], kind: 'x' }; } From 0e8d7e6abd3a6930b8c3e0fa35d5a7d35a46d42c Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 13 Oct 2023 09:03:06 -0700 Subject: [PATCH 018/108] caching docs --- documentation/docs/30-advanced/60-images.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/30-advanced/60-images.md b/documentation/docs/30-advanced/60-images.md index a23eb583878a..358962f556c3 100644 --- a/documentation/docs/30-advanced/60-images.md +++ b/documentation/docs/30-advanced/60-images.md @@ -22,9 +22,9 @@ For assets included via the CSS `url()` function, you may find [`vitePreprocess` You may wish to transform your images to output compressed image formats such as `.webp` or `.avif`, responsive images with different sizes for different devices, or images with the EXIF data stripped for privacy. There are two approaches two transforming images, which will be discussed below. With either approach, the transformed images may be served via a CDN. CDNs reduce latency by distributing copies of static assets globally. -The `@sveltejs/static-img` package only handles images that are located in your project and can be referred to with a static string. It can automatically set the intrinsic `width` and `height` for you, which can't be done with a dynamic approach. It generates images at build time, so building may take longer the more images you transform. +The `@sveltejs/static-img` package handles images that are located in your project and can be referred to with a static string. It can automatically set the intrinsic `width` and `height` for you, which can't be done with a dynamic approach. It is generated with a hash in the filename so that it can be maximally cached. It generates images at build time, so building may take longer the more images you transform. -Alternatively, using a CDN to do the image transformation provides more flexibility with regards to sizes and you can pass image sources not known at build time, but it comes with potentially a bit of setup overhead (configuring the image CDN) and possibly usage cost. Building HTML to target CDNs may result in slightly smaller and simpler HTML because they can serve the appropriate file format for an `img` tag based on the `User-Agent` header whereas build-time optimizations must produce `picture` tags. Finally some CDNs may generate images lazily, which could have a negative performance impact for sites with low traffic and frequently changing images. SvelteKit does not currently offer any tools for dynamic image transforms since they're more straightforward for users to implement on their own, but we may offer such utilities in the future. +Alternatively, using a CDN to do the image transformation provides more flexibility with regards to sizes and you can pass image sources not known at build time, but it comes with potentially a bit of setup overhead (configuring the image CDN) and possibly usage cost. Some images served from a CDN may require a request to the server to verify that the image has not changed. This will block the browser from using its cache until a [304 response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304) is received from the server. Building HTML to target CDNs may result in slightly smaller and simpler HTML because they can serve the appropriate file format for an `img` tag based on the `User-Agent` header whereas build-time optimizations must produce `picture` tags. Finally some CDNs may generate images lazily, which could have a negative performance impact for sites with low traffic and frequently changing images. SvelteKit does not currently offer any tools for dynamic image transforms since they're more straightforward for users to implement on their own, but we may offer such utilities in the future. You can mix and match both solutions in one project. For example, you may display images on your homepage with `@sveltejs/static-img` and display user-submitted content with a dynamic approach. From 31e82adfa7e530e17ad1357b1c8b113fe551f698 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 14 Oct 2023 07:57:05 -0700 Subject: [PATCH 019/108] pixel density descriptors --- packages/static-img/src/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index 0fc2d6a98c6e..a97176dcb62f 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -59,7 +59,7 @@ async function imagetools(plugin_opts) { const imagetools_opts = { defaultDirectives: async (url, metadata) => { if (url.searchParams.has('static-img')) { - /** @type {Record} */ + /** @type {Record} */ const directives = { as: 'picture' }; @@ -67,7 +67,11 @@ async function imagetools(plugin_opts) { const sizes = url.searchParams.get('sizes') ?? undefined; const widthParam = url.searchParams.get('width'); const width = widthParam === null ? (await metadata()).width : parseInt(widthParam); - directives.w = getWidths(width, sizes).widths.join(';'); + const calculated = getWidths(width, sizes); + directives.w = calculated.widths.join(';'); + if (calculated.kind === 'x') { + directives.basePixels = calculated.widths[0]; + } const ext = path.extname(url.pathname); directives.format = `avif;webp;${fallback[ext] ?? 'png'}`; @@ -134,5 +138,5 @@ function getWidths(width, sizes, deviceSizes, imageSizes) { // Instead the user should provide the high-res image and we'll downscale // Also, Vercel builds specific image sizes and picks the closest from those, // but we can just build the ones we want exactly. - return { widths: [width, Math.round(width / 2)], kind: 'x' }; + return { widths: [Math.round(width / 2), width], kind: 'x' }; } From 3d58fa16b34fa4e09252f5f66ad5e09407df3a4a Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 14 Oct 2023 08:21:41 -0700 Subject: [PATCH 020/108] changeset --- .changeset/eighty-timers-exist.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eighty-timers-exist.md diff --git a/.changeset/eighty-timers-exist.md b/.changeset/eighty-timers-exist.md new file mode 100644 index 000000000000..e451119a33da --- /dev/null +++ b/.changeset/eighty-timers-exist.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/static-img': patch +--- + +feat: add experimental `@sveltejs/static-img` package From 95aa3fa270eb8fafe05cdb4774dbf2cbf0ca4cfa Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:32:30 -0700 Subject: [PATCH 021/108] reduce code --- packages/static-img/src/index.js | 37 ++++++++++++-------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index a97176dcb62f..887d4b43bc4b 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -57,28 +57,19 @@ async function imagetools(plugin_opts) { /** @type {import('../types').PluginOptions} */ const imagetools_opts = { - defaultDirectives: async (url, metadata) => { - if (url.searchParams.has('static-img')) { - /** @type {Record} */ - const directives = { - as: 'picture' - }; + defaultDirectives: async ({ pathname, searchParams }, metadata) => { + if (!searchParams.has('static-img')) searchParams; - const sizes = url.searchParams.get('sizes') ?? undefined; - const widthParam = url.searchParams.get('width'); - const width = widthParam === null ? (await metadata()).width : parseInt(widthParam); - const calculated = getWidths(width, sizes); - directives.w = calculated.widths.join(';'); - if (calculated.kind === 'x') { - directives.basePixels = calculated.widths[0]; - } - - const ext = path.extname(url.pathname); - directives.format = `avif;webp;${fallback[ext] ?? 'png'}`; - - return new URLSearchParams(directives); - } - return url.searchParams; + const width_param = searchParams.get('width'); + const width = width_param ? parseInt(width_param) : (await metadata()).width; + const sizes = searchParams.get('sizes'); + const { widths } = getWidths(width, sizes); + return new URLSearchParams({ + as: 'picture', + format: `avif;webp;${fallback[path.extname(pathname)] ?? 'png'}`, + w: widths.join(';'), + ...(!sizes && { basePixels: widths[0].toString() }) + }); }, ...(plugin_opts || {}) }; @@ -93,8 +84,8 @@ async function imagetools(plugin_opts) { * Derived from * https://github.com/vercel/next.js/blob/3f25a2e747fc27da6c2166e45d54fc95e96d7895/packages/next/src/shared/lib/get-img-props.ts#L132 * under the MIT license. Copyright (c) Vercel, Inc. - * @param {number | string | undefined} width - * @param {string | undefined} sizes + * @param {number | undefined} width + * @param {string | null | undefined} sizes * @param {number[]} [deviceSizes] * @param {number[]} [imageSizes] * @returns {{ widths: number[]; kind: 'w' | 'x' }} From c9ccd24871f15897719653f3a09d30317811748c Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:35:12 -0700 Subject: [PATCH 022/108] tweak --- packages/static-img/src/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index 887d4b43bc4b..812af50a6cac 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -63,12 +63,12 @@ async function imagetools(plugin_opts) { const width_param = searchParams.get('width'); const width = width_param ? parseInt(width_param) : (await metadata()).width; const sizes = searchParams.get('sizes'); - const { widths } = getWidths(width, sizes); + const calculated = getWidths(width, sizes); return new URLSearchParams({ as: 'picture', format: `avif;webp;${fallback[path.extname(pathname)] ?? 'png'}`, - w: widths.join(';'), - ...(!sizes && { basePixels: widths[0].toString() }) + w: calculated.widths.join(';'), + ...(calculated.kind === 'x' && { basePixels: calculated.widths[0].toString() }) }); }, ...(plugin_opts || {}) From e41aa950e6ab42015827daf275aeb55a62ba5e1a Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:46:33 -0700 Subject: [PATCH 023/108] reduce code some more --- packages/static-img/src/index.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index 812af50a6cac..72b428062e6b 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -57,13 +57,10 @@ async function imagetools(plugin_opts) { /** @type {import('../types').PluginOptions} */ const imagetools_opts = { - defaultDirectives: async ({ pathname, searchParams }, metadata) => { - if (!searchParams.has('static-img')) searchParams; + defaultDirectives: async ({ pathname, searchParams: qs }, metadata) => { + if (!qs.has('static-img')) qs; - const width_param = searchParams.get('width'); - const width = width_param ? parseInt(width_param) : (await metadata()).width; - const sizes = searchParams.get('sizes'); - const calculated = getWidths(width, sizes); + const calculated = getWidths(qs.get('width') ?? (await metadata()).width, qs.get('sizes')); return new URLSearchParams({ as: 'picture', format: `avif;webp;${fallback[path.extname(pathname)] ?? 'png'}`, @@ -84,13 +81,14 @@ async function imagetools(plugin_opts) { * Derived from * https://github.com/vercel/next.js/blob/3f25a2e747fc27da6c2166e45d54fc95e96d7895/packages/next/src/shared/lib/get-img-props.ts#L132 * under the MIT license. Copyright (c) Vercel, Inc. - * @param {number | undefined} width + * @param {number | string | undefined} width * @param {string | null | undefined} sizes * @param {number[]} [deviceSizes] * @param {number[]} [imageSizes] * @returns {{ widths: number[]; kind: 'w' | 'x' }} */ function getWidths(width, sizes, deviceSizes, imageSizes) { + width = typeof width === 'string' ? parseInt(width) : width; const chosen_device_sizes = deviceSizes || [640, 750, 828, 1080, 1200, 1920, 2048, 3840]; const all_sizes = (imageSizes || [16, 32, 48, 64, 96, 128, 256, 384]).concat(chosen_device_sizes); From 89a49b54c8032c7e99cef3e3fc8c634d6eca60e4 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:37:24 -0700 Subject: [PATCH 024/108] lint --- sites/kit.svelte.dev/src/app.d.ts | 4 ++-- sites/kit.svelte.dev/vite.config.js | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/sites/kit.svelte.dev/src/app.d.ts b/sites/kit.svelte.dev/src/app.d.ts index 01ee8741538d..97437ffb950c 100644 --- a/sites/kit.svelte.dev/src/app.d.ts +++ b/sites/kit.svelte.dev/src/app.d.ts @@ -11,8 +11,8 @@ declare global { // TODO: I think we need to do something like this to fix Showcase.svelte but why isn't it working? declare module '*?static-img' { - const value: string - export default value + const value: string; + export default value; } export {}; diff --git a/sites/kit.svelte.dev/vite.config.js b/sites/kit.svelte.dev/vite.config.js index 51b049457671..b236be98576c 100644 --- a/sites/kit.svelte.dev/vite.config.js +++ b/sites/kit.svelte.dev/vite.config.js @@ -19,10 +19,7 @@ const config = { cssMinify: 'lightningcss' }, - plugins: [ - staticImages(), - sveltekit() - ], + plugins: [staticImages(), sveltekit()], ssr: { noExternal: ['@sveltejs/site-kit'] From bc7adda7a9bcfb7acfa650304a3e4edc8bc11b4d Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:09:29 -0700 Subject: [PATCH 025/108] docs update --- documentation/docs/30-advanced/60-images.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/30-advanced/60-images.md b/documentation/docs/30-advanced/60-images.md index 358962f556c3..a3d1b41a3960 100644 --- a/documentation/docs/30-advanced/60-images.md +++ b/documentation/docs/30-advanced/60-images.md @@ -24,7 +24,7 @@ You may wish to transform your images to output compressed image formats such as The `@sveltejs/static-img` package handles images that are located in your project and can be referred to with a static string. It can automatically set the intrinsic `width` and `height` for you, which can't be done with a dynamic approach. It is generated with a hash in the filename so that it can be maximally cached. It generates images at build time, so building may take longer the more images you transform. -Alternatively, using a CDN to do the image transformation provides more flexibility with regards to sizes and you can pass image sources not known at build time, but it comes with potentially a bit of setup overhead (configuring the image CDN) and possibly usage cost. Some images served from a CDN may require a request to the server to verify that the image has not changed. This will block the browser from using its cache until a [304 response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304) is received from the server. Building HTML to target CDNs may result in slightly smaller and simpler HTML because they can serve the appropriate file format for an `img` tag based on the `User-Agent` header whereas build-time optimizations must produce `picture` tags. Finally some CDNs may generate images lazily, which could have a negative performance impact for sites with low traffic and frequently changing images. SvelteKit does not currently offer any tools for dynamic image transforms since they're more straightforward for users to implement on their own, but we may offer such utilities in the future. +Alternatively, using a CDN to do the image transformation provides more flexibility with regards to sizes and you can pass image sources not known at build time, but it comes with potentially a bit of setup overhead (configuring the image CDN) and possibly usage cost. Some images served from a CDN may require a request to the server to verify that the image has not changed. This will block the browser from using its cache until a [304 response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304) is received from the server. Building HTML to target CDNs may result in slightly smaller and simpler HTML because they can serve the appropriate file format for an `img` tag based on the `User-Agent` header whereas build-time optimizations must produce `picture` tags. Finally some CDNs may generate images lazily, which could have a negative performance impact for sites with low traffic and frequently changing images. We do not currently offer any tools for dynamic image transforms since they're more straightforward for users to implement on their own, but we may offer such utilities in the future. You can mix and match both solutions in one project. For example, you may display images on your homepage with `@sveltejs/static-img` and display user-submitted content with a dynamic approach. @@ -32,7 +32,7 @@ You can mix and match both solutions in one project. For example, you may displa > **WARNING**: The `@sveltejs/static-img` package is experimental. It uses pre-1.0 versioning and may introduce breaking changes with every minor version release. -`@sveltejs/static-img` aims to bring plug and play image processing to SvelteKit that is opinionated enough so you don't have to worry about the details, yet flexible enough for more advanced use cases or tweaks. It serves smaller file formats like `avif` or `webp` and automatically adds the intrinsic width and height of the image to avoid layout shift. +`@sveltejs/static-img` aims to offer plug and play image processing that is opinionated enough so you don't have to worry about the details, yet flexible enough for more advanced use cases or tweaks. It serves smaller file formats like `avif` or `webp`, automatically sets the intrinsic width and height of the image to avoid layout shift, and creates images of multiple sizes for use with the `sizes attribute`. It will work in any Vite-based project including, but not limited to, Svelte projects. ### Setup From b5c7a9421a4d6f45d1d768c39b69cce7274988da Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:00:36 -0700 Subject: [PATCH 026/108] redirect --- sites/kit.svelte.dev/src/hooks.server.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sites/kit.svelte.dev/src/hooks.server.js b/sites/kit.svelte.dev/src/hooks.server.js index c7e8742e90e9..572abb94ea5c 100644 --- a/sites/kit.svelte.dev/src/hooks.server.js +++ b/sites/kit.svelte.dev/src/hooks.server.js @@ -1,6 +1,7 @@ const redirects = { - '/docs/typescript': '/docs/types', - '/docs/amp': '/docs/seo#manual-setup-amp' + '/docs/amp': '/docs/seo#manual-setup-amp', + '/docs/assets': '/docs/images', + '/docs/typescript': '/docs/types' }; const preload_types = ['js', 'css', 'font']; From 7fabe7b012cd652bf0f2069fc8bcc62ee58b9b68 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:22:24 -0700 Subject: [PATCH 027/108] imagetools 6 --- packages/static-img/package.json | 2 +- packages/static-img/src/preprocessor.js | 4 +-- pnpm-lock.yaml | 43 ++++++++----------------- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/packages/static-img/package.json b/packages/static-img/package.json index 2a2435affd7b..b32a0263d5a5 100644 --- a/packages/static-img/package.json +++ b/packages/static-img/package.json @@ -28,7 +28,7 @@ "dependencies": { "magic-string": "^0.30.0", "svelte-parse-markup": "^0.1.1", - "vite-imagetools": "^5.1.2" + "vite-imagetools": "^6.0.0" }, "devDependencies": { "@types/estree": "^1.0.2", diff --git a/packages/static-img/src/preprocessor.js b/packages/static-img/src/preprocessor.js index 044ec7412ca1..421ac8c03fda 100644 --- a/packages/static-img/src/preprocessor.js +++ b/packages/static-img/src/preprocessor.js @@ -186,8 +186,8 @@ function img_to_picture(content, node, import_name) { } return ` - {#each Object.entries(${import_name}.sources) as [format, images]} - \`\${i.src} \${i.w}w\`).join(', ')}${sizes_string} type={'image/' + format} /> + {#each Object.entries(${import_name}.sources) as [format, srcset]} + {/each} `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58769932ee26..045763c9ce08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -986,8 +986,8 @@ importers: specifier: ^0.1.1 version: 0.1.2(svelte@4.2.0) vite-imagetools: - specifier: ^5.1.2 - version: 5.1.2(rollup@3.7.0) + specifier: ^6.0.0 + version: 6.0.0(rollup@3.7.0) devDependencies: '@types/estree': specifier: ^1.0.2 @@ -2015,21 +2015,6 @@ packages: picomatch: 2.3.1 rollup: 3.7.0 - /@rollup/pluginutils@5.0.4(rollup@3.7.0): - resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@types/estree': 1.0.2 - estree-walker: 2.0.2 - picomatch: 2.3.1 - rollup: 3.7.0 - dev: false - /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -4015,11 +4000,11 @@ packages: engines: {node: '>= 4'} dev: true - /imagetools-core@4.1.0: - resolution: {integrity: sha512-GDMgj3zhQ3g6Ga3yWBSmdJC7mM8UePgHiZsVQZ8sUWgAk2UzRyGlFV5qdv0topv63vE86KaK6MmAbjVUVibOzA==} + /imagetools-core@5.0.0: + resolution: {integrity: sha512-BCtqtjFoFo0C423IaeoKxS2SYp3s7+Oilu9ToRG9q5OBmT/wx0kEeHEiW87Ly2er2jX2fChFDuckQGzb3gFwFQ==} engines: {node: '>=12.0.0'} dependencies: - sharp: 0.32.5 + sharp: 0.32.6 dev: false /import-fresh@3.3.0: @@ -4761,8 +4746,8 @@ packages: tslib: 2.6.1 dev: false - /node-abi@3.47.0: - resolution: {integrity: sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==} + /node-abi@3.51.0: + resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==} engines: {node: '>=10'} dependencies: semver: 7.5.4 @@ -5110,7 +5095,7 @@ packages: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 1.0.2 - node-abi: 3.47.0 + node-abi: 3.51.0 pump: 3.0.0 rc: 1.2.8 simple-get: 4.0.1 @@ -5450,8 +5435,8 @@ packages: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} dev: false - /sharp@0.32.5: - resolution: {integrity: sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==} + /sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} engines: {node: '>=14.15.0'} requiresBuild: true dependencies: @@ -6342,12 +6327,12 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /vite-imagetools@5.1.2(rollup@3.7.0): - resolution: {integrity: sha512-r8WVrrmYSmctm1IEMJuYSo3jGH93W6mf0NK3+85rmG+BJBO05E/YsbWw/lhE1nPBHgYb8liPC2ID3jSzGiPzJw==} + /vite-imagetools@6.0.0(rollup@3.7.0): + resolution: {integrity: sha512-t/k4LXN6+V+otKRM3EAsqG/iHS2Ja8S+biEJWBJk5IKeyQpJ2KZKR5hAn7n5UlNC7J5EzUpiBkUyUFKxGr90fw==} engines: {node: '>=12.0.0'} dependencies: - '@rollup/pluginutils': 5.0.4(rollup@3.7.0) - imagetools-core: 4.1.0 + '@rollup/pluginutils': 5.0.2(rollup@3.7.0) + imagetools-core: 5.0.0 transitivePeerDependencies: - rollup dev: false From 08aa49c2de2ae5ba1f7d515a0286f5351f92ff02 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:26:26 -0700 Subject: [PATCH 028/108] remove options --- packages/static-img/src/index.js | 15 +++++---------- packages/static-img/types/index.d.ts | 12 +----------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/static-img/src/index.js b/packages/static-img/src/index.js index 72b428062e6b..d5dbc433ca67 100644 --- a/packages/static-img/src/index.js +++ b/packages/static-img/src/index.js @@ -2,11 +2,10 @@ import path from 'node:path'; import { image } from './preprocessor.js'; /** - * @param {import('../types').PluginOptions | undefined} opts * @returns {Promise} */ -export async function staticImages(opts) { - const imagetools_plugin = await imagetools(opts); +export async function staticImages() { + const imagetools_plugin = await imagetools(); if (!imagetools_plugin) { console.error( '@sveltejs/static-img: vite-imagetools is not installed. Skipping build-time optimizations' @@ -43,10 +42,7 @@ const fallback = { '.gif': 'gif' }; -/** - * @param {import('../types').PluginOptions | undefined} plugin_opts - */ -async function imagetools(plugin_opts) { +async function imagetools() { /** @type {typeof import('vite-imagetools').imagetools} */ let imagetools; try { @@ -55,7 +51,7 @@ async function imagetools(plugin_opts) { return; } - /** @type {import('../types').PluginOptions} */ + /** @type {Partial} */ const imagetools_opts = { defaultDirectives: async ({ pathname, searchParams: qs }, metadata) => { if (!qs.has('static-img')) qs; @@ -67,8 +63,7 @@ async function imagetools(plugin_opts) { w: calculated.widths.join(';'), ...(calculated.kind === 'x' && { basePixels: calculated.widths[0].toString() }) }); - }, - ...(plugin_opts || {}) + } }; // TODO: should we make formats or sizes configurable besides just letting people override defaultDirectives? diff --git a/packages/static-img/types/index.d.ts b/packages/static-img/types/index.d.ts index 438be3c80d94..e5e952f11eaf 100644 --- a/packages/static-img/types/index.d.ts +++ b/packages/static-img/types/index.d.ts @@ -1,13 +1,3 @@ import type { Plugin } from 'vite'; -import type { DefaultDirectives } from 'vite-imagetools'; -interface PluginOptions { - /** - * This option allows you to specify directives that should be applied _by default_ to every image. - * You can also provide a function, in which case the function gets passed the asset ID and should return an object of directives. - * This can be used to define all sorts of shorthands or presets. - */ - defaultDirectives?: DefaultDirectives; -} - -export function staticImages(opts?: PluginOptions): Promise; +export function staticImages(): Promise; From 1187cce8fe27ac005e5269b497dbadc50fbcdb5e Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:36:42 -0700 Subject: [PATCH 029/108] update test output --- packages/static-img/test/Output.svelte | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/static-img/test/Output.svelte b/packages/static-img/test/Output.svelte index fb23f41d22f3..3c38a1f86313 100644 --- a/packages/static-img/test/Output.svelte +++ b/packages/static-img/test/Output.svelte @@ -21,43 +21,43 @@ {foo} - {#each Object.entries(___ASSET___0.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {#each Object.entries(___ASSET___0.sources) as [format, srcset]} + {/each} basic test - {#each Object.entries(___ASSET___1.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {#each Object.entries(___ASSET___1.sources) as [format, srcset]} + {/each} dimensions test - {#each Object.entries(___ASSET___1.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {#each Object.entries(___ASSET___1.sources) as [format, srcset]} + {/each} unquoted dimensions test - {#each Object.entries(___ASSET___2.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {#each Object.entries(___ASSET___2.sources) as [format, srcset]} + {/each} directive test - {#each Object.entries(___ASSET___0.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {#each Object.entries(___ASSET___0.sources) as [format, srcset]} + {/each} spread attributes test - {#each Object.entries(___ASSET___3.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')}sizes="(min-width: 60rem) 80vw, (min-width: 40rem) 90vw, 100vw" type={'image/' + format} /> + {#each Object.entries(___ASSET___3.sources) as [format, srcset]} + {/each} sizes test @@ -65,8 +65,8 @@ event handler test - {#each Object.entries(___ASSET___5.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {#each Object.entries(___ASSET___5.sources) as [format, srcset]} + {/each} alias test @@ -79,8 +79,8 @@ opt-in test {:else} - {#each Object.entries(image.sources) as [format, images]} - `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} /> + {#each Object.entries(image.sources) as [format, srcset]} + {/each} opt-in test From 882335a937febcbd10cf16d262a718842f33a139 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 18 Oct 2023 09:11:38 -0700 Subject: [PATCH 030/108] fix typescript issue --- sites/kit.svelte.dev/src/ambient.d.ts | 4 ++++ sites/kit.svelte.dev/src/app.d.ts | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 sites/kit.svelte.dev/src/ambient.d.ts diff --git a/sites/kit.svelte.dev/src/ambient.d.ts b/sites/kit.svelte.dev/src/ambient.d.ts new file mode 100644 index 000000000000..7952d9193c03 --- /dev/null +++ b/sites/kit.svelte.dev/src/ambient.d.ts @@ -0,0 +1,4 @@ +declare module '*?static-img' { + const value: string; + export default value; +} diff --git a/sites/kit.svelte.dev/src/app.d.ts b/sites/kit.svelte.dev/src/app.d.ts index 97437ffb950c..f59b884c51ed 100644 --- a/sites/kit.svelte.dev/src/app.d.ts +++ b/sites/kit.svelte.dev/src/app.d.ts @@ -9,10 +9,4 @@ declare global { } } -// TODO: I think we need to do something like this to fix Showcase.svelte but why isn't it working? -declare module '*?static-img' { - const value: string; - export default value; -} - export {}; From 46341ce23297a0a25810a54a1d42dbd42ecff015 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 18 Oct 2023 09:49:06 -0700 Subject: [PATCH 031/108] docs syntax --- documentation/docs/30-advanced/60-images.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/30-advanced/60-images.md b/documentation/docs/30-advanced/60-images.md index a3d1b41a3960..777a2d106a0b 100644 --- a/documentation/docs/30-advanced/60-images.md +++ b/documentation/docs/30-advanced/60-images.md @@ -6,7 +6,7 @@ title: Images [Vite will automatically process imported assets](https://vitejs.dev/guide/assets.html) for improved performance. Hashes will be added to the filenames so that they can be cached and assets smaller than `assetsInlineLimit` will be inlined. -```html +```svelte @@ -102,7 +102,7 @@ const pictures = import.meta.glob( `width` and `height` are optional as they can be inferred from the source image and will be automatically added when the `` tag is preprocessed. These attributes are added because the browser can reserve the correct amount of space when it knows image dimensions which prevents layout shift. If you'd like to use a different `width` and `height` you can style the image with CSS. Because the preprocessor adds a `width` and `height` for you, if you'd like one of the dimensions to be automatically calculated then you will need to specify that: -``` +```svelte