toiljs 0.0.79 → 0.0.81
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/CHANGELOG.md +20 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/components/Image.d.ts +7 -1
- package/build/client/components/Image.js +20 -6
- package/build/client/index.d.ts +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +6 -2
- package/build/compiler/image-blur.d.ts +2 -0
- package/build/compiler/image-blur.js +24 -0
- package/build/compiler/vite.js +2 -0
- package/examples/basic/client/assets/cube.webp +0 -0
- package/examples/basic/client/routes/test.tsx +5 -0
- package/package.json +1 -1
- package/src/client/components/Image.tsx +67 -28
- package/src/client/index.ts +1 -1
- package/src/compiler/generate.ts +8 -2
- package/src/compiler/image-blur.ts +42 -0
- package/src/compiler/vite.ts +5 -0
- package/test/dom/Image.test.tsx +40 -4
- package/test/image-blur.test.ts +50 -0
|
@@ -5,21 +5,32 @@ import { type ComponentPropsWithRef, type CSSProperties, type ReactNode, useStat
|
|
|
5
5
|
* `src` and `alt` are required (`alt` is enforced for accessibility, pass `alt=""` for decorative
|
|
6
6
|
* images). `width`/`height` (or `fill`) reserve space to prevent layout shift.
|
|
7
7
|
*/
|
|
8
|
+
/** A toil image source carrying an auto-generated blur LQIP + intrinsic size, produced by importing an
|
|
9
|
+
* image with the `?toil` flag (`import hero from './hero.webp?toil'`). Pass it straight to `Image`. */
|
|
10
|
+
export interface ToilImageSource {
|
|
11
|
+
src: string;
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
blurDataURL?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export interface ImageProps extends Omit<
|
|
9
18
|
ComponentPropsWithRef<'img'>,
|
|
10
|
-
'loading' | 'placeholder' | 'width' | 'height'
|
|
19
|
+
'loading' | 'placeholder' | 'width' | 'height' | 'src'
|
|
11
20
|
> {
|
|
12
|
-
|
|
21
|
+
/** The image URL, or a `?toil` import object that auto-fills the blur placeholder + aspect-ratio. */
|
|
22
|
+
src: string | ToilImageSource;
|
|
13
23
|
alt: string;
|
|
14
24
|
/** Intrinsic width in px. Set together with `height` to reserve space (avoids layout shift). */
|
|
15
25
|
width?: number;
|
|
16
26
|
/** Intrinsic height in px. Set together with `width` to reserve space (avoids layout shift). */
|
|
17
27
|
height?: number;
|
|
18
28
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
29
|
+
* Make the image fill the width of a box toil wraps around it (a block-level `<span>`), scaling to
|
|
30
|
+
* its natural height — or, if you size the box (`width`/`height`, or `style` like
|
|
31
|
+
* `aspectRatio: '16/9'`), the image covers that box, cropped per `objectFit` (default `cover`).
|
|
32
|
+
* The image flows in-block (never absolutely positioned), so it can't escape to fill the page or
|
|
33
|
+
* collapse to nothing. `className`/`style` apply to the box, not the `<img>`.
|
|
23
34
|
*/
|
|
24
35
|
fill?: boolean;
|
|
25
36
|
/** `object-fit` for the rendered image (handy with `fill`). */
|
|
@@ -30,12 +41,17 @@ export interface ImageProps extends Omit<
|
|
|
30
41
|
*/
|
|
31
42
|
priority?: boolean;
|
|
32
43
|
/**
|
|
33
|
-
* Placeholder shown until the image loads: `'empty'` (default) or `'blur'`. `'blur'`
|
|
34
|
-
* `blurDataURL`
|
|
35
|
-
*
|
|
44
|
+
* Placeholder shown until the image loads: `'empty'` (default) or `'blur'`. `'blur'` paints the
|
|
45
|
+
* `blurDataURL` (auto-filled from a `?toil` import, or passed) blurred, or a neutral skeleton
|
|
46
|
+
* shimmer if there's none. It needs a reserved size (`width`+`height`, or `fill`) to paint into —
|
|
47
|
+
* which also prevents layout shift (the placeholder is cosmetic; the reserved size is what holds).
|
|
36
48
|
*/
|
|
37
49
|
placeholder?: 'empty' | 'blur';
|
|
38
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* A tiny base64 image shown blurred while the real image loads (with `placeholder="blur"`).
|
|
52
|
+
* Auto-generated when you import via `?toil` (`import hero from './hero.webp?toil'`); pass it
|
|
53
|
+
* explicitly only for a string `src`. Omit it to fall back to a neutral skeleton shimmer.
|
|
54
|
+
*/
|
|
39
55
|
blurDataURL?: string;
|
|
40
56
|
}
|
|
41
57
|
|
|
@@ -47,36 +63,58 @@ export interface ImageProps extends Omit<
|
|
|
47
63
|
*/
|
|
48
64
|
export function Image(props: ImageProps): ReactNode {
|
|
49
65
|
const {
|
|
50
|
-
src,
|
|
66
|
+
src: srcProp,
|
|
51
67
|
alt,
|
|
52
|
-
width,
|
|
53
|
-
height,
|
|
68
|
+
width: widthProp,
|
|
69
|
+
height: heightProp,
|
|
54
70
|
fill = false,
|
|
55
71
|
objectFit,
|
|
56
72
|
priority = false,
|
|
57
73
|
placeholder = 'empty',
|
|
58
|
-
blurDataURL,
|
|
74
|
+
blurDataURL: blurDataURLProp,
|
|
59
75
|
className: userClassName,
|
|
60
76
|
style,
|
|
61
77
|
onLoad,
|
|
62
78
|
...rest
|
|
63
79
|
} = props;
|
|
64
80
|
|
|
81
|
+
// `src` may be a plain URL or a `?toil` import object; unpack it and let explicit props win, so a
|
|
82
|
+
// `?toil` import auto-fills the blur placeholder + aspect-ratio with no extra props.
|
|
83
|
+
const source = typeof srcProp === 'string' ? null : srcProp;
|
|
84
|
+
const src = typeof srcProp === 'string' ? srcProp : srcProp.src;
|
|
85
|
+
const width = widthProp ?? source?.width;
|
|
86
|
+
const height = heightProp ?? source?.height;
|
|
87
|
+
const blurDataURL = blurDataURLProp ?? source?.blurDataURL;
|
|
88
|
+
|
|
65
89
|
const [loaded, setLoaded] = useState(false);
|
|
66
|
-
|
|
90
|
+
// The placeholder paints while the real image loads, then drops on load. With a `blurDataURL`
|
|
91
|
+
// (auto-filled by a `?toil` import, or passed) it's that tiny image blurred; otherwise it's a
|
|
92
|
+
// neutral skeleton shimmer — so `placeholder="blur"` is never a silent no-op. It only shows if
|
|
93
|
+
// there's a reserved box to paint into (give `width`+`height`, or use `fill` in a sized parent).
|
|
94
|
+
const showPlaceholder = placeholder === 'blur' && !loaded;
|
|
95
|
+
const placeholderClass = !showPlaceholder
|
|
96
|
+
? undefined
|
|
97
|
+
: blurDataURL !== undefined
|
|
98
|
+
? 'toil-img-blur'
|
|
99
|
+
: 'toil-img-skeleton';
|
|
100
|
+
// Reserve space so the image can't shift layout when it loads: an explicit aspect-ratio derived
|
|
101
|
+
// from width+height survives responsive `width:100%` CSS (bare width/height attributes do not).
|
|
102
|
+
const aspectRatio =
|
|
103
|
+
width !== undefined && height !== undefined ? `${width} / ${height}` : undefined;
|
|
67
104
|
|
|
68
|
-
// Layout +
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// wrapper box (below) instead, since that box is what the caller sizes.
|
|
105
|
+
// Layout + placeholder come from toil's shipped CSS classes (see `buildHtml`'s `<style id="toil-
|
|
106
|
+
// base">`) not inline styles, so they're SSR-safe AND overridable. Only per-instance bits stay
|
|
107
|
+
// inline: `objectFit`, the aspect-ratio, and the blur image URL. For a non-`fill` image the
|
|
108
|
+
// caller's `className`/`style` ride on the <img>; for `fill` they go on the wrapper box below.
|
|
73
109
|
const imgClass =
|
|
74
|
-
[fill ? 'toil-img-fill' : userClassName,
|
|
75
|
-
|
|
76
|
-
.join(' ') || undefined;
|
|
110
|
+
[fill ? 'toil-img-fill' : userClassName, placeholderClass].filter(Boolean).join(' ') ||
|
|
111
|
+
undefined;
|
|
77
112
|
const imgStyle: CSSProperties = {
|
|
78
113
|
...(objectFit !== undefined ? { objectFit } : {}),
|
|
79
|
-
...(
|
|
114
|
+
...(showPlaceholder && blurDataURL !== undefined
|
|
115
|
+
? { backgroundImage: `url(${blurDataURL})` }
|
|
116
|
+
: {}),
|
|
117
|
+
...(fill || aspectRatio === undefined ? {} : { aspectRatio }),
|
|
80
118
|
...(fill ? {} : style),
|
|
81
119
|
};
|
|
82
120
|
|
|
@@ -101,11 +139,12 @@ export function Image(props: ImageProps): ReactNode {
|
|
|
101
139
|
|
|
102
140
|
if (!fill) return img;
|
|
103
141
|
|
|
104
|
-
// `fill`: the image
|
|
105
|
-
// the
|
|
106
|
-
//
|
|
107
|
-
//
|
|
142
|
+
// `fill`: the image flows in-block at 100% of its box's width (scaling to natural height), or
|
|
143
|
+
// covers the box if the caller sizes it (`width`/`height`, or `style` like `aspectRatio`). It is
|
|
144
|
+
// NEVER absolutely positioned, so it can't escape to fill the page nor collapse to a zero-height
|
|
145
|
+
// box. We still wrap it so the caller's `width`/`height`/`style` size the box, not the raw `<img>`.
|
|
108
146
|
const boxStyle: CSSProperties = {
|
|
147
|
+
...(aspectRatio !== undefined ? { aspectRatio } : {}),
|
|
109
148
|
...(width !== undefined ? { width } : {}),
|
|
110
149
|
...(height !== undefined ? { height } : {}),
|
|
111
150
|
...style,
|
package/src/client/index.ts
CHANGED
|
@@ -96,7 +96,7 @@ export type {
|
|
|
96
96
|
export { usePageSearch } from './search/use-page-search.js';
|
|
97
97
|
export type { PageSearch } from './search/use-page-search.js';
|
|
98
98
|
export { Image } from './components/Image.js';
|
|
99
|
-
export type { ImageProps } from './components/Image.js';
|
|
99
|
+
export type { ImageProps, ToilImageSource } from './components/Image.js';
|
|
100
100
|
export { Script } from './components/Script.js';
|
|
101
101
|
export type { ScriptProps, ScriptStrategy } from './components/Script.js';
|
|
102
102
|
export { Form } from './components/Form.js';
|
package/src/compiler/generate.ts
CHANGED
|
@@ -36,6 +36,9 @@ const IMAGETOOLS_MODULES = [
|
|
|
36
36
|
`declare module '*as=srcset' {\n const src: string;\n export default src;\n}`,
|
|
37
37
|
`declare module '*as=url' {\n const src: string;\n export default src;\n}`,
|
|
38
38
|
`declare module '*as=metadata' {\n const metadata: { src: string; width?: number; height?: number; format?: string }[];\n export default metadata;\n}`,
|
|
39
|
+
// `import hero from './hero.webp?toil'` -> a Toil.Image source carrying an auto-generated blur LQIP
|
|
40
|
+
// and intrinsic size (see compiler/image-blur.ts). Pass it straight to `<Toil.Image src={hero} />`.
|
|
41
|
+
`declare module '*?toil' {\n const image: { src: string; width: number; height: number; blurDataURL: string };\n export default image;\n}`,
|
|
39
42
|
].join('\n');
|
|
40
43
|
|
|
41
44
|
export const TOIL_ENV_DTS =
|
|
@@ -409,9 +412,12 @@ const ENTRY_SCRIPT = `<script type="module" src="./entry.tsx"></script>`;
|
|
|
409
412
|
* SSR-safe (present before JS). The app can restyle `.toil-img-*` freely. */
|
|
410
413
|
const TOIL_BASE_STYLE =
|
|
411
414
|
`<style id="toil-base">` +
|
|
412
|
-
`.toil-img-fill-box{
|
|
413
|
-
`.toil-img-fill{
|
|
415
|
+
`.toil-img-fill-box{display:block;overflow:hidden}` +
|
|
416
|
+
`.toil-img-fill{display:block;width:100%;height:100%;object-fit:cover}` +
|
|
414
417
|
`.toil-img-blur{background-size:cover;background-position:center;filter:blur(20px)}` +
|
|
418
|
+
`.toil-img-skeleton{background:linear-gradient(90deg,#ececed 25%,#f4f4f5 37%,#ececed 63%);background-size:400% 100%;animation:toil-img-shimmer 1.4s ease infinite}` +
|
|
419
|
+
`@keyframes toil-img-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}` +
|
|
420
|
+
`@media (prefers-reduced-motion:reduce){.toil-img-skeleton{animation:none}}` +
|
|
415
421
|
`</style>`;
|
|
416
422
|
|
|
417
423
|
/**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
/** An image import carrying the `?toil` flag: `import hero from './hero.webp?toil'`. */
|
|
4
|
+
const TOIL_QUERY = /[?&]toil(?:&|$)/;
|
|
5
|
+
/** Raster formats sharp can downscale into an LQIP. (SVG/animated stay as-is — no blur.) */
|
|
6
|
+
const RASTER = /\.(?:png|jpe?g|webp|avif|gif|tiff?)(?:\?|$)/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Auto-generates a tiny blurred base64 LQIP (low-quality image placeholder) for raster images imported
|
|
10
|
+
* with the `?toil` flag, and returns a `{ src, width, height, blurDataURL }` object that `Toil.Image`
|
|
11
|
+
* consumes to auto-fill its `blurDataURL` (`placeholder="blur"`) and its aspect-ratio — the way Next.js
|
|
12
|
+
* bakes a `blurDataURL` for static image imports. Runs in dev and build (Vite `load`); `sharp` is loaded
|
|
13
|
+
* lazily so it only costs anything when a `?toil` import is actually resolved.
|
|
14
|
+
*
|
|
15
|
+
* Usage: `import hero from './hero.webp?toil'` then `<Toil.Image src={hero} placeholder="blur" />`.
|
|
16
|
+
* The bare `src` is re-imported with no query, so Vite/imagetools still optimize + hash the real asset
|
|
17
|
+
* (this plugin only adds the placeholder + intrinsic size); it skips itself on that bare import.
|
|
18
|
+
*/
|
|
19
|
+
export function imageBlurPlugin(): Plugin {
|
|
20
|
+
return {
|
|
21
|
+
name: 'toil:image-blur',
|
|
22
|
+
enforce: 'pre',
|
|
23
|
+
async load(id) {
|
|
24
|
+
if (!TOIL_QUERY.test(id) || !RASTER.test(id)) return undefined;
|
|
25
|
+
const file = id.replace(/\?.*$/, '');
|
|
26
|
+
const { default: sharp } = await import('sharp');
|
|
27
|
+
const meta = await sharp(file).metadata();
|
|
28
|
+
// ~24px longest edge, blurred, webp — a few hundred base64 bytes, inlined as a data URI.
|
|
29
|
+
const lqip = await sharp(file)
|
|
30
|
+
.resize(24, 24, { fit: 'inside' })
|
|
31
|
+
.blur()
|
|
32
|
+
.webp({ quality: 40 })
|
|
33
|
+
.toBuffer();
|
|
34
|
+
const blurDataURL = `data:image/webp;base64,${lqip.toString('base64')}`;
|
|
35
|
+
return (
|
|
36
|
+
`import src from ${JSON.stringify(file)};\n` +
|
|
37
|
+
`export default { src, width: ${meta.width ?? 0}, height: ${meta.height ?? 0}, ` +
|
|
38
|
+
`blurDataURL: ${JSON.stringify(blurDataURL)} };\n`
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/compiler/vite.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createLogger, type InlineConfig, type Logger, mergeConfig, type PluginO
|
|
|
12
12
|
import { type ResolvedToilConfig } from './config.js';
|
|
13
13
|
import { fontPreloadPlugin } from './fonts.js';
|
|
14
14
|
import { imageReportPlugin } from './image-report.js';
|
|
15
|
+
import { imageBlurPlugin } from './image-blur.js';
|
|
15
16
|
import { toilPlugin } from './plugin.js';
|
|
16
17
|
import { prerenderPlugin } from './prerender.js';
|
|
17
18
|
|
|
@@ -166,6 +167,10 @@ export async function createViteConfig(cfg: ResolvedToilConfig): Promise<InlineC
|
|
|
166
167
|
customLogger: brandedLogger(),
|
|
167
168
|
plugins: [
|
|
168
169
|
tailwind,
|
|
170
|
+
// Auto-generate a tiny blurred base64 LQIP for `?toil` image imports (consumed by
|
|
171
|
+
// `Toil.Image` placeholder="blur"). Runs before imagetools (enforce:'pre') so it owns the
|
|
172
|
+
// `?toil` query; the bare re-imported src still flows through imagetools below.
|
|
173
|
+
cfg.images ? imageBlurPlugin() : undefined,
|
|
169
174
|
// Build-time image resize/optimization. Every *imported* raster image is compressed to
|
|
170
175
|
// webp by default (so a plain `<img src={imported}>` is optimized too, not just
|
|
171
176
|
// `Toil.Image`); add `?w=400;800&format=…` to resize or pick a format. `public/` assets
|
package/test/dom/Image.test.tsx
CHANGED
|
@@ -23,8 +23,8 @@ describe('Image', () => {
|
|
|
23
23
|
expect(img.getAttribute('width')).toBe('200');
|
|
24
24
|
expect(img.getAttribute('height')).toBe('100');
|
|
25
25
|
expect(img.getAttribute('fetchpriority')).toBe('auto');
|
|
26
|
-
//
|
|
27
|
-
expect(img.
|
|
26
|
+
// width+height reserve space via an explicit aspect-ratio (survives responsive `width:100%`).
|
|
27
|
+
expect(img.style.aspectRatio).toBe('200 / 100');
|
|
28
28
|
expect(img.className).toBe('');
|
|
29
29
|
});
|
|
30
30
|
|
|
@@ -41,7 +41,7 @@ describe('Image', () => {
|
|
|
41
41
|
expect(img.getAttribute('fetchpriority')).toBe('high');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it('fill wraps the image in a
|
|
44
|
+
it('fill wraps the image in a box; the image lays out via a class, not inline styles', () => {
|
|
45
45
|
const { getByAltText } = render(
|
|
46
46
|
<Image
|
|
47
47
|
src="/bg.png"
|
|
@@ -58,7 +58,7 @@ describe('Image', () => {
|
|
|
58
58
|
expect(img.style.position).toBe('');
|
|
59
59
|
// objectFit is genuinely per-instance, so it stays inline.
|
|
60
60
|
expect(img.style.objectFit).toBe('cover');
|
|
61
|
-
// The image is wrapped in a
|
|
61
|
+
// The image is wrapped in a <span> box the caller sizes, so it fills the box, not the page.
|
|
62
62
|
const box = img.parentElement as HTMLElement;
|
|
63
63
|
expect(box.tagName).toBe('SPAN');
|
|
64
64
|
expect(box.classList.contains('toil-img-fill-box')).toBe(true);
|
|
@@ -105,4 +105,40 @@ describe('Image', () => {
|
|
|
105
105
|
expect(img.style.backgroundImage).toBe('');
|
|
106
106
|
expect(img.classList.contains('toil-img-blur')).toBe(false);
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
it('falls back to a skeleton shimmer when placeholder="blur" has no blurDataURL', () => {
|
|
110
|
+
const { getByAltText } = render(
|
|
111
|
+
<Image src="/s.png" alt="s" width={10} height={10} placeholder="blur" />,
|
|
112
|
+
);
|
|
113
|
+
const img = getByAltText('s') as HTMLImageElement;
|
|
114
|
+
// never a silent no-op: a neutral skeleton paints (not the blur class, no background image)
|
|
115
|
+
expect(img.classList.contains('toil-img-skeleton')).toBe(true);
|
|
116
|
+
expect(img.classList.contains('toil-img-blur')).toBe(false);
|
|
117
|
+
expect(img.style.backgroundImage).toBe('');
|
|
118
|
+
fireEvent.load(img);
|
|
119
|
+
expect(img.classList.contains('toil-img-skeleton')).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('reserves space with an aspect-ratio derived from width+height', () => {
|
|
123
|
+
const { getByAltText } = render(<Image src="/a.png" alt="a" width={400} height={300} />);
|
|
124
|
+
const img = getByAltText('a') as HTMLImageElement;
|
|
125
|
+
expect(img.style.aspectRatio).toBe('400 / 300');
|
|
126
|
+
expect(img.getAttribute('width')).toBe('400');
|
|
127
|
+
expect(img.getAttribute('height')).toBe('300');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reads src/dimensions/blur from a ?toil import object', () => {
|
|
131
|
+
const { getByAltText } = render(
|
|
132
|
+
<Image
|
|
133
|
+
src={{ src: '/hero.webp', width: 40, height: 30, blurDataURL: 'data:image/blur' }}
|
|
134
|
+
alt="h"
|
|
135
|
+
placeholder="blur"
|
|
136
|
+
/>,
|
|
137
|
+
);
|
|
138
|
+
const img = getByAltText('h') as HTMLImageElement;
|
|
139
|
+
expect(img.getAttribute('src')).toBe('/hero.webp');
|
|
140
|
+
expect(img.style.aspectRatio).toBe('40 / 30');
|
|
141
|
+
expect(img.classList.contains('toil-img-blur')).toBe(true);
|
|
142
|
+
expect(img.style.backgroundImage).toContain('data:image/blur');
|
|
143
|
+
});
|
|
108
144
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { imageBlurPlugin } from '../src/compiler/image-blur';
|
|
9
|
+
|
|
10
|
+
/** Call the plugin's `load` hook directly (it uses no Rollup `this` context). */
|
|
11
|
+
function loadHook(): (id: string) => Promise<string | undefined> {
|
|
12
|
+
const plugin = imageBlurPlugin();
|
|
13
|
+
return plugin.load as unknown as (id: string) => Promise<string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('imageBlurPlugin', () => {
|
|
17
|
+
it('emits a { src, width, height, blurDataURL } module for a ?toil image import', async () => {
|
|
18
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'toil-blur-'));
|
|
19
|
+
const file = path.join(dir, 'pic.png');
|
|
20
|
+
await sharp({
|
|
21
|
+
create: { width: 40, height: 30, channels: 3, background: { r: 200, g: 100, b: 50 } },
|
|
22
|
+
})
|
|
23
|
+
.png()
|
|
24
|
+
.toFile(file);
|
|
25
|
+
try {
|
|
26
|
+
const load = loadHook();
|
|
27
|
+
const out = await load(`${file}?toil`);
|
|
28
|
+
expect(typeof out).toBe('string');
|
|
29
|
+
// re-imports the bare asset for src (so Vite/imagetools still optimize + hash it)
|
|
30
|
+
expect(out).toContain(`import src from ${JSON.stringify(file)}`);
|
|
31
|
+
// carries the intrinsic size for the aspect-ratio
|
|
32
|
+
expect(out).toContain('width: 40');
|
|
33
|
+
expect(out).toContain('height: 30');
|
|
34
|
+
// a real inlined LQIP, not a placeholder string
|
|
35
|
+
expect(out).toContain('blurDataURL: "data:image/webp;base64,');
|
|
36
|
+
const b64 = /base64,([^"]+)"/.exec(out as string)?.[1] ?? '';
|
|
37
|
+
expect(b64.length).toBeGreaterThan(20);
|
|
38
|
+
} finally {
|
|
39
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('ignores imports without the ?toil flag', async () => {
|
|
44
|
+
const load = loadHook();
|
|
45
|
+
expect(await load('/some/pic.png')).toBeUndefined();
|
|
46
|
+
expect(await load('/some/pic.png?w=400')).toBeUndefined();
|
|
47
|
+
// non-raster ?toil (e.g. svg) is skipped too
|
|
48
|
+
expect(await load('/some/icon.svg?toil')).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|