@ultimat3/storage 8.0.0 → 10.0.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/CLAUDE.md +26 -1
- package/README.md +16 -1
- package/package.json +2 -2
- package/src/driver-local.ts +40 -5
- package/src/errors.ts +1 -2
- package/src/image.ts +49 -7
- package/src/index.ts +6 -2
package/CLAUDE.md
CHANGED
|
@@ -32,6 +32,7 @@ Tier 1. Object storage: named disks, safe keys, signed URLs, sniffed uploads.
|
|
|
32
32
|
| `signed-url.ts` | HMAC over the constraint tuple, constant-time verify |
|
|
33
33
|
| `upload.ts` | magic-byte sniff + size/allowlist/checksum policy |
|
|
34
34
|
| `image.ts` | deterministic variant keys; byte path over core's pipeline (png/jpeg encode only) |
|
|
35
|
+
| | `VARIANT_FORMATS` — what a variant KEY can carry — and NOT `IMAGE_FORMATS`, which is core's and means what core can PROBE. See below |
|
|
35
36
|
| | `variantKey` is the cache identity `@ultimat3/cli`'s `/media/*` route looks a variant up by — derived, never stored, so a request that misses transforms once and every later one is a disk read |
|
|
36
37
|
| `storage.ts` | `defineStorage` + module-level `storage()` / `disk()` |
|
|
37
38
|
| `grant.ts` | `grantUpload` — the ONE way a presigned PUT is minted; the client never names a key |
|
|
@@ -153,6 +154,16 @@ Gotchas:
|
|
|
153
154
|
may be `NODE_ENV`'s, never a variable the process did not set. `usesDevStorageSecret()` is the
|
|
154
155
|
`x doctor` predicate, mirroring core's `usesDevCursorSecret()`; it reads the env var, so a disk
|
|
155
156
|
handed an explicit `signingSecret` is outside its question.
|
|
157
|
+
- **`localDriver({ env })` and `usesDevStorageSecret({ env })` are ONE question about ONE table**
|
|
158
|
+
(`As of 2026-08-23`). The predicate learned core's `env` slot first, so `dev-runtime.ts`'s guard
|
|
159
|
+
— `!isLocal({ env }) && usesDevStorageSecret({ env })` — asked about the BOOT while the
|
|
160
|
+
constructor it guards still read `process.env` for the secret, for `isLocal()` and for the
|
|
161
|
+
environment its refusal names. An embedding caller whose env is not the process's (`serveApp({ env })`,
|
|
162
|
+
a test fixture) got the verdict from one table and the behaviour from another, in the dangerous
|
|
163
|
+
direction: a production boot with no secret, launched from a development shell that has one,
|
|
164
|
+
signing every grant with the published literal. All three reads now come off `options.env ??
|
|
165
|
+
process.env`, so a bare `localDriver({ root })` is unchanged and additive. `driver-local.test.ts`
|
|
166
|
+
pins it by mutation — reverting any one read to `process.env` fails.
|
|
156
167
|
- **The mounted read half is `@ultimat3/cli`'s `dev-storage.ts`, not this package.** `GET
|
|
157
168
|
/_storage/:disk/*key` gates on `@ultimat3/policy`'s `evaluate()` (`storage:read`), which is tier
|
|
158
169
|
2 and unreachable from here — so a "serve this object" helper in this package could only ever be
|
|
@@ -169,7 +180,21 @@ Gotchas:
|
|
|
169
180
|
in `beforeEach`.
|
|
170
181
|
- `image.ts` owns no pixels: core's `transformImageBytes`/`blurDataUrl` are the only scaler.
|
|
171
182
|
Its image failures (`X_IMAGE_UNSUPPORTED`, `X_IMAGE_DECODE_FAILED`) surface unwrapped —
|
|
172
|
-
wrapping them in a `StorageError` would give one failure two codes.
|
|
183
|
+
wrapping them in a `StorageError` would give one failure two codes. `variantKey` now RAISES
|
|
184
|
+
core's `imageUnsupported()` as well as passing them through, for that same reason.
|
|
185
|
+
- **`VARIANT_FORMATS` is this package's format vocabulary, and `IMAGE_FORMATS` is core's.** Until
|
|
186
|
+
9.0.0 both packages exported `IMAGE_FORMATS` **and** `ImageFormat` from their own barrels over
|
|
187
|
+
different sets (core: `png|jpeg|webp|avif|gif|svg`, what it can PROBE; storage: `avif|webp|jpeg|png`),
|
|
188
|
+
so a caller narrowing on storage's held a type saying `gif` and `svg` could not occur and a
|
|
189
|
+
`probeImage()` value that was one — and `variantKey('photos/hero.gif', { format })` minted
|
|
190
|
+
`photos/hero@full.undefined`, a well-formed writable key naming a file nothing can serve. The set
|
|
191
|
+
is now a strict subset **by the compiler**: `as const satisfies readonly ImageFormat[]`, so a
|
|
192
|
+
member core cannot name is a build error here. `isVariantFormat` takes `string` on purpose, so
|
|
193
|
+
`probeImage(bytes).format` narrows through it with no cast. `avif` is in the set and `gif`/`svg`
|
|
194
|
+
are not because the question is what a variant KEY can carry, never what core can transform —
|
|
195
|
+
core decodes `gif` perfectly well, and naming this set `TRANSFORMABLE_FORMATS` would have been
|
|
196
|
+
the same lie one rename later. `scripts/render-modes.ts` holds the `IMAGE_FORMATS` row (`by:
|
|
197
|
+
'name'`) and `image.test.ts` fails the day either core name reappears in `src/index.ts`.
|
|
173
198
|
- `transformImage()` must encode at exactly `fitDimensions()`'s size, and passes `format`
|
|
174
199
|
explicitly (`?? 'webp'`, which then rejects): bytes that disagree with the `variantKey`
|
|
175
200
|
extension, or with the `width`/`height` `@ultimat3/seo` inlined, are the layout shift the
|
package/README.md
CHANGED
|
@@ -113,6 +113,12 @@ any key with a `maxBytes` and `contentType` of their choosing, which `acceptSign
|
|
|
113
113
|
trusts over the app's own `uploadPolicy`. Setting `STORAGE_SIGNING_SECRET=$DEV_SIGNING_SECRET`, or
|
|
114
114
|
pasting the literal into `signingSecret`, is refused exactly as an unset variable is. `usesDevStorageSecret()` is the
|
|
115
115
|
`x doctor` probe for it, the twin of core's `usesDevCursorSecret()`.
|
|
116
|
+
|
|
117
|
+
Both read **one table**, and `env` is how a caller says which. `localDriver({ root, env })` reads
|
|
118
|
+
the secret, the environment test and the environment the refusal names off that table;
|
|
119
|
+
`usesDevStorageSecret({ env })` reads it off the same one. Both default to `process.env`, so a bare
|
|
120
|
+
call is unchanged — pass `env` wherever the boot's environment is not the process's (`serveApp({ env })`,
|
|
121
|
+
a test fixture), or the guard answers about one process and the disk signs according to another.
|
|
116
122
|
Verification is constant-time, checks the signature *before* the expiry (a forged URL never
|
|
117
123
|
learns it was merely late), takes a `Clock` so tests freeze time, and returns
|
|
118
124
|
`{ ok: false, reason }` rather than throwing — `malformed | unsafe-key | signature-mismatch |
|
|
@@ -235,7 +241,7 @@ Inside `pending/` deliberately: an upload nobody ever scanned is still an orphan
|
|
|
235
241
|
| `X_STORAGE_QUARANTINED` | `promoteAttachment` on a key nothing has released from `pending/quarantine/` |
|
|
236
242
|
| `X_NOT_IMPLEMENTED` | S3 user metadata / cache-control; `serverSideEncryption` on either driver |
|
|
237
243
|
| `X_ENV_MISSING` | core's: S3 credential env vars, or a `localDriver` built outside development where neither `signingSecret` nor `STORAGE_SIGNING_SECRET` holds a secret other than the published `DEV_SIGNING_SECRET` |
|
|
238
|
-
| `X_IMAGE_UNSUPPORTED` | core's: an `avif` encode,
|
|
244
|
+
| `X_IMAGE_UNSUPPORTED` | core's: an `avif` encode, a source no built-in decoder reads, or a `variantKey` format no variant can carry |
|
|
239
245
|
| `X_IMAGE_DECODE_FAILED` | core's: truncated or corrupt image bytes |
|
|
240
246
|
|
|
241
247
|
## Images
|
|
@@ -243,6 +249,15 @@ Inside `pending/` deliberately: an upload nobody ever scanned is still an orphan
|
|
|
243
249
|
`variantKey()`, `srcsetDescriptors()`, `fitDimensions()` are pure — `@ultimat3/seo` builds
|
|
244
250
|
`srcset` from them without decoding a byte.
|
|
245
251
|
|
|
252
|
+
**`VARIANT_FORMATS` / `VariantFormat` are what a variant KEY can carry, `As of 2026-08`** —
|
|
253
|
+
`avif`, `webp`, `jpeg`, `png` — and this package exports no `IMAGE_FORMATS` and no `ImageFormat`. Those two names are
|
|
254
|
+
`@ultimat3/core`'s, over the six formats it can PROBE (`png`, `jpeg`, `webp`, `avif`, `gif`,
|
|
255
|
+
`svg`). Until 9.0.0 both packages exported both names over different sets, so a caller narrowing on
|
|
256
|
+
storage's held a type saying `gif` and `svg` could not occur and a `probeImage()` value that was
|
|
257
|
+
one. `isVariantFormat(format)` takes a `string`, so a probed format narrows through it with no
|
|
258
|
+
cast, and `variantKey()` refuses anything else with core's `X_IMAGE_UNSUPPORTED` instead of minting
|
|
259
|
+
`photos/hero@full.undefined`.
|
|
260
|
+
|
|
246
261
|
`transformImage()` and `blurPlaceholder()` are real, over `@ultimat3/core`'s pipeline, which is
|
|
247
262
|
`Bun.Image` — no `sharp`, no dependency. **It encodes `png`, `jpeg` and `webp`** — so the default
|
|
248
263
|
format and the default `.webp` key extension finally agree, and a `srcset` entry can be served
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/storage",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Named disks over Bun.file and Bun.s3: safe keys, signed URLs, sniffed uploads",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
34
|
+
"@ultimat3/core": "10.0.0"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/driver-local.ts
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
// Content type, etag and user metadata live in a sidecar under `.meta/`: a POSIX file has
|
|
4
4
|
// nowhere to keep them, and `get` must round-trip exactly what `put` was handed.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type Clock,
|
|
8
|
+
isLocal,
|
|
9
|
+
type ResolveEnvironmentOptions,
|
|
10
|
+
resolveEnvironment,
|
|
11
|
+
stringField,
|
|
12
|
+
systemClock,
|
|
13
|
+
} from '@ultimat3/core';
|
|
7
14
|
import {
|
|
8
15
|
DEFAULT_CONTENT_TYPE,
|
|
9
16
|
etagOf,
|
|
@@ -52,9 +59,17 @@ export const STORAGE_SIGNING_SECRET_KEY = 'STORAGE_SIGNING_SECRET';
|
|
|
52
59
|
* Reads the environment, not a driver instance: this is the same question `x doctor` asks about
|
|
53
60
|
* the cursor secret, and a disk handed an explicit `signingSecret` in `app.config.ts` never
|
|
54
61
|
* consults the variable at all.
|
|
62
|
+
*
|
|
63
|
+
* `env` is core's own slot, so this half of the guard reads the SAME table its other half does:
|
|
64
|
+
* `dev-runtime.ts` asks `!isLocal({ env }) && usesDevStorageSecret({ env })`, and an embedding
|
|
65
|
+
* caller (`serveApp({ env })`, a test fixture) whose `env` is not `process.env` used to get one
|
|
66
|
+
* answer about the boot and one about the process — for the decision of whether a disk may be
|
|
67
|
+
* signed with the published development key. Defaulted to `process.env`, so a bare call is
|
|
68
|
+
* unchanged.
|
|
55
69
|
*/
|
|
56
|
-
export function usesDevStorageSecret(): boolean {
|
|
57
|
-
const
|
|
70
|
+
export function usesDevStorageSecret(options?: Pick<ResolveEnvironmentOptions, 'env'>): boolean {
|
|
71
|
+
const source = options?.env ?? (process.env as Record<string, string | undefined>);
|
|
72
|
+
const configured = source[STORAGE_SIGNING_SECRET_KEY];
|
|
58
73
|
return configured === undefined || configured === '' || configured === DEV_SIGNING_SECRET;
|
|
59
74
|
}
|
|
60
75
|
|
|
@@ -66,6 +81,21 @@ export interface LocalDriverOptions {
|
|
|
66
81
|
/** Route prefix the dev server serves signed URLs from. */
|
|
67
82
|
readonly baseUrl?: string | undefined;
|
|
68
83
|
readonly clock?: Clock | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The environment table this DISK belongs to — the boot's, which is not always the process's.
|
|
86
|
+
* Core's own slot (`ResolveEnvironmentOptions['env']`), narrowed to that one field because the
|
|
87
|
+
* `fallback` beside it is a question this constructor never asks.
|
|
88
|
+
*
|
|
89
|
+
* It exists because the guard and the thing it guards have to read one table. `x doctor` and
|
|
90
|
+
* `dev-runtime.ts` ask `!isLocal({ env }) && usesDevStorageSecret({ env })` about the boot; the
|
|
91
|
+
* constructor below is what actually decides whether this disk signs with the published
|
|
92
|
+
* development key, and while it read `process.env` an embedding caller (`serveApp({ env })`, a
|
|
93
|
+
* test fixture) got the verdict from one table and the behaviour from another — in the
|
|
94
|
+
* dangerous direction, a production boot signing with a key published in this repo.
|
|
95
|
+
*
|
|
96
|
+
* Defaults to `process.env`, so a bare `localDriver({ root })` is unchanged.
|
|
97
|
+
*/
|
|
98
|
+
readonly env?: ResolveEnvironmentOptions['env'];
|
|
69
99
|
/**
|
|
70
100
|
* Ceiling on ONE server-side `put()`, because `put()` buffers the whole body. Defaults to the
|
|
71
101
|
* upload policy's ceiling — the same number for the same fact. The dev disk enforces it for
|
|
@@ -143,12 +173,17 @@ export function localDriver(options: LocalDriverOptions): StorageDriver {
|
|
|
143
173
|
// the first upload.
|
|
144
174
|
// The published literal counts as no secret at all, whichever way it arrives: an env var or an
|
|
145
175
|
// `app.config.ts` that pasted it in signs exactly as weakly as the fallback does.
|
|
146
|
-
|
|
176
|
+
// One table for all three reads — the secret, the environment test and the environment the
|
|
177
|
+
// refusal names. Splitting them is how the guard and the disk came to answer about two
|
|
178
|
+
// different processes.
|
|
179
|
+
const env = options.env ?? (process.env as Record<string, string | undefined>);
|
|
180
|
+
const supplied = options.signingSecret ?? env[STORAGE_SIGNING_SECRET_KEY];
|
|
147
181
|
const configured =
|
|
148
182
|
supplied === undefined || supplied === '' || supplied === DEV_SIGNING_SECRET
|
|
149
183
|
? undefined
|
|
150
184
|
: supplied;
|
|
151
|
-
if (configured === undefined && !isLocal(
|
|
185
|
+
if (configured === undefined && !isLocal({ env }))
|
|
186
|
+
throw signingSecretMissing(resolveEnvironment({ env }));
|
|
152
187
|
const secret = configured ?? DEV_SIGNING_SECRET;
|
|
153
188
|
|
|
154
189
|
const filePath = (key: string): string => `${root}/${key}`;
|
package/src/errors.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// rejected upload must tell the caller which constraint fired and where that constraint is
|
|
3
3
|
// configured, or the caller retries the same bytes forever.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { registerErrorCodes, renderThrowable, UltimateError } from '@ultimat3/core';
|
|
6
6
|
|
|
7
7
|
/** Codes this package declares and owns. */
|
|
8
8
|
export const STORAGE_OWNED_ERROR_CODES = [
|
|
@@ -80,7 +80,6 @@ export class StorageError extends UltimateError {
|
|
|
80
80
|
code: init.code,
|
|
81
81
|
cause: init.cause,
|
|
82
82
|
fix: init.fix,
|
|
83
|
-
docs: errorDocsUrl(init.code),
|
|
84
83
|
meta: init.meta,
|
|
85
84
|
});
|
|
86
85
|
}
|
package/src/image.ts
CHANGED
|
@@ -3,11 +3,41 @@
|
|
|
3
3
|
// `<img srcset>` from `srcsetDescriptors()` without decoding a byte, and when it does need the
|
|
4
4
|
// bytes, `transformImage()` returns exactly the size `fitDimensions()` already promised.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
blurDataUrl,
|
|
8
|
+
type ImageFormat,
|
|
9
|
+
imageUnsupported,
|
|
10
|
+
probeImage,
|
|
11
|
+
transformImageBytes,
|
|
12
|
+
} from '@ultimat3/core';
|
|
7
13
|
import { assertSafeKey, keyExtname } from './path';
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The formats a stored VARIANT can be minted in — a strict subset of the ones `@ultimat3/core`
|
|
17
|
+
* can PROBE, and a different question from them. `satisfies readonly ImageFormat[]` is the whole
|
|
18
|
+
* derivation: a member core cannot name is a compile error here, so this can never become a
|
|
19
|
+
* second vocabulary the way it was one until 9.0.0 (both packages exported `IMAGE_FORMATS` and
|
|
20
|
+
* `ImageFormat`, over different sets, from their own barrels — so a caller narrowing on storage's
|
|
21
|
+
* held a type saying `gif` and `svg` could not occur and a `probeImage()` value that was one).
|
|
22
|
+
*
|
|
23
|
+
* NOT "the formats storage can transform": core decodes `gif` perfectly well. This set is what a
|
|
24
|
+
* variant KEY can carry, which is why `avif` is in it (key and `srcset` math only — asking for
|
|
25
|
+
* its bytes rejects with core's `X_IMAGE_UNSUPPORTED`) and `gif` and `svg` are not.
|
|
26
|
+
*/
|
|
27
|
+
export const VARIANT_FORMATS = [
|
|
28
|
+
'avif',
|
|
29
|
+
'webp',
|
|
30
|
+
'jpeg',
|
|
31
|
+
'png',
|
|
32
|
+
] as const satisfies readonly ImageFormat[];
|
|
33
|
+
export type VariantFormat = (typeof VARIANT_FORMATS)[number];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Takes `string`, so `probeImage(bytes).format` narrows through it without a cast. That is the
|
|
37
|
+
* whole point: the answer for `gif` is `false`, and it used to be unaskable.
|
|
38
|
+
*/
|
|
39
|
+
export const isVariantFormat = (format: string): format is VariantFormat =>
|
|
40
|
+
(VARIANT_FORMATS as readonly string[]).includes(format);
|
|
11
41
|
|
|
12
42
|
/** `cover` fills the box and crops the overflow; `contain` fits inside it, no crop. */
|
|
13
43
|
export type ImageFit = 'cover' | 'contain';
|
|
@@ -15,7 +45,7 @@ export type ImageFit = 'cover' | 'contain';
|
|
|
15
45
|
export interface ImageTransform {
|
|
16
46
|
readonly width?: number | undefined;
|
|
17
47
|
readonly height?: number | undefined;
|
|
18
|
-
readonly format?:
|
|
48
|
+
readonly format?: VariantFormat | undefined;
|
|
19
49
|
/** 1-100. Omitted means the format default (`DEFAULT_QUALITY`). */
|
|
20
50
|
readonly quality?: number | undefined;
|
|
21
51
|
readonly fit?: ImageFit | undefined;
|
|
@@ -29,7 +59,7 @@ export interface ImageSize {
|
|
|
29
59
|
export const DEFAULT_QUALITY = 80;
|
|
30
60
|
export const DEFAULT_SRCSET_WIDTHS = [320, 640, 960, 1280, 1920] as const;
|
|
31
61
|
|
|
32
|
-
const FORMAT_EXTENSIONS: Readonly<Record<
|
|
62
|
+
const FORMAT_EXTENSIONS: Readonly<Record<VariantFormat, string>> = {
|
|
33
63
|
avif: 'avif',
|
|
34
64
|
webp: 'webp',
|
|
35
65
|
jpeg: 'jpg',
|
|
@@ -43,7 +73,19 @@ const FORMAT_EXTENSIONS: Readonly<Record<ImageFormat, string>> = {
|
|
|
43
73
|
export function variantKey(sourceKey: string, transform: ImageTransform): string {
|
|
44
74
|
const safe = assertSafeKey(sourceKey);
|
|
45
75
|
const stem = safe.slice(0, safe.length - keyExtname(safe).length);
|
|
46
|
-
|
|
76
|
+
// `string`, not `VariantFormat`: a format off a query string or through an `as` reaches here as
|
|
77
|
+
// any string core can probe, and indexing FORMAT_EXTENSIONS with `gif` minted
|
|
78
|
+
// `photos/hero@full.undefined` — a well-formed, writable key naming a file nothing can serve.
|
|
79
|
+
// Core's code, not a storage one: one bad format is one failure, and seo and the dev asset
|
|
80
|
+
// route already report it as X_IMAGE_UNSUPPORTED.
|
|
81
|
+
const format: string = transform.format ?? 'webp';
|
|
82
|
+
if (!isVariantFormat(format)) {
|
|
83
|
+
throw imageUnsupported(
|
|
84
|
+
`"${format}" is not a format a stored variant can be minted in`,
|
|
85
|
+
`variantKey('${safe}', { format: 'webp' }) — one of ${VARIANT_FORMATS.join(', ')}; a ${format} source is served as-is, it has no variant`,
|
|
86
|
+
{ format, key: safe },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
47
89
|
const parts: string[] = [];
|
|
48
90
|
if (transform.width !== undefined) parts.push(`w${transform.width}`);
|
|
49
91
|
if (transform.height !== undefined) parts.push(`h${transform.height}`);
|
|
@@ -63,7 +105,7 @@ export interface SrcsetDescriptor {
|
|
|
63
105
|
|
|
64
106
|
export interface SrcsetOptions {
|
|
65
107
|
readonly widths?: readonly number[] | undefined;
|
|
66
|
-
readonly format?:
|
|
108
|
+
readonly format?: VariantFormat | undefined;
|
|
67
109
|
readonly quality?: number | undefined;
|
|
68
110
|
/** Intrinsic size of the source. Widths above it are dropped — upscaling is never useful. */
|
|
69
111
|
readonly intrinsic?: ImageSize | undefined;
|
package/src/index.ts
CHANGED
|
@@ -97,20 +97,24 @@ export type { GrantUploadInput, UploadGrant, UploadRequest } from './grant';
|
|
|
97
97
|
export { grantUpload } from './grant';
|
|
98
98
|
export type {
|
|
99
99
|
ImageFit,
|
|
100
|
-
ImageFormat,
|
|
101
100
|
ImageSize,
|
|
102
101
|
ImageTransform,
|
|
103
102
|
SrcsetDescriptor,
|
|
104
103
|
SrcsetOptions,
|
|
104
|
+
VariantFormat,
|
|
105
105
|
} from './image';
|
|
106
|
+
// No `ImageFormat` and no `IMAGE_FORMATS` here, deliberately: `@ultimat3/core` owns that name and
|
|
107
|
+
// that set (what it can PROBE), this package owns `VARIANT_FORMATS` (what a variant can be minted
|
|
108
|
+
// in), and `image.test.ts` fails the day either core name reappears in this file.
|
|
106
109
|
export {
|
|
107
110
|
blurPlaceholder,
|
|
108
111
|
DEFAULT_QUALITY,
|
|
109
112
|
DEFAULT_SRCSET_WIDTHS,
|
|
110
113
|
fitDimensions,
|
|
111
|
-
|
|
114
|
+
isVariantFormat,
|
|
112
115
|
srcsetDescriptors,
|
|
113
116
|
transformImage,
|
|
117
|
+
VARIANT_FORMATS,
|
|
114
118
|
variantKey,
|
|
115
119
|
} from './image';
|
|
116
120
|
|