imagepreparation-lazy-srcset 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # imagepreparation-lazy-srcset
2
+
3
+ This package was created specifically to prepare photos for use with the [`lazy-srcset`](https://www.npmjs.com/package/lazy-srcset) package.
4
+
5
+ A Node.js library + CLI that prepares a folder of source photos for the [`lazy-srcset`](https://www.npmjs.com/package/lazy-srcset) frontend lazy-loading pattern.
6
+
7
+ It resizes every image down to a set of responsive breakpoints, re-encodes each one to a modern format (`webp` by default), and stores everything in a content-addressable store so identical work is never redone. Each photo is content-hashed, organized into its own hash-named folder, and (optionally) "published" into the exact filename convention `lazy-srcset` expects on the frontend.
8
+
9
+ ## Features
10
+
11
+ - **Responsive breakpoints** — resizes each source image to multiple widths (default: `1920, 1440, 1280, 1024, 768, 640, 480, 320`) without ever upscaling.
12
+ - **Modern formats** — re-encodes to `webp` (or any format `sharp` supports) at a configurable quality.
13
+ - **Content-addressable storage** — resized variants are stored by content hash; running the tool again never duplicates work or storage.
14
+ - **Content-based deduplication** — two uploaded photos with identical bytes (even under different filenames) collapse into a single processed entry.
15
+ - **Resumable manifest** — a single `.img-manifest.json` per collection tracks what's already been processed, so interrupted or repeated runs pick up where they left off.
16
+ - **Originals are preserved**, not deleted — each source photo is kept (renamed) next to its generated variants, so future re-encodes (different quality/format/breakpoints) don't need the original upload again.
17
+ - **`lazy-srcset`-ready output** — an explicit publish step links generated variants into the exact `name-{width}.{ext}` sibling-file convention the frontend package expects.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install imagepreparation-lazy-srcset
23
+ ```
24
+
25
+ This installs the `image-preparation` CLI binary. `sharp` is the only runtime dependency.
26
+
27
+ ## CLI usage
28
+
29
+ ```bash
30
+ image-preparation <imagesDir> [options]
31
+ ```
32
+
33
+ or, without installing the bin globally:
34
+
35
+ ```bash
36
+ node bin/cli.js <imagesDir> [options]
37
+ ```
38
+
39
+ | Argument / Flag | Default | Description |
40
+ | - | - | - |
41
+ | `imagesDir` (positional) | `./img` | Folder containing the source photos (or path to a single image file). |
42
+ | `--format` | `webp` | Output format passed to `sharp` (e.g. `webp`, `avif`). |
43
+ | `--quality` | `82` | Encoding quality for the output format. |
44
+ | `--breakpoints` | `1920,1440,1280,1024,768,640,480,320` | Comma-separated list of target widths, in pixels. |
45
+ | `--publish` | off | After processing, link the generated variants into the `lazy-srcset` naming convention (see below). |
46
+
47
+ ### Example
48
+
49
+ ```bash
50
+ image-preparation ./img --format webp --quality 80 --breakpoints 1600,1200,800,400 --publish
51
+ ```
52
+
53
+ ## How it works
54
+
55
+ 1. **Discovery** — `<imagesDir>` is scanned for source images. Loose image files directly inside it, and one level of subfolders, are picked up as input. Already-known bucket folders (see below) and `_`-prefixed folders are skipped, so the tool's own output is never mistaken for new source material on the next run.
56
+
57
+ 2. **Content hashing** — each source file's *content* is SHA-1 hashed. That hash becomes both the photo's manifest key and its working folder name: `<imagesDir>/<hash>/`. Because the hash is based on content, uploading the same photo twice under different names always resolves to the same folder.
58
+
59
+ 3. **Resize + encode** — for every configured breakpoint, the image is resized (`withoutEnlargement: true`, so small originals are never blown up) and re-encoded to the target format/quality. If a resize produces byte-identical output to the previous (larger) breakpoint — common once the source is smaller than a given width — the existing variant is reused instead of storing a duplicate.
60
+
61
+ 4. **Content-addressable store (CAS)** — every resized variant is saved into `<hash>/.store/<h0:2>/<h2:4>/<variantHash>.<format>`, keyed by its own content hash. A variant already on disk is never rewritten.
62
+
63
+ 5. **Original preserved** — the source file is moved (not deleted) into its bucket folder as `<hash>.<originalExt>`, so it survives for future re-processing (e.g. with different quality/format/breakpoints).
64
+
65
+ 6. **Manifest** — a single `<imagesDir>/.img-manifest.json` records, per photo hash, the `{ width: variantHash }` map. This is the durable record that makes re-runs skip already-processed photos.
66
+
67
+ 7. **Publish (`--publish`)** — hard-links (falling back to a copy across filesystems) every CAS-stored variant out to `<hash>/<hash>-<width>.<format>`, right next to the preserved original. Already-correct links are left untouched, so publishing again after adding one new photo only touches that photo's folder.
68
+
69
+ ### Output layout
70
+
71
+ After processing and publishing a single photo, its folder looks like this:
72
+
73
+ ```
74
+ img/
75
+ └── 2c905c69d5921635fcb59f8176c508c3ff806f4b/
76
+ ├── .store/ # content-addressable, internal
77
+ │ └── 84/27/84279ca394ec...webp
78
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b.jpg # preserved original
79
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b-320.webp
80
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b-480.webp
81
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b-640.webp
82
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b-768.webp
83
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b-1024.webp
84
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b-1280.webp
85
+ ├── 2c905c69d5921635fcb59f8176c508c3ff806f4b-1440.webp
86
+ └── 2c905c69d5921635fcb59f8176c508c3ff806f4b-1920.webp
87
+ ```
88
+
89
+ ## Using the output with `lazy-srcset`
90
+
91
+ [`lazy-srcset`](https://www.npmjs.com/package/lazy-srcset) builds its `srcset` by appending `-{width}.{ext}` directly to whatever path is in an image's `data-srcset` attribute — it has no awareness of this tool's internal hashing. Point `data-srcset` at the published path for that photo's hash:
92
+
93
+ ```html
94
+ <img
95
+ class="lazy-srcset"
96
+ alt=""
97
+ src="data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 9"%3E%3C/svg%3E"
98
+ data-srcset="img/2c905c69d5921635fcb59f8176c508c3ff806f4b/2c905c69d5921635fcb59f8176c508c3ff806f4b.webp"
99
+ />
100
+ ```
101
+
102
+ `lazy-srcset` will then resolve `…-320.webp`, `…-480.webp`, … `…-1920.webp` automatically. The hash for a given source photo can be looked up from `.img-manifest.json` (it's the top-level key).
103
+
104
+ ## Programmatic API
105
+
106
+ ```js
107
+ import { imagePipeline } from 'imagepreparation-lazy-srcset';
108
+
109
+ await imagePipeline({
110
+ imagesCollectionDir: './img', // folder or single file path
111
+ format: 'webp',
112
+ quality: 82,
113
+ breakpoints: [1920, 1440, 1280, 1024, 768, 640, 480, 320],
114
+ inputFormats: ['jpg', 'jpeg', 'png', 'webp', 'avif'],
115
+ publish: true,
116
+ });
117
+ ```
118
+
119
+ All options are optional and merge over the defaults shown above.
120
+
121
+ ## Status
122
+
123
+ This is an early, actively-evolving package — there is no test suite, lint config, or type checker yet. Planned next steps include manifest versioning, a garbage collector for orphaned `.store` entries, and incremental rebuilds that skip files whose settings haven't changed.
124
+
125
+ ## License
126
+
127
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { imagePipeline } from '../src/index.js';
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ function getArg(flag, defaultValue) {
8
+ const index = args.indexOf(flag);
9
+ if (index === -1) return defaultValue;
10
+ return args[index + 1];
11
+ }
12
+
13
+ const dir = args[0] || './img';
14
+
15
+ const format = getArg('--format', 'webp');
16
+ const quality = Number(getArg('--quality', 82));
17
+
18
+ const breakpoints = getArg('--breakpoints', '1920, 1440, 1280, 1024, 768, 640, 480, 320')
19
+ .split(',')
20
+ .map(Number);
21
+
22
+ const publish = args.includes('--publish');
23
+
24
+ await imagePipeline({
25
+ imagesCollectionDir: dir,
26
+ format,
27
+ quality,
28
+ breakpoints,
29
+ publish,
30
+ }).catch(console.error);
@@ -0,0 +1 @@
1
+ import fs from"fs/promises";import path from"path";import{processFolder}from"./processFolder.js";import{loadManifest}from"./manifest.js";const defaults={breakpoints:[1920,1440,1280,1024,768,640,480,320],imagesCollectionDir:"img",format:"webp",quality:82,inputFormats:["jpg","jpeg","png","webp","avif"],publish:!1};export async function imagePipeline(e={}){const s={...defaults,...e},t=path.resolve(s.imagesCollectionDir);if((await fs.stat(t)).isFile())return processFolder([t],s,path.dirname(t));const i=t,o=await loadManifest(i),a=new Set(Object.keys(o.files)),r=await fs.readdir(i,{withFileTypes:!0}),n=[],p=[];for(const e of r){const s=path.join(i,e.name);if(e.isDirectory()){if(e.name.startsWith("_")||a.has(e.name))continue;n.push(s)}else p.push(s)}p.length>0&&await processFolder(p,s,i);for(const e of n)await processFolder(e,s,i)}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "imagepreparation-lazy-srcset",
3
+ "version": "0.1.0",
4
+ "homepage": "",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": ""
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/lazy-srcset.min.js",
11
+ "exports": {
12
+ ".": "./dist/lazy-srcset.min.js"
13
+ },
14
+ "description": "Automatically prepare fotos for lazy-srcset npm plugin",
15
+ "author": "",
16
+ "scripts": {
17
+ "build": "terser src/index.js -o dist/imagePreparation.min.js -c -m"
18
+ },
19
+ "bin": {
20
+ "image-preparation": "bin/cli.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "keywords": [
27
+ "lazyload",
28
+ "lazy-loading",
29
+ "images",
30
+ "responsive-images",
31
+ "srcset",
32
+ "webp",
33
+ "avif",
34
+ "placeholder",
35
+ "intersection-observer",
36
+ "performance",
37
+ "pagespeed",
38
+ "lighthouse",
39
+ "core-web-vitals",
40
+ "cls",
41
+ "lcp",
42
+ "wordpress",
43
+ "vue",
44
+ "nuxt",
45
+ "react",
46
+ "javascript"
47
+ ],
48
+ "license": "MIT",
49
+ "devDependencies": {
50
+ "terser": "^5.48.0"
51
+ },
52
+ "dependencies": {
53
+ "sharp": "^0.35.1"
54
+ }
55
+ }