next-imagetools 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +489 -2
- package/client.d.ts +138 -0
- package/dist/asset-loader.cjs +38 -0
- package/dist/config-MI8A5KvL.d.mts +27 -0
- package/dist/config-MI8A5KvL.d.mts.map +1 -0
- package/dist/config.cjs +7 -0
- package/dist/config.d.cts +26 -0
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +2 -0
- package/dist/config.mjs +8 -0
- package/dist/config.mjs.map +1 -0
- package/dist/core.cjs +9 -0
- package/dist/core.d.cts +1 -0
- package/dist/core.d.mts +1 -0
- package/dist/core.mjs +2 -0
- package/dist/index.cjs +87 -0
- package/dist/index.d.cts +46 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +88 -0
- package/dist/index.mjs.map +1 -0
- package/dist/loader.cjs +441 -0
- package/dist/shared-C5zWV4nI.cjs +300 -0
- package/package.json +108 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Roya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,490 @@
|
|
|
1
|
-
#
|
|
1
|
+
# next-imagetools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/awdr74100/next-imagetools/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/awdr74100/next-imagetools/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Build-time image transforms for Next.js with Turbopack. Describe the variants you want in the import, and get static, optimized files — no image server, no runtime cost, and it works with `output: 'export'`.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import hero from './hero.jpg?w=480;960;1440&format=avif;webp&as=picture';
|
|
10
|
+
|
|
11
|
+
export function Hero() {
|
|
12
|
+
return (
|
|
13
|
+
<picture>
|
|
14
|
+
{Object.entries(hero.sources).map(([format, srcset]) => (
|
|
15
|
+
<source key={format} type={`image/${format}`} srcSet={srcset} />
|
|
16
|
+
))}
|
|
17
|
+
<img src={hero.img.src} width={hero.img.w} height={hero.img.h} alt="" />
|
|
18
|
+
</picture>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Powered by [imagetools-core](https://github.com/JonasKruckenberg/imagetools), the engine behind vite-imagetools, so the directive syntax is the same.
|
|
24
|
+
|
|
25
|
+
> [!NOTE]
|
|
26
|
+
> **Pre-release.** Everything documented here is implemented and tested, but the API may still change before 1.0.
|
|
27
|
+
|
|
28
|
+
- [Why](#why)
|
|
29
|
+
- [Installation](#installation)
|
|
30
|
+
- [Usage](#usage)
|
|
31
|
+
- [TypeScript](#typescript)
|
|
32
|
+
- [Recipes](#recipes)
|
|
33
|
+
- [Configuration](#configuration)
|
|
34
|
+
- [Migrating from vite-imagetools](#migrating-from-vite-imagetools)
|
|
35
|
+
- [How it works](#how-it-works)
|
|
36
|
+
- [Troubleshooting](#troubleshooting)
|
|
37
|
+
|
|
38
|
+
## Why
|
|
39
|
+
|
|
40
|
+
`next/image` optimizes images **on demand**, when they are first requested. That needs a running Next.js server, and with `output: 'export'` it is not available at all — a limitation discussed since 2020 in [vercel/next.js#19065](https://github.com/vercel/next.js/discussions/19065).
|
|
41
|
+
|
|
42
|
+
Pre-generating images by crawling the rendered pages has its own blind spot: an image that only appears after a click, inside a client-only component, or behind a login is never rendered at build time, so it 404s in production. [nuxt/image#2379](https://github.com/nuxt/image/issues/2379) documents exactly this failure.
|
|
43
|
+
|
|
44
|
+
next-imagetools generates images from **imports** instead. If your code imports an image, every variant it asks for is built — however and whenever it ends up rendered.
|
|
45
|
+
|
|
46
|
+
- **Explicit.** The transforms live in the import that uses them, not in a config file somewhere else.
|
|
47
|
+
- **Static.** Output is plain files under `/_next/static/media`, cached by your CDN like any other asset.
|
|
48
|
+
- **Every rendering mode.** Static, SSR, Edge, client components, the Pages Router and static export all behave the same.
|
|
49
|
+
- **Fast rebuilds.** Processed images are cached on disk, keyed by the source content and the directives.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm add next-imagetools
|
|
55
|
+
# or: npm install next-imagetools / yarn add next-imagetools
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Requirements:** Next.js 16.2 or newer and Node.js 22.12 or newer. Next.js builds with Turbopack by default; projects that opt into webpack (`next dev --webpack`, `next build --webpack`) aren't supported yet — support is planned for the next release ([#23](https://github.com/awdr74100/next-imagetools/issues/23)).
|
|
59
|
+
|
|
60
|
+
Wrap your Next.js config with `withImagetools`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// next.config.ts
|
|
64
|
+
import type { NextConfig } from 'next';
|
|
65
|
+
import { withImagetools } from 'next-imagetools';
|
|
66
|
+
|
|
67
|
+
const nextConfig: NextConfig = {
|
|
68
|
+
// your existing config
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default withImagetools(nextConfig);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Wrapping the config is how Next.js plugins hook in (`@next/mdx`, `@next/bundle-analyzer` and others work the same way). `withImagetools` only adds rules to `turbopack.rules`; the rest of your config is passed through untouched, and rules you already have for image files keep running first.
|
|
75
|
+
|
|
76
|
+
With TypeScript, also [reference the client types](#typescript).
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
Add a query to an image import. The query is a list of **directives**:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import thumbnail from './photo.jpg?w=300&format=webp';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Imports **without** a query are not touched, so `import logo from './logo.png'` still behaves exactly as Next.js defines it.
|
|
87
|
+
|
|
88
|
+
### Directives
|
|
89
|
+
|
|
90
|
+
The most common ones:
|
|
91
|
+
|
|
92
|
+
- `w` / `h` — width / height in pixels
|
|
93
|
+
- `format` — `avif`, `webp`, `jpg`, `png`, `gif`, `tiff` or `heif`
|
|
94
|
+
- `quality` — `1`–`100`; see [Quality](#quality) for the defaults
|
|
95
|
+
- `fit` — `cover`, `contain`, `fill`, `inside` or `outside` (when both `w` and `h` are set)
|
|
96
|
+
- `position` — where to crop from with `fit=cover`, e.g. `top` or `attention`
|
|
97
|
+
- `blur`, `rotate`, `flip`, `flop`, `grayscale`, `tint`, `normalize`, `lossless`, `progressive`, …
|
|
98
|
+
|
|
99
|
+
See the [imagetools directive reference](https://github.com/JonasKruckenberg/imagetools/blob/main/docs/directives.md) for every directive. It still lists `withoutEnlargement` and `withoutReduction`, which no longer have an effect ([imagetools#965](https://github.com/JonasKruckenberg/imagetools/pull/965)); use `allowUpscale` instead.
|
|
100
|
+
|
|
101
|
+
Mistakes are reported instead of silently producing the wrong image. Each message names the import it is about:
|
|
102
|
+
|
|
103
|
+
- an **unknown directive** logs a warning with the closest match — `widht=300` → _Did you mean w?_
|
|
104
|
+
- an **invalid value** fails the build — `w=abc`, `quality=0`, or a `format` without output support such as `jxl`
|
|
105
|
+
- an **unknown output format** fails the build — `as=piture` → _Did you mean as=picture?_
|
|
106
|
+
- a **wrong type marker** fails the build — see [TypeScript](#typescript)
|
|
107
|
+
|
|
108
|
+
A stray separator is ignored: `format=webp;` means `format=webp`, not a second image without a format.
|
|
109
|
+
|
|
110
|
+
Sizes larger than the source are reduced to it: a 1600px photo never comes out at 3000px. Add `allowUpscale=true` to scale up. Photos with an EXIF rotation, such as phone portraits, are measured upright.
|
|
111
|
+
|
|
112
|
+
If reducing makes two variants identical — `w=1600;3000` on a 1600px photo — both are kept and share one file, and the build warns. The [shape of the import](#multiple-variants) never depends on the source.
|
|
113
|
+
|
|
114
|
+
Add `inline` to embed the image in your JavaScript as a `data:` URL instead of emitting a file — worth it for tiny images such as blurred placeholders, where a separate request costs more than the bytes:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import placeholder from './photo.jpg?w=24&blur=2&format=webp&inline';
|
|
118
|
+
// "data:image/webp;base64,…"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Animated GIF and WebP sources keep their animation when the output is `gif` or `webp`; every frame is resized and cropped alike. Other formats can't hold frames, so they keep the first one, and the build warns. (A multi-page TIFF isn't an animation: it gives its first page.)
|
|
122
|
+
|
|
123
|
+
### Quality
|
|
124
|
+
|
|
125
|
+
Every generated image is encoded again, also when `format` isn't set: `?w=800` on a JPEG gives a new JPEG. Without `quality`, each format uses [sharp's default](https://sharp.pixelplumbing.com/api-output):
|
|
126
|
+
|
|
127
|
+
- **JPEG, WebP, TIFF:** 80
|
|
128
|
+
- **AVIF, HEIF:** 50, on a different scale — it looks about like JPEG at 80
|
|
129
|
+
- **PNG:** lossless
|
|
130
|
+
|
|
131
|
+
These are the same as in vite-imagetools, which uses the same engine. (They differ from `next/image`, which uses 75.) To set one quality for every import, use [`defaultDirectives`](#presets-and-defaults):
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
export default withImagetools(nextConfig, { defaultDirectives: 'quality=75' });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
An import's own `quality` still wins.
|
|
138
|
+
|
|
139
|
+
### Multiple variants
|
|
140
|
+
|
|
141
|
+
Separate values with `;` to get one image per value. With several directives, you get every combination:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// 3 widths × 2 formats = 6 images
|
|
145
|
+
import urls from './photo.jpg?w=400;800;1200&format=avif;webp';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The number of images is always the product of the value counts in the query, so you can tell from the query alone whether an import gives one value or an array. It never depends on the source file: swapping in a smaller photo can't turn an array into a string.
|
|
149
|
+
|
|
150
|
+
### Output formats
|
|
151
|
+
|
|
152
|
+
`as` chooses what the import gives you back. The examples leave out the [TypeScript markers](#typescript).
|
|
153
|
+
|
|
154
|
+
**Default — URL.** One image gives a URL string; several give an array of URLs.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import url from './photo.jpg?w=800&format=webp'; // "/_next/static/media/…webp"
|
|
158
|
+
import urls from './photo.jpg?w=400;800&format=webp'; // ["…", "…"]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**`as=srcset`** — a ready-made `srcset` string.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import srcset from './photo.jpg?w=400;800;1200&format=webp&as=srcset';
|
|
165
|
+
|
|
166
|
+
<img srcSet={srcset} sizes="(min-width: 768px) 50vw, 100vw" alt="" />;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**`as=img`** — `{ src, w, h }` for the largest image, plus `srcset` when there are several.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import photo from './photo.jpg?w=400;800&format=webp&as=img';
|
|
173
|
+
|
|
174
|
+
<img src={photo.src} srcSet={photo.srcset} width={photo.w} height={photo.h} alt="" />;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**`as=picture`** — `{ sources, img }`: a `srcset` per format for `<source>`, and a fallback `<img>` from the **last** format, so list the format you want as fallback last. The fallback format also gets a source when it has several sizes, so browsers without AVIF or WebP still pick a fitting size. Format keys are MIME subtypes (`jpeg`, not `jpg`).
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import photo from './photo.jpg?w=400;800&format=avif;webp;jpg&as=picture';
|
|
181
|
+
// photo.sources → { avif: "…", webp: "…", jpeg: "…" }, photo.img → { src, w, h } (the 800px jpg)
|
|
182
|
+
import single from './photo.jpg?w=800&format=avif;jpg&as=picture';
|
|
183
|
+
// single.sources → { avif: "…" }: one fallback size needs no source
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**`as=metadata`** (or `as=meta`) — the image's metadata: `src`, `width`, `height`, `format`, and everything else [sharp reports](https://sharp.pixelplumbing.com/api-input#metadata) (`channels`, `hasAlpha`, `space`, …). An array when there are several images. Narrow it to specific keys with `as=metadata:width;height`.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import photo from './photo.jpg?w=800&format=webp&as=metadata';
|
|
190
|
+
// { src: "…", width: 800, height: 533, format: "webp", … }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**`as=next`** — what a plain Next.js image import gives you (`StaticImageData`): `src`, `width`, `height`, and a `blurDataURL` with its `blurWidth` / `blurHeight`, for the largest variant. It goes straight into `next/image`, blur placeholder included — see [Blur placeholders](#blur-placeholders).
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import photo from './photo.jpg?w=640;1280&format=webp&as=next';
|
|
197
|
+
// { src: "…", width: 1280, height: 853, blurDataURL: "data:image/webp;base64,…", blurWidth: 8, blurHeight: 5 }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Named imports.** When the result is a single object (`as=metadata`, `as=img`, `as=picture`, `as=next`), its keys are also exported by name:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { src, width, height } from './photo.jpg?w=800&format=webp&as=metadata&imagetools';
|
|
204
|
+
import { sources, img } from './photo.jpg?w=400;800&format=avif;webp&as=picture';
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## TypeScript
|
|
208
|
+
|
|
209
|
+
Reference the client types from any `.d.ts` file in your app:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// imagetools.d.ts
|
|
213
|
+
/// <reference types="next-imagetools/client" />
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
TypeScript recognizes an import by how its query ends. Queries ending in `as=srcset`, `as=img`, `as=picture` or `as=next` are typed as they are. The URL and metadata outputs give a single value or an array, depending on how many images the query makes, so end those with a **marker**:
|
|
217
|
+
|
|
218
|
+
- `&imagetools` — one image, a single value
|
|
219
|
+
- `&imagetools-gallery` — several images, an array
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import url from './photo.jpg?w=800&format=webp&imagetools'; // string
|
|
223
|
+
import urls from './photo.jpg?w=400;800&format=webp&imagetools-gallery'; // string[]
|
|
224
|
+
import meta from './photo.jpg?w=800&as=metadata&imagetools'; // ImagetoolsMetadata
|
|
225
|
+
import metas from './photo.jpg?w=400;800&as=metadata&imagetools-gallery'; // ImagetoolsMetadata[]
|
|
226
|
+
import srcset from './photo.jpg?w=400;800&format=webp&as=srcset'; // string, no marker needed
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**The build checks every marker.** One that doesn't match what the import gives — `&imagetools` on a query that makes two images — fails the build and says which marker to use, so a type can't be silently wrong.
|
|
230
|
+
|
|
231
|
+
Markers only select the type: a marked and an unmarked import of the same image share their output files. They are the markers commonly used with vite-imagetools.
|
|
232
|
+
|
|
233
|
+
Two cases need a declaration of your own, as for a [custom output format](#custom-transforms-and-output-formats):
|
|
234
|
+
|
|
235
|
+
- `as=metadata:<keys>`, whose keys vary.
|
|
236
|
+
- A preset that sets `as`, since the query only shows the preset's name:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// imagetools.d.ts
|
|
240
|
+
declare module '*preset=hero' {
|
|
241
|
+
const hero: ImagetoolsPicture;
|
|
242
|
+
export default hero;
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Recipes
|
|
247
|
+
|
|
248
|
+
### Blur placeholders
|
|
249
|
+
|
|
250
|
+
`as=next` returns the same shape as a plain Next.js image import, so `placeholder="blur"` works as usual:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
import Image from 'next/image';
|
|
254
|
+
|
|
255
|
+
import photo from './photo.jpg?w=640;1280&format=webp&as=next';
|
|
256
|
+
|
|
257
|
+
<Image src={photo} placeholder="blur" sizes="(min-width: 768px) 50vw, 100vw" alt="" />;
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The `blurDataURL` is made the way Next.js makes its own: the image shrunk to 8 pixels on its longer side, inlined as a data URL, and blurred by `next/image` while the real image loads.
|
|
261
|
+
|
|
262
|
+
For other outputs, add the **`placeholder`** directive to put a placeholder on the result — one import for the image and its placeholder:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import photo from './photo.jpg?w=400;800&format=avif;webp;jpg&placeholder&as=picture';
|
|
266
|
+
|
|
267
|
+
photo.placeholder; // "data:image/webp;base64,…" — 8px on its longer side; blur it with CSS
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import avatar from './me.jpg?w=96&h=96&fit=cover&format=webp&placeholder=color&as=img';
|
|
272
|
+
|
|
273
|
+
avatar.placeholder; // "#48a898", the dominant color
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- `placeholder` / `placeholder=blur` — a tiny image as a data URL
|
|
277
|
+
- `placeholder=color` — the dominant color, `#rrggbb`
|
|
278
|
+
|
|
279
|
+
Placeholders are made from the processed image, so a cropped variant gets a placeholder of the same shape — no jump when the image arrives. `placeholder` works with the object outputs (`as=img`, `as=picture`, `as=metadata` for one image); on a URL or `srcset` string there is nowhere to put it, and the build says so.
|
|
280
|
+
|
|
281
|
+
### With `next/image`
|
|
282
|
+
|
|
283
|
+
There are two ways to combine them:
|
|
284
|
+
|
|
285
|
+
- **`as=next`**, as in [Blur placeholders](#blur-placeholders): `next/image` treats the result like any static import — it still serves sizes for each screen through its optimizer, and the blur placeholder works.
|
|
286
|
+
- **`unoptimized`**: the generated file is served as it is, with no `/_next/image` round trip at request time — and no image server needed at all:
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
import Image from 'next/image';
|
|
290
|
+
|
|
291
|
+
import photo from './photo.jpg?w=1200&format=webp&as=metadata&imagetools';
|
|
292
|
+
|
|
293
|
+
<Image src={photo.src} width={photo.width} height={photo.height} alt="" unoptimized />;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Static export
|
|
297
|
+
|
|
298
|
+
With `output: 'export'`, Next.js refuses to build while its image optimizer is enabled — even though nothing needs it. Turn it off:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
export default withImagetools({
|
|
302
|
+
output: 'export',
|
|
303
|
+
images: { unoptimized: true },
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The exported site needs no Next.js server; any static host or CDN serves the images.
|
|
308
|
+
|
|
309
|
+
### Images that render only on the client
|
|
310
|
+
|
|
311
|
+
Nothing special is needed. Images are generated because they are imported, not because they were rendered during the build, so this works in `next dev`, `next build` and static export alike:
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
'use client';
|
|
315
|
+
import { useState } from 'react';
|
|
316
|
+
|
|
317
|
+
import zoomed from './photo.jpg?w=2000&format=webp&imagetools';
|
|
318
|
+
|
|
319
|
+
export function Zoom() {
|
|
320
|
+
const [open, setOpen] = useState(false);
|
|
321
|
+
return open ? <img src={zoomed} alt="" /> : <button onClick={() => setOpen(true)}>Zoom</button>;
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Deploying
|
|
326
|
+
|
|
327
|
+
Generated images are emitted with the rest of Next.js' static files, so they follow your deployment settings without extra configuration:
|
|
328
|
+
|
|
329
|
+
- **`basePath`** — URLs include it: `/docs/_next/static/media/…`.
|
|
330
|
+
- **`assetPrefix`** — URLs carry the prefix, like Next.js' own scripts, so a CDN in front of `/_next/static` serves the images too.
|
|
331
|
+
- **`output: 'standalone'`** — the images live in `.next/static`. Copy it into the standalone output as [Next.js documents](https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files); nothing else is needed.
|
|
332
|
+
- **`output: 'export'`** — see [Static export](#static-export).
|
|
333
|
+
|
|
334
|
+
URLs are percent-encoded, so source files with spaces or non-ASCII characters in their names work in `src` and `srcset` alike. (A `#` in a file name can't be imported with Turbopack at all — it starts a URL fragment.)
|
|
335
|
+
|
|
336
|
+
## Configuration
|
|
337
|
+
|
|
338
|
+
Pass options as the second argument:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
export default withImagetools(nextConfig, {
|
|
342
|
+
cache: { dir: 'node_modules/.cache/next-imagetools', retention: 60 * 60 * 24 * 30 },
|
|
343
|
+
removeMetadata: true,
|
|
344
|
+
namedExports: true,
|
|
345
|
+
extensions: ['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'tiff', 'heif'],
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- **`cache`** — the on-disk cache of processed images:
|
|
350
|
+
- `dir` — where, relative to your Next.js app. Default: `node_modules/.cache/next-imagetools`. Persist it in CI to speed up builds.
|
|
351
|
+
- `retention` — remove entries that haven't been used for this many seconds. Default: keep everything.
|
|
352
|
+
- `enabled` — set to `false` to process every image on every build. Default: `true`.
|
|
353
|
+
- **`removeMetadata`** — strip EXIF, ICC, XMP and other private metadata from generated images. Default: `true`.
|
|
354
|
+
- **`namedExports`** — also export the keys of object results by name, as in `import { width } from './a.jpg?as=metadata&imagetools'`. Default: `true`.
|
|
355
|
+
- **`extensions`** — source file extensions handled when imported with a query. Default: `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `tiff`, `heif`.
|
|
356
|
+
- **`include`** — globs; only imports whose path matches one of them are handled. Default: all.
|
|
357
|
+
- **`exclude`** — globs; imports whose path matches one of them are never handled.
|
|
358
|
+
|
|
359
|
+
`include` and `exclude` match paths relative to your Next.js app (where `next.config.ts` is), also in a monorepo — `exclude: ['src/legacy/**']`. An import they rule out is left to Next.js exactly as if it had no query.
|
|
360
|
+
|
|
361
|
+
Options must be JSON-serializable: Turbopack runs loaders in separate worker processes and passes their options as data.
|
|
362
|
+
|
|
363
|
+
### Presets and defaults
|
|
364
|
+
|
|
365
|
+
Give a set of directives a name, and use it with `preset=<name>`:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
export default withImagetools(nextConfig, {
|
|
369
|
+
presets: {
|
|
370
|
+
hero: 'w=640;1280;1920&format=avif;webp&as=picture',
|
|
371
|
+
thumb: { w: '160', h: '160', fit: 'cover', format: 'webp' },
|
|
372
|
+
},
|
|
373
|
+
defaultDirectives: 'quality=75',
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import hero from './banner.jpg?preset=hero';
|
|
379
|
+
import avatar from './me.jpg?preset=thumb&imagetools';
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
- **`presets`** — named directive sets, as a query string or an object. An unknown preset name fails the build.
|
|
383
|
+
- **`defaultDirectives`** — applied to every import that has a query (imports without one are left to Next.js).
|
|
384
|
+
|
|
385
|
+
The directives of an import are layered: `defaultDirectives` first, then the preset, then the import's own query — so `?preset=thumb&w=200` uses the preset with a different width. The import still shows which preset it uses, so its transforms stay traceable from the code.
|
|
386
|
+
|
|
387
|
+
Prefer single values in `defaultDirectives` (`quality`, `effort`, a single `format`). A list such as `format=avif;webp` multiplies the images of _every_ import, turning single values into arrays; the build warns when the config loads.
|
|
388
|
+
|
|
389
|
+
### Custom transforms and output formats
|
|
390
|
+
|
|
391
|
+
Turbopack passes loader options to worker processes as JSON, so options that take functions live in a module of their own. Point `configFile` at it (relative to your Next.js app):
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
// next.config.ts
|
|
395
|
+
export default withImagetools(nextConfig, { configFile: './imagetools.config.mjs' });
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
// imagetools.config.mjs
|
|
400
|
+
import { defineConfig } from 'next-imagetools/config';
|
|
401
|
+
|
|
402
|
+
export default defineConfig({
|
|
403
|
+
// add an output format: import sizes from './a.jpg?w=400;800&as=sizes'
|
|
404
|
+
extendOutputFormats: builtins => ({
|
|
405
|
+
...builtins,
|
|
406
|
+
sizes: () => images => images.map(image => ({ src: image.src, width: image.info.width })),
|
|
407
|
+
}),
|
|
408
|
+
|
|
409
|
+
// add a transform, applied when its directive is present: ./a.jpg?w=400&sepia
|
|
410
|
+
extendTransforms: builtins => [
|
|
411
|
+
...builtins,
|
|
412
|
+
config =>
|
|
413
|
+
'sepia' in config
|
|
414
|
+
? (_, image) =>
|
|
415
|
+
image.recomb([
|
|
416
|
+
[0.393, 0.769, 0.189],
|
|
417
|
+
[0.349, 0.686, 0.168],
|
|
418
|
+
[0.272, 0.534, 0.131],
|
|
419
|
+
])
|
|
420
|
+
: undefined,
|
|
421
|
+
],
|
|
422
|
+
|
|
423
|
+
// directives that depend on the image itself
|
|
424
|
+
defaultDirectives: async (url, metadata) =>
|
|
425
|
+
new URLSearchParams((await metadata()).hasAlpha ? 'format=webp' : 'format=avif'),
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- **`extendOutputFormats`** — receives the built-in output formats, returns the ones to use. A format's result is serialized into the importing module, so it must be JSON-serializable.
|
|
430
|
+
- **`extendTransforms`** — receives the built-in transform factories, returns the ones to use.
|
|
431
|
+
- **`resolveConfigs`** — replaces how directives expand into the list of images to generate.
|
|
432
|
+
- **`defaultDirectives`** — directives computed per image; `metadata()` reads the source's [sharp metadata](https://sharp.pixelplumbing.com/api-input#metadata). They sit between the `defaultDirectives` option and the preset.
|
|
433
|
+
|
|
434
|
+
Editing the config file takes effect immediately, also in a running `next dev`: it is tracked as a dependency of every import that uses it, and its content is part of every cache key. Only the config file itself is tracked, though: if it imports other local modules, an edit to one of those isn't seen — keep the code in the config file, or after such an edit, restart `next dev` and delete `node_modules/.cache/next-imagetools`. The imagetools-core API it builds on is re-exported from `next-imagetools/core`. A `.ts` config file needs a Node.js version that runs TypeScript natively (22.18 or newer); otherwise use `.mjs` or `.js`.
|
|
435
|
+
|
|
436
|
+
Types for a custom output format are up to you, as with any `as=` value TypeScript can't know about:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// imagetools.d.ts
|
|
440
|
+
declare module '*as=sizes' {
|
|
441
|
+
const sizes: { src: string; width: number }[];
|
|
442
|
+
export default sizes;
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Migrating from vite-imagetools
|
|
447
|
+
|
|
448
|
+
Directives, output formats and the `&imagetools` type markers are the same, so most imports move over unchanged. Replace the Vite plugin with `withImagetools` in `next.config.ts`, and your hand-written module declarations with the [client types](#typescript). The build now checks your markers, so a wrong one shows up as a build error.
|
|
449
|
+
|
|
450
|
+
One deliberate difference: when values clamp to the source size and become identical, vite-imagetools merges them, so `w=1600;3000` on a 1600px source gives a single URL instead of an array. Here the array keeps both entries, so the shape of an import [follows from its query](#multiple-variants).
|
|
451
|
+
|
|
452
|
+
Options that take functions (`extendTransforms`, `extendOutputFormats`, `resolveConfigs`, and `defaultDirectives` as a function) move to a [config file](#custom-transforms-and-output-formats), since `next.config` can only pass data to the loaders. The `defaultDirectives` option itself takes a query string or an object. A custom directive such as `?hero` still works from the config file's `defaultDirectives`, but a [preset](#presets-and-defaults) (`?preset=hero`) needs no code.
|
|
453
|
+
|
|
454
|
+
## How it works
|
|
455
|
+
|
|
456
|
+
Turbopack runs webpack-style loaders, but its loader API has no `emitFile`, and files written during a build are invisible to its resolver. So the loader never writes output files itself:
|
|
457
|
+
|
|
458
|
+
1. An import with a query, like `./photo.jpg?w=400;800&format=webp`, runs the **module loader**. It resolves the directives with imagetools-core, processes each variant with sharp (caching the result on disk), and returns a small JavaScript module.
|
|
459
|
+
2. That module imports each variant back from the **source file**, with an internal query that identifies it.
|
|
460
|
+
3. A second rule matches that internal query and marks the import as an **asset**. Its loader returns the encoded image, and Turbopack's own asset pipeline hashes the file, emits it, and gives the module its URL.
|
|
461
|
+
|
|
462
|
+
Because the generated images are ordinary Turbopack assets, they behave the same in every rendering mode and deployment target.
|
|
463
|
+
|
|
464
|
+
Generated files are named after their source: `hero.<hash>.jpg` when the format stays the same, and `hero.jpg.<hash>.webp` when it changes. Turbopack can only rename a module by adding to its file name, so a converted image keeps its original extension in the name.
|
|
465
|
+
|
|
466
|
+
## Troubleshooting
|
|
467
|
+
|
|
468
|
+
**An image looks outdated, or a change doesn't show up.** Delete `.next` and `node_modules/.cache/next-imagetools` and rebuild. If that fixes it, please [open an issue](https://github.com/awdr74100/next-imagetools/issues/new/choose) — caches are supposed to invalidate on their own.
|
|
469
|
+
|
|
470
|
+
**`Cannot find module './photo.jpg?…'` in TypeScript.** Reference the client types, and end the query in a marker or an `as=` that types on its own. See [TypeScript](#typescript).
|
|
471
|
+
|
|
472
|
+
**The build runs out of memory.** sharp encodes on Node's libuv thread pool — 4 threads per process by default — and Turbopack runs several loader worker processes, which all inherit the pool size. A smaller pool lowers peak memory for some speed:
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
UV_THREADPOOL_SIZE=2 next build
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Measured on 12 variants of a 24-megapixel JPEG in one worker: peak memory went from 549 MB to 405 MB, time from 8.7 s to 9.6 s.
|
|
479
|
+
|
|
480
|
+
## Contributing
|
|
481
|
+
|
|
482
|
+
Bug reports and pull requests are welcome. See [CONTRIBUTING.md](https://github.com/awdr74100/next-imagetools/blob/main/CONTRIBUTING.md) for the development setup and the test suites, and the [Code of Conduct](https://github.com/awdr74100/next-imagetools/blob/main/CODE_OF_CONDUCT.md).
|
|
483
|
+
|
|
484
|
+
## Acknowledgements
|
|
485
|
+
|
|
486
|
+
The directive syntax, transforms and output formats come from [imagetools](https://github.com/JonasKruckenberg/imagetools) by Jonas Kruckenberg. This package bridges them to Next.js.
|
|
487
|
+
|
|
488
|
+
## License
|
|
489
|
+
|
|
490
|
+
[MIT](https://github.com/awdr74100/next-imagetools/blob/main/LICENSE) © Roya
|
package/client.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Ambient types for imagetools imports. Reference them from a .d.ts in your app:
|
|
2
|
+
//
|
|
3
|
+
// /// <reference types="next-imagetools/client" />
|
|
4
|
+
//
|
|
5
|
+
// TypeScript allows a single `*` per module pattern, so an import is typed by how its query ends.
|
|
6
|
+
// End the query on a marker to pick the shape; the markers are stripped before processing, so a
|
|
7
|
+
// marked and an unmarked import of the same image share one output file:
|
|
8
|
+
//
|
|
9
|
+
// import hero from './hero.jpg?w=800&format=webp&imagetools'; // string
|
|
10
|
+
// import thumbs from './hero.jpg?w=200;400&format=webp&imagetools-gallery'; // string[]
|
|
11
|
+
// import meta from './hero.jpg?w=800&as=metadata&imagetools'; // ImagetoolsMetadata
|
|
12
|
+
//
|
|
13
|
+
// The markers match the ones commonly used with vite-imagetools, so imports carry over unchanged.
|
|
14
|
+
// Object results (metadata, img, picture) are also exported by name:
|
|
15
|
+
//
|
|
16
|
+
// import { src, width, height } from './hero.jpg?w=800&as=metadata&imagetools';
|
|
17
|
+
//
|
|
18
|
+
// `as=srcset`, `as=img`, `as=picture` and `as=next` always produce a single value, so those type
|
|
19
|
+
// without a marker as long as the `as=` directive comes last. The build checks every marker
|
|
20
|
+
// against what the import gives, so a wrong one fails it rather than giving a wrong type.
|
|
21
|
+
|
|
22
|
+
// Global, so an app can name these types without depending on imagetools-core itself.
|
|
23
|
+
type ImagetoolsImg = import('imagetools-core').Img;
|
|
24
|
+
type ImagetoolsPicture = import('imagetools-core').Picture;
|
|
25
|
+
|
|
26
|
+
/** Flat metadata of one generated image: sharp's metadata, with the output's size and format. */
|
|
27
|
+
interface ImagetoolsMetadata extends Omit<import('sharp').Metadata, 'width' | 'height' | 'format'> {
|
|
28
|
+
/** URL of the generated image. */
|
|
29
|
+
src: string;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
format: string;
|
|
33
|
+
/** With the `placeholder` directive: a blurred data URL, or `#rrggbb` for `placeholder=color`. */
|
|
34
|
+
placeholder?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `as=next`: structurally Next.js' `StaticImageData`, so it goes straight into next/image,
|
|
39
|
+
* `placeholder="blur"` included.
|
|
40
|
+
*/
|
|
41
|
+
interface ImagetoolsStaticImage {
|
|
42
|
+
src: string;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
blurDataURL: string;
|
|
46
|
+
blurWidth: number;
|
|
47
|
+
blurHeight: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare module '*as=next' {
|
|
51
|
+
const image: ImagetoolsStaticImage;
|
|
52
|
+
export default image;
|
|
53
|
+
export const src: ImagetoolsStaticImage['src'];
|
|
54
|
+
export const width: ImagetoolsStaticImage['width'];
|
|
55
|
+
export const height: ImagetoolsStaticImage['height'];
|
|
56
|
+
export const blurDataURL: ImagetoolsStaticImage['blurDataURL'];
|
|
57
|
+
export const blurWidth: ImagetoolsStaticImage['blurWidth'];
|
|
58
|
+
export const blurHeight: ImagetoolsStaticImage['blurHeight'];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare module '*as=metadata&imagetools' {
|
|
62
|
+
const metadata: ImagetoolsMetadata;
|
|
63
|
+
export default metadata;
|
|
64
|
+
export const src: ImagetoolsMetadata['src'];
|
|
65
|
+
export const width: ImagetoolsMetadata['width'];
|
|
66
|
+
export const height: ImagetoolsMetadata['height'];
|
|
67
|
+
export const format: ImagetoolsMetadata['format'];
|
|
68
|
+
export const size: ImagetoolsMetadata['size'];
|
|
69
|
+
export const space: ImagetoolsMetadata['space'];
|
|
70
|
+
export const channels: ImagetoolsMetadata['channels'];
|
|
71
|
+
export const depth: ImagetoolsMetadata['depth'];
|
|
72
|
+
export const density: ImagetoolsMetadata['density'];
|
|
73
|
+
export const isProgressive: ImagetoolsMetadata['isProgressive'];
|
|
74
|
+
export const hasProfile: ImagetoolsMetadata['hasProfile'];
|
|
75
|
+
export const hasAlpha: ImagetoolsMetadata['hasAlpha'];
|
|
76
|
+
export const orientation: ImagetoolsMetadata['orientation'];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare module '*as=metadata&imagetools-gallery' {
|
|
80
|
+
const metadata: ImagetoolsMetadata[];
|
|
81
|
+
export default metadata;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
declare module '*as=meta&imagetools' {
|
|
85
|
+
const metadata: ImagetoolsMetadata;
|
|
86
|
+
export default metadata;
|
|
87
|
+
export const src: ImagetoolsMetadata['src'];
|
|
88
|
+
export const width: ImagetoolsMetadata['width'];
|
|
89
|
+
export const height: ImagetoolsMetadata['height'];
|
|
90
|
+
export const format: ImagetoolsMetadata['format'];
|
|
91
|
+
export const size: ImagetoolsMetadata['size'];
|
|
92
|
+
export const space: ImagetoolsMetadata['space'];
|
|
93
|
+
export const channels: ImagetoolsMetadata['channels'];
|
|
94
|
+
export const depth: ImagetoolsMetadata['depth'];
|
|
95
|
+
export const density: ImagetoolsMetadata['density'];
|
|
96
|
+
export const isProgressive: ImagetoolsMetadata['isProgressive'];
|
|
97
|
+
export const hasProfile: ImagetoolsMetadata['hasProfile'];
|
|
98
|
+
export const hasAlpha: ImagetoolsMetadata['hasAlpha'];
|
|
99
|
+
export const orientation: ImagetoolsMetadata['orientation'];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
declare module '*as=meta&imagetools-gallery' {
|
|
103
|
+
const metadata: ImagetoolsMetadata[];
|
|
104
|
+
export default metadata;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare module '*&imagetools' {
|
|
108
|
+
const url: string;
|
|
109
|
+
export default url;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
declare module '*&imagetools-gallery' {
|
|
113
|
+
const urls: string[];
|
|
114
|
+
export default urls;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
declare module '*as=srcset' {
|
|
118
|
+
const srcset: string;
|
|
119
|
+
export default srcset;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
declare module '*as=img' {
|
|
123
|
+
const img: import('imagetools-core').Img & { placeholder?: string };
|
|
124
|
+
export default img;
|
|
125
|
+
export const src: import('imagetools-core').Img['src'];
|
|
126
|
+
export const w: import('imagetools-core').Img['w'];
|
|
127
|
+
export const h: import('imagetools-core').Img['h'];
|
|
128
|
+
export const srcset: import('imagetools-core').Img['srcset'];
|
|
129
|
+
export const placeholder: string | undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
declare module '*as=picture' {
|
|
133
|
+
const picture: import('imagetools-core').Picture & { placeholder?: string };
|
|
134
|
+
export default picture;
|
|
135
|
+
export const sources: import('imagetools-core').Picture['sources'];
|
|
136
|
+
export const placeholder: string | undefined;
|
|
137
|
+
export const img: import('imagetools-core').Picture['img'];
|
|
138
|
+
}
|