sanity-plugin-image-resizer 1.0.2 → 1.0.4
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/dist/index.js +24 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +24 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/helpers.ts +56 -3
- package/src/tool/ImageResizer.tsx +5 -1
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function buildReplacementPatch(obj, oldId, newId, path = "") {
|
|
|
66
66
|
{}
|
|
67
67
|
);
|
|
68
68
|
const record = obj;
|
|
69
|
-
return record.asset?._ref === oldId ? { [path ? `${path}.asset` : "asset"]: { _type: "reference", _ref: newId } } : Object.keys(record).filter((k) => !k.startsWith("_")).reduce(
|
|
69
|
+
return record.asset?._ref === oldId ? { [path ? `${path}.asset` : "asset"]: { _type: "reference", _ref: newId } } : record._type === "reference" && record._ref === oldId ? { [path || "_ref"]: { _type: "reference", _ref: newId } } : Object.keys(record).filter((k) => !k.startsWith("_")).reduce(
|
|
70
70
|
(acc, key) => {
|
|
71
71
|
const childPath = path ? `${path}.${key}` : key;
|
|
72
72
|
return Object.assign(
|
|
@@ -77,6 +77,25 @@ function buildReplacementPatch(obj, oldId, newId, path = "") {
|
|
|
77
77
|
{}
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
|
+
const ASSET_METADATA_FIELDS = [
|
|
81
|
+
"title",
|
|
82
|
+
"description",
|
|
83
|
+
"altText",
|
|
84
|
+
"creditLine",
|
|
85
|
+
"source",
|
|
86
|
+
"opt"
|
|
87
|
+
];
|
|
88
|
+
async function copyAssetMetadata(client, oldAssetId, newAssetId) {
|
|
89
|
+
const projection = ASSET_METADATA_FIELDS.join(", "), oldMeta = await client.fetch(
|
|
90
|
+
`*[_id == $id][0]{ ${projection} }`,
|
|
91
|
+
{ id: oldAssetId }
|
|
92
|
+
);
|
|
93
|
+
if (!oldMeta) return;
|
|
94
|
+
const patch = {};
|
|
95
|
+
for (const field of ASSET_METADATA_FIELDS)
|
|
96
|
+
oldMeta[field] !== void 0 && oldMeta[field] !== null && (patch[field] = oldMeta[field]);
|
|
97
|
+
Object.keys(patch).length > 0 && await client.patch(newAssetId).set(patch).commit();
|
|
98
|
+
}
|
|
80
99
|
const validateImageSize = async (value, context) => {
|
|
81
100
|
if (!value?.asset?._ref) return !0;
|
|
82
101
|
const asset = await context.getClient({ apiVersion: "2025-02-19" }).fetch(
|
|
@@ -247,7 +266,7 @@ function ImageResizerView() {
|
|
|
247
266
|
mimeType == "image/tiff" ||
|
|
248
267
|
metadata.dimensions.width > ${exports.IMAGE_MAX_WIDTH} ||
|
|
249
268
|
size > ${exports.IMAGE_MAX_SIZE}
|
|
250
|
-
)][] {
|
|
269
|
+
)] | order(size desc) [0...500] {
|
|
251
270
|
_id, url, originalFilename, mimeType, size,
|
|
252
271
|
"width": metadata.dimensions.width
|
|
253
272
|
}`
|
|
@@ -291,7 +310,9 @@ function ImageResizerView() {
|
|
|
291
310
|
const baseName = asset.originalFilename?.replace(/\.[^.]+$/, "") || "image", newAsset = await client.assets.upload("image", blob, {
|
|
292
311
|
filename: `${baseName}.${outFormat}`,
|
|
293
312
|
contentType: `image/${outFormat}`
|
|
294
|
-
})
|
|
313
|
+
});
|
|
314
|
+
await copyAssetMetadata(client, asset._id, newAsset._id);
|
|
315
|
+
const refs = await client.fetch(
|
|
295
316
|
"*[references($id)]{ _id }",
|
|
296
317
|
{ id: asset._id }
|
|
297
318
|
);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * `asset._ref === oldId`. Returns a flat `{ path: newRef }` object\n * compatible with `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Labels shown on violation badges */\nconst VIOLATION_LABELS: Record<Violation, string> = {\n format: 'TIFF → WebP',\n width: `> ${IMAGE_MAX_WIDTH}px`,\n size: `> ${MAX_SIZE_MB} MB`,\n}\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(settings: ConversionSettings): string {\n const parts: string[] = []\n parts.push(settings.tiffToJpg ? 'TIFF → JPG' : 'TIFF → WebP')\n if (settings.pngToWebp) parts.push('PNG → WebP')\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n}: {\n type: Violation\n settings: ConversionSettings\n}) {\n const label =\n type === 'format' ? formatViolationLabel(settings) : VIOLATION_LABELS[type]\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB →{' '}\n {(asset.newSize! / 1024 / 1024).toFixed(1)} MB\n {sizeReduction !== null && sizeReduction > 0\n ? ` (−${sizeReduction}%)`\n : ''}{' '}\n — {asset.newWidth}px wide\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {asset.width}px → {asset.newWidth}px\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB — {asset.width}px\n wide\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text=\"Process\"\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && <Badge tone=\"positive\">Done</Badge>}\n {asset.status === 'error' && (\n <Button\n text=\"Retry\"\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )][] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>Image Resizer</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n Converts TIFF images to WebP. Resizes/compresses all images to fit\n within {IMAGE_MAX_WIDTH}px / {MAX_SIZE_MB} MB.\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text=\"Refresh\"\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && (\n <Button\n text={\n processingAll\n ? 'Processing…'\n : `Process All (${counts.pending})`\n }\n tone=\"primary\"\n onClick={processAll}\n disabled={processingAll || loading}\n icon={processingAll ? Spinner : undefined}\n />\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">{counts.pending} pending</Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">{counts.processing} processing</Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">{counts.done} done</Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">{counts.error} failed</Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>Scanning assets…</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">All images meet the requirements.</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header=\"Conversion Settings\"\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n Convert PNG → WebP\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n Convert TIFF → JPG (instead of WebP)\n </Label>\n </Flex>\n <Text size={1} muted>\n Changes apply on next Refresh.\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport { imageResizerUsEnglishLocaleBundle } from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["defineLocaleResourceBundle","IMAGE_ACCEPT","IMAGE_MAX_SIZE","IMAGE_MAX_WIDTH","MAX_SIZE_MB","Badge","jsx","Card","jsxs","Flex","Box","Stack","Text","Fragment","Button","Spinner","useClient","useState","useCallback","useRef","useEffect","useMemo","Container","Heading","CogIcon","Dialog","Switch","Label","definePlugin"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator, type SanityClient } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * a reference points to `oldId` (either via `asset._ref` or a direct\n * `_ref`). Returns a flat `{ path: newRef }` object compatible with\n * `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n // Match image fields with asset._ref\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n // Match direct references (e.g. inside media.tag arrays)\n if (\n record._type === 'reference' &&\n record._ref === oldId\n ) {\n const refPath = path || '_ref'\n return { [refPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n/**\n * Metadata fields to preserve when replacing an image asset.\n * Covers built-in Sanity asset fields and sanity-plugin-media tags.\n */\nconst ASSET_METADATA_FIELDS = [\n 'title',\n 'description',\n 'altText',\n 'creditLine',\n 'source',\n 'opt',\n] as const\n\n/**\n * Copies user-editable metadata (title, description, alt text, credit,\n * source, and opt.media.tags) from the old asset to the new one.\n */\nexport async function copyAssetMetadata(\n client: SanityClient,\n oldAssetId: string,\n newAssetId: string,\n): Promise<void> {\n const projection = ASSET_METADATA_FIELDS.join(', ')\n const oldMeta = await client.fetch(\n `*[_id == $id][0]{ ${projection} }`,\n { id: oldAssetId },\n )\n if (!oldMeta) return\n\n // Build a flat patch with only the fields that actually exist\n const patch: Record<string, unknown> = {}\n for (const field of ASSET_METADATA_FIELDS) {\n if (oldMeta[field] !== undefined && oldMeta[field] !== null) {\n patch[field] = oldMeta[field]\n }\n }\n\n if (Object.keys(patch).length > 0) {\n await client.patch(newAssetId).set(patch).commit()\n }\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Labels shown on violation badges */\nconst VIOLATION_LABELS: Record<Violation, string> = {\n format: 'TIFF → WebP',\n width: `> ${IMAGE_MAX_WIDTH}px`,\n size: `> ${MAX_SIZE_MB} MB`,\n}\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(settings: ConversionSettings): string {\n const parts: string[] = []\n parts.push(settings.tiffToJpg ? 'TIFF → JPG' : 'TIFF → WebP')\n if (settings.pngToWebp) parts.push('PNG → WebP')\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n}: {\n type: Violation\n settings: ConversionSettings\n}) {\n const label =\n type === 'format' ? formatViolationLabel(settings) : VIOLATION_LABELS[type]\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB →{' '}\n {(asset.newSize! / 1024 / 1024).toFixed(1)} MB\n {sizeReduction !== null && sizeReduction > 0\n ? ` (−${sizeReduction}%)`\n : ''}{' '}\n — {asset.newWidth}px wide\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {asset.width}px → {asset.newWidth}px\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB — {asset.width}px\n wide\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text=\"Process\"\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && <Badge tone=\"positive\">Done</Badge>}\n {asset.status === 'error' && (\n <Button\n text=\"Retry\"\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n copyAssetMetadata,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )] | order(size desc) [0...500] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 2b — Copy metadata (tags, alt text, credits, etc.) to the new asset\n await copyAssetMetadata(client, asset._id, newAsset._id)\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>Image Resizer</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n Converts TIFF images to WebP. Resizes/compresses all images to fit\n within {IMAGE_MAX_WIDTH}px / {MAX_SIZE_MB} MB.\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text=\"Refresh\"\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && (\n <Button\n text={\n processingAll\n ? 'Processing…'\n : `Process All (${counts.pending})`\n }\n tone=\"primary\"\n onClick={processAll}\n disabled={processingAll || loading}\n icon={processingAll ? Spinner : undefined}\n />\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">{counts.pending} pending</Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">{counts.processing} processing</Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">{counts.done} done</Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">{counts.error} failed</Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>Scanning assets…</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">All images meet the requirements.</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header=\"Conversion Settings\"\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n Convert PNG → WebP\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n Convert TIFF → JPG (instead of WebP)\n </Label>\n </Flex>\n <Text size={1} muted>\n Changes apply on next Refresh.\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport { imageResizerUsEnglishLocaleBundle } from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["defineLocaleResourceBundle","IMAGE_ACCEPT","IMAGE_MAX_SIZE","IMAGE_MAX_WIDTH","MAX_SIZE_MB","Badge","jsx","Card","jsxs","Flex","Box","Stack","Text","Fragment","Button","Spinner","useClient","useState","useCallback","useRef","useEffect","useMemo","Container","Heading","CogIcon","Dialog","Switch","Label","definePlugin"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -64,7 +64,7 @@ function buildReplacementPatch(obj, oldId, newId, path = "") {
|
|
|
64
64
|
{}
|
|
65
65
|
);
|
|
66
66
|
const record = obj;
|
|
67
|
-
return record.asset?._ref === oldId ? { [path ? `${path}.asset` : "asset"]: { _type: "reference", _ref: newId } } : Object.keys(record).filter((k) => !k.startsWith("_")).reduce(
|
|
67
|
+
return record.asset?._ref === oldId ? { [path ? `${path}.asset` : "asset"]: { _type: "reference", _ref: newId } } : record._type === "reference" && record._ref === oldId ? { [path || "_ref"]: { _type: "reference", _ref: newId } } : Object.keys(record).filter((k) => !k.startsWith("_")).reduce(
|
|
68
68
|
(acc, key) => {
|
|
69
69
|
const childPath = path ? `${path}.${key}` : key;
|
|
70
70
|
return Object.assign(
|
|
@@ -75,6 +75,25 @@ function buildReplacementPatch(obj, oldId, newId, path = "") {
|
|
|
75
75
|
{}
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
|
+
const ASSET_METADATA_FIELDS = [
|
|
79
|
+
"title",
|
|
80
|
+
"description",
|
|
81
|
+
"altText",
|
|
82
|
+
"creditLine",
|
|
83
|
+
"source",
|
|
84
|
+
"opt"
|
|
85
|
+
];
|
|
86
|
+
async function copyAssetMetadata(client, oldAssetId, newAssetId) {
|
|
87
|
+
const projection = ASSET_METADATA_FIELDS.join(", "), oldMeta = await client.fetch(
|
|
88
|
+
`*[_id == $id][0]{ ${projection} }`,
|
|
89
|
+
{ id: oldAssetId }
|
|
90
|
+
);
|
|
91
|
+
if (!oldMeta) return;
|
|
92
|
+
const patch = {};
|
|
93
|
+
for (const field of ASSET_METADATA_FIELDS)
|
|
94
|
+
oldMeta[field] !== void 0 && oldMeta[field] !== null && (patch[field] = oldMeta[field]);
|
|
95
|
+
Object.keys(patch).length > 0 && await client.patch(newAssetId).set(patch).commit();
|
|
96
|
+
}
|
|
78
97
|
const validateImageSize = async (value, context) => {
|
|
79
98
|
if (!value?.asset?._ref) return !0;
|
|
80
99
|
const asset = await context.getClient({ apiVersion: "2025-02-19" }).fetch(
|
|
@@ -245,7 +264,7 @@ function ImageResizerView() {
|
|
|
245
264
|
mimeType == "image/tiff" ||
|
|
246
265
|
metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||
|
|
247
266
|
size > ${IMAGE_MAX_SIZE}
|
|
248
|
-
)][] {
|
|
267
|
+
)] | order(size desc) [0...500] {
|
|
249
268
|
_id, url, originalFilename, mimeType, size,
|
|
250
269
|
"width": metadata.dimensions.width
|
|
251
270
|
}`
|
|
@@ -289,7 +308,9 @@ function ImageResizerView() {
|
|
|
289
308
|
const baseName = asset.originalFilename?.replace(/\.[^.]+$/, "") || "image", newAsset = await client.assets.upload("image", blob, {
|
|
290
309
|
filename: `${baseName}.${outFormat}`,
|
|
291
310
|
contentType: `image/${outFormat}`
|
|
292
|
-
})
|
|
311
|
+
});
|
|
312
|
+
await copyAssetMetadata(client, asset._id, newAsset._id);
|
|
313
|
+
const refs = await client.fetch(
|
|
293
314
|
"*[references($id)]{ _id }",
|
|
294
315
|
{ id: asset._id }
|
|
295
316
|
);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * `asset._ref === oldId`. Returns a flat `{ path: newRef }` object\n * compatible with `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Labels shown on violation badges */\nconst VIOLATION_LABELS: Record<Violation, string> = {\n format: 'TIFF → WebP',\n width: `> ${IMAGE_MAX_WIDTH}px`,\n size: `> ${MAX_SIZE_MB} MB`,\n}\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(settings: ConversionSettings): string {\n const parts: string[] = []\n parts.push(settings.tiffToJpg ? 'TIFF → JPG' : 'TIFF → WebP')\n if (settings.pngToWebp) parts.push('PNG → WebP')\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n}: {\n type: Violation\n settings: ConversionSettings\n}) {\n const label =\n type === 'format' ? formatViolationLabel(settings) : VIOLATION_LABELS[type]\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB →{' '}\n {(asset.newSize! / 1024 / 1024).toFixed(1)} MB\n {sizeReduction !== null && sizeReduction > 0\n ? ` (−${sizeReduction}%)`\n : ''}{' '}\n — {asset.newWidth}px wide\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {asset.width}px → {asset.newWidth}px\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB — {asset.width}px\n wide\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text=\"Process\"\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && <Badge tone=\"positive\">Done</Badge>}\n {asset.status === 'error' && (\n <Button\n text=\"Retry\"\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )][] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>Image Resizer</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n Converts TIFF images to WebP. Resizes/compresses all images to fit\n within {IMAGE_MAX_WIDTH}px / {MAX_SIZE_MB} MB.\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text=\"Refresh\"\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && (\n <Button\n text={\n processingAll\n ? 'Processing…'\n : `Process All (${counts.pending})`\n }\n tone=\"primary\"\n onClick={processAll}\n disabled={processingAll || loading}\n icon={processingAll ? Spinner : undefined}\n />\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">{counts.pending} pending</Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">{counts.processing} processing</Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">{counts.done} done</Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">{counts.error} failed</Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>Scanning assets…</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">All images meet the requirements.</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header=\"Conversion Settings\"\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n Convert PNG → WebP\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n Convert TIFF → JPG (instead of WebP)\n </Label>\n </Flex>\n <Text size={1} muted>\n Changes apply on next Refresh.\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport { imageResizerUsEnglishLocaleBundle } from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["MAX_SIZE_MB"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator, type SanityClient } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * a reference points to `oldId` (either via `asset._ref` or a direct\n * `_ref`). Returns a flat `{ path: newRef }` object compatible with\n * `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n // Match image fields with asset._ref\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n // Match direct references (e.g. inside media.tag arrays)\n if (\n record._type === 'reference' &&\n record._ref === oldId\n ) {\n const refPath = path || '_ref'\n return { [refPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n/**\n * Metadata fields to preserve when replacing an image asset.\n * Covers built-in Sanity asset fields and sanity-plugin-media tags.\n */\nconst ASSET_METADATA_FIELDS = [\n 'title',\n 'description',\n 'altText',\n 'creditLine',\n 'source',\n 'opt',\n] as const\n\n/**\n * Copies user-editable metadata (title, description, alt text, credit,\n * source, and opt.media.tags) from the old asset to the new one.\n */\nexport async function copyAssetMetadata(\n client: SanityClient,\n oldAssetId: string,\n newAssetId: string,\n): Promise<void> {\n const projection = ASSET_METADATA_FIELDS.join(', ')\n const oldMeta = await client.fetch(\n `*[_id == $id][0]{ ${projection} }`,\n { id: oldAssetId },\n )\n if (!oldMeta) return\n\n // Build a flat patch with only the fields that actually exist\n const patch: Record<string, unknown> = {}\n for (const field of ASSET_METADATA_FIELDS) {\n if (oldMeta[field] !== undefined && oldMeta[field] !== null) {\n patch[field] = oldMeta[field]\n }\n }\n\n if (Object.keys(patch).length > 0) {\n await client.patch(newAssetId).set(patch).commit()\n }\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Labels shown on violation badges */\nconst VIOLATION_LABELS: Record<Violation, string> = {\n format: 'TIFF → WebP',\n width: `> ${IMAGE_MAX_WIDTH}px`,\n size: `> ${MAX_SIZE_MB} MB`,\n}\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(settings: ConversionSettings): string {\n const parts: string[] = []\n parts.push(settings.tiffToJpg ? 'TIFF → JPG' : 'TIFF → WebP')\n if (settings.pngToWebp) parts.push('PNG → WebP')\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n}: {\n type: Violation\n settings: ConversionSettings\n}) {\n const label =\n type === 'format' ? formatViolationLabel(settings) : VIOLATION_LABELS[type]\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB →{' '}\n {(asset.newSize! / 1024 / 1024).toFixed(1)} MB\n {sizeReduction !== null && sizeReduction > 0\n ? ` (−${sizeReduction}%)`\n : ''}{' '}\n — {asset.newWidth}px wide\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {asset.width}px → {asset.newWidth}px\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {(asset.size / 1024 / 1024).toFixed(1)} MB — {asset.width}px\n wide\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text=\"Process\"\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && <Badge tone=\"positive\">Done</Badge>}\n {asset.status === 'error' && (\n <Button\n text=\"Retry\"\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n copyAssetMetadata,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )] | order(size desc) [0...500] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 2b — Copy metadata (tags, alt text, credits, etc.) to the new asset\n await copyAssetMetadata(client, asset._id, newAsset._id)\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>Image Resizer</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n Converts TIFF images to WebP. Resizes/compresses all images to fit\n within {IMAGE_MAX_WIDTH}px / {MAX_SIZE_MB} MB.\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text=\"Refresh\"\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && (\n <Button\n text={\n processingAll\n ? 'Processing…'\n : `Process All (${counts.pending})`\n }\n tone=\"primary\"\n onClick={processAll}\n disabled={processingAll || loading}\n icon={processingAll ? Spinner : undefined}\n />\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">{counts.pending} pending</Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">{counts.processing} processing</Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">{counts.done} done</Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">{counts.error} failed</Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>Scanning assets…</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">All images meet the requirements.</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header=\"Conversion Settings\"\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n Convert PNG → WebP\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n Convert TIFF → JPG (instead of WebP)\n </Label>\n </Flex>\n <Text size={1} muted>\n Changes apply on next Refresh.\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport { imageResizerUsEnglishLocaleBundle } from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["MAX_SIZE_MB"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CustomValidator } from 'sanity'
|
|
1
|
+
import { type CustomValidator, type SanityClient } from 'sanity'
|
|
2
2
|
|
|
3
3
|
// ─── types ──────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -150,8 +150,9 @@ export async function processImage(
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Recursively traverses a document tree and collects all paths where
|
|
153
|
-
*
|
|
154
|
-
* compatible with
|
|
153
|
+
* a reference points to `oldId` (either via `asset._ref` or a direct
|
|
154
|
+
* `_ref`). Returns a flat `{ path: newRef }` object compatible with
|
|
155
|
+
* `client.patch().set()`.
|
|
155
156
|
*/
|
|
156
157
|
export function buildReplacementPatch(
|
|
157
158
|
obj: unknown,
|
|
@@ -180,11 +181,21 @@ export function buildReplacementPatch(
|
|
|
180
181
|
|
|
181
182
|
const record = obj as Record<string, unknown>
|
|
182
183
|
|
|
184
|
+
// Match image fields with asset._ref
|
|
183
185
|
if ((record.asset as any)?._ref === oldId) {
|
|
184
186
|
const assetPath = path ? `${path}.asset` : 'asset'
|
|
185
187
|
return { [assetPath]: { _type: 'reference', _ref: newId } }
|
|
186
188
|
}
|
|
187
189
|
|
|
190
|
+
// Match direct references (e.g. inside media.tag arrays)
|
|
191
|
+
if (
|
|
192
|
+
record._type === 'reference' &&
|
|
193
|
+
record._ref === oldId
|
|
194
|
+
) {
|
|
195
|
+
const refPath = path || '_ref'
|
|
196
|
+
return { [refPath]: { _type: 'reference', _ref: newId } }
|
|
197
|
+
}
|
|
198
|
+
|
|
188
199
|
return Object.keys(record)
|
|
189
200
|
.filter((k) => !k.startsWith('_'))
|
|
190
201
|
.reduce<Record<string, { _type: 'reference'; _ref: string }>>(
|
|
@@ -199,6 +210,48 @@ export function buildReplacementPatch(
|
|
|
199
210
|
)
|
|
200
211
|
}
|
|
201
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Metadata fields to preserve when replacing an image asset.
|
|
215
|
+
* Covers built-in Sanity asset fields and sanity-plugin-media tags.
|
|
216
|
+
*/
|
|
217
|
+
const ASSET_METADATA_FIELDS = [
|
|
218
|
+
'title',
|
|
219
|
+
'description',
|
|
220
|
+
'altText',
|
|
221
|
+
'creditLine',
|
|
222
|
+
'source',
|
|
223
|
+
'opt',
|
|
224
|
+
] as const
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Copies user-editable metadata (title, description, alt text, credit,
|
|
228
|
+
* source, and opt.media.tags) from the old asset to the new one.
|
|
229
|
+
*/
|
|
230
|
+
export async function copyAssetMetadata(
|
|
231
|
+
client: SanityClient,
|
|
232
|
+
oldAssetId: string,
|
|
233
|
+
newAssetId: string,
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
const projection = ASSET_METADATA_FIELDS.join(', ')
|
|
236
|
+
const oldMeta = await client.fetch(
|
|
237
|
+
`*[_id == $id][0]{ ${projection} }`,
|
|
238
|
+
{ id: oldAssetId },
|
|
239
|
+
)
|
|
240
|
+
if (!oldMeta) return
|
|
241
|
+
|
|
242
|
+
// Build a flat patch with only the fields that actually exist
|
|
243
|
+
const patch: Record<string, unknown> = {}
|
|
244
|
+
for (const field of ASSET_METADATA_FIELDS) {
|
|
245
|
+
if (oldMeta[field] !== undefined && oldMeta[field] !== null) {
|
|
246
|
+
patch[field] = oldMeta[field]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (Object.keys(patch).length > 0) {
|
|
251
|
+
await client.patch(newAssetId).set(patch).commit()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
202
255
|
// ─── validation ─────────────────────────────────────────────────────────────
|
|
203
256
|
|
|
204
257
|
/** @public */
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
IMAGE_MAX_SIZE,
|
|
25
25
|
IMAGE_MAX_WIDTH,
|
|
26
26
|
buildReplacementPatch,
|
|
27
|
+
copyAssetMetadata,
|
|
27
28
|
getViolations,
|
|
28
29
|
processImage,
|
|
29
30
|
} from '../helpers'
|
|
@@ -100,7 +101,7 @@ export function ImageResizerView() {
|
|
|
100
101
|
mimeType == "image/tiff" ||
|
|
101
102
|
metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||
|
|
102
103
|
size > ${IMAGE_MAX_SIZE}
|
|
103
|
-
)][] {
|
|
104
|
+
)] | order(size desc) [0...500] {
|
|
104
105
|
_id, url, originalFilename, mimeType, size,
|
|
105
106
|
"width": metadata.dimensions.width
|
|
106
107
|
}`
|
|
@@ -169,6 +170,9 @@ export function ImageResizerView() {
|
|
|
169
170
|
contentType: `image/${outFormat}`,
|
|
170
171
|
})
|
|
171
172
|
|
|
173
|
+
// 2b — Copy metadata (tags, alt text, credits, etc.) to the new asset
|
|
174
|
+
await copyAssetMetadata(client, asset._id, newAsset._id)
|
|
175
|
+
|
|
172
176
|
// 3 — Re-link all referencing documents
|
|
173
177
|
const refs = await client.fetch<{ _id: string }[]>(
|
|
174
178
|
`*[references($id)]{ _id }`,
|