@adaptive-ds/assets-optimizer
v0.14.0
Published
Process and sync web project images and videos between local folders and R2
Downloads
1,269
Maintainers
Readme
@adaptive-ds/assets-optimizer
Process, hash, sync, and clean image assets for web projects that keep originals outside git and optionally sync through any rclone remote, with a separate pass for web videos.
Features
- originals may live on an
rcloneremote and be synced locally - optimized outputs should be deterministic and aggressively cacheable
- output filenames should change when either the source file or the transform changes
- old optimized files should be removed locally and remotely
- generates type-safe
imageList.tsandvideoList.tsshould stay in sync with processed assets
Quick link
- code - https://github.com/david1gp/assets-optimizer
- npm - https://www.npmjs.com/package/@adaptive-ds/assets-optimizer
Diagrams
Overview
Images
Videos
What It Does
processAssets() orchestrates the full workflow:
- Syncs originals from remote source to local
imagesandvideosfolders viarclone bisync - Runs
assetsOptimize()to process images and videos locally - Uploads optimized assets to s3-compatible remote destination with caching headers
assetsOptimize() performs the core asset processing:
- Resolves the project name from
package.json.name - If
rcloneRemoteis configured, uses that project name as the base path on the remote - If
rcloneRemoteis configured, syncs originals between the remote andimages - Scans transform folders like
1920x1080_jpg, optionally nested in grouping folders - Processes matching image source files with
sharp - Writes flat optimized images into
public/images - Names image files as
<basename>_<hash>.<ext> - Skips already-generated images
- Deletes stale optimized images locally
- If
rcloneRemoteis configured, uploads missing optimized images to the remote with cache headers - If
rcloneRemoteis configured, deletes stale optimized images from the remote - Runs a separate optional video pass from
videostopublic/videos - Generates a hashed
<basename>_<hash>.webppreview beside each processed video (full resolution,sharp-encoded) - Keeps video filenames unchanged and skips any processed video or preview that already exists
- If
rcloneRemoteis configured, uploads videos with a short cache header and their hashed previews with a long (immutable) cache header - Generates
src/app/assets/imageList.tsandsrc/app/assets/videoList.tsby default - Prints a clear summary of what changed
The hash is derived from:
- source file bytes
- normalized transform spec
That means image cache keys change when the source image changes or when you change the folder rule, even if the output filename format stays short.
Folder Convention
Original files belong under one transform folder inside images. Arbitrary grouping folders may appear before and after the transform folder.
Example:
images/
interiors/
homes/
1920x1080_jpg/
hero/
living-room.png
products/
1200x1200_webp/
kitchen.jpgThis produces flat optimized image output like:
public/images/
living-room_9f8e7d6c.jpg
kitchen_7c6b5a4d.webpFiles that are not inside a transform folder are ignored. Folders whose name contains ignore are skipped entirely.
Only the first transform folder in a path is used. If another transform-looking folder appears below it, that nested folder is skipped with a warning.
Videos are handled separately and do not use transform folders:
videos/
hero.mp4
intro.webm
public/videos/
hero.mp4
hero_a1b2c3d4.webp
intro.webm
intro_9f8e7d6c.webpVideo behavior:
- if both local
videosand remotevideo-originalsare missing, the video pass does nothing - if
rcloneRemoteis configured, source videos sync throughvideo-originals - if
rcloneRemoteis configured, processed videos sync throughvideo-processed - missing processed videos are created with
ffmpeg - missing preview images are created beside processed videos as
<basename>_<hash>.webp:ffmpegextracts a representative frame at full resolution, thensharpre-encodes it to webp atvideoPreviewQuality(default65, the same encoder/quality scale used for images) - the preview hash is derived from the processed video's content, so the preview filename busts when the video changes; previews can therefore be cached immutably even though video filenames are not hashed
- existing processed videos are skipped and preserved as manual transformations
- existing previews are skipped; stale previews from a previous hash are removed when regenerated
- video filenames and relative paths are kept as-is (optimize them by hand)
- stale processed videos are not deleted
Hash Length
Output filenames are <basename>_<hash>.<ext>, where the hash defaults to 8 hex chars.
Set imageHashLength to use a shorter (or longer) suffix:
await assetsOptimize({
imageHashLength: 3,
})images/1920x1080_webp/ship_PYM.webp -> public/images/ship_PYM_a1b.webpThe hash only busts cache when a single file's content changes — the basename already
distinguishes different images, so cross-file collisions never matter. The only risk of a
short hash is that an edited file re-hashes to the same suffix and its cache is not busted:
roughly 1 / 16^length per edit (≈ 1/4096 at length 3). The generated imageList.ts
strips exactly imageHashLength hex chars to keep stable keys, so keep this value in sync
with the existing list.
imageTypeImportPath sets the import type { ImageType } from "..." line of the
generated imageList.ts (defaults to this package's name).
Transform Folder Format
Folder names must use:
<width>x<height>_<format>Supported image output formats:
jpgpngwebpExamples:1920x1080_jpg1600x900_webpImage processing behavior:resize fit:
inside/ max-bounds scalingwithoutEnlargement: trueimage auto-rotation is applied
default quality is
80
Supported video source extensions:
mp4movm4vwebmavimkv
Installation
bun add -D @adaptive-ds/assets-optimizerBasic Usage
Example project entrypoint:
import { assetsProcess } from "@adaptive-ds/assets-optimizer"
await assetsProcess()This generates optimized images, processed videos, hashed webp video previews, imageList.ts, and videoList.ts in one run.
Existing image alt text and existing video preview alt text are preserved when the generated files already exist.
Image alt text can also be provided with a same-basename .txt or .md file next to the source image inside a transform folder. If both exist, .txt is used and .md is the fallback.
Local folders
This package is built for a workflow with two local directories:
images: original source images, never modifiedpublic/images: generated optimized images, flat output onlyvideos: original source videos, optionalpublic/videos: processed videos, optional
Optimization
Images
- Source images live below transform folders like
1920x1080_jpginsideimages/ - Grouping folders may appear before and after the transform folder
- Each source file is resized to fit within the specified bounds without enlargement
- Auto-rotation is applied based on EXIF data
- Output quality defaults to 80%
- Files are named using a hash of the source content and transform spec
- Existing optimized files are skipped unless their source changed
- Stale optimized files (from deleted sources or changed transforms) are removed
- A TypeScript list file is generated with all processed image references
Videos
- Source videos live directly in
videos/(no transform folders) - Each video is copied to the output directory using
ffmpeg - A hashed, full-resolution webp preview (
sharp-encoded atvideoPreviewQuality, default65) is generated beside each processed video - Video filenames stay unhashed (optimize length/etc. by hand); previews are content-hashed so they cache-bust on change
- Existing processed videos and previews are preserved as-is
- A TypeScript list file is generated with all processed video references
Requirements
bunrcloneffmpeg- an existing
rcloneremote - write access to the target bucket/path
- Node/Bun environment capable of running
sharp
This package assumes the remote bucket/path already exists or can be created by rclone mkdir.
Cleanup Behavior
The package does not use a manifest.
Instead it derives the expected output set from the current originals and current transform folders, then reconciles that against:
- local
public/images - remote
images/optimizedobjects
That means:
- files no longer produced by the current source set are deleted
- renaming or removing a source file cleans up stale optimized files
- changing a transform folder causes a different hash and a different output filename
Recommended Workflow
- Add or sync originals into
images/<grouping-folders>/<transform-folder>/<grouping-folders>/ - Run your local image pipeline entrypoint
- Regenerate your typed image list
- Reference the generated hashed filenames from app code or derived metadata
Important Caveat
If your project currently stores source images directly at the root of images, this package will skip them by design.
Before adopting it fully, move originals below explicit transform folders such as:
images/1920x1080_jpg/That contract is what makes the output deterministic and safe to clean automatically.
License
MIT
